##// 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 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import functools
7 7 import json
8 8 import logging
9 9 import os
10 10 import re
11 11 import sys
12 12 import traceback
13 13 try:
14 14 # py3
15 15 from http.client import responses
16 16 except ImportError:
17 17 from httplib import responses
18 18
19 19 from jinja2 import TemplateNotFound
20 20 from tornado import web
21 21
22 22 try:
23 23 from tornado.log import app_log
24 24 except ImportError:
25 25 app_log = logging.getLogger()
26 26
27 27 from IPython.config import Application
28 28 from IPython.utils.path import filefind
29 29 from IPython.utils.py3compat import string_types
30 30 from IPython.html.utils import is_hidden
31 31
32 32 #-----------------------------------------------------------------------------
33 33 # Top-level handlers
34 34 #-----------------------------------------------------------------------------
35 35 non_alphanum = re.compile(r'[^A-Za-z0-9]')
36 36
37 37 class AuthenticatedHandler(web.RequestHandler):
38 38 """A RequestHandler with an authenticated user."""
39 39
40 40 def set_default_headers(self):
41 41 headers = self.settings.get('headers', {})
42 42
43 43 if "X-Frame-Options" not in headers:
44 44 headers["X-Frame-Options"] = "SAMEORIGIN"
45 45
46 46 for header_name,value in headers.items() :
47 47 try:
48 48 self.set_header(header_name, value)
49 49 except Exception:
50 50 # tornado raise Exception (not a subclass)
51 51 # if method is unsupported (websocket and Access-Control-Allow-Origin
52 52 # for example, so just ignore)
53 53 pass
54 54
55 55 def clear_login_cookie(self):
56 56 self.clear_cookie(self.cookie_name)
57 57
58 58 def get_current_user(self):
59 59 user_id = self.get_secure_cookie(self.cookie_name)
60 60 # For now the user_id should not return empty, but it could eventually
61 61 if user_id == '':
62 62 user_id = 'anonymous'
63 63 if user_id is None:
64 64 # prevent extra Invalid cookie sig warnings:
65 65 self.clear_login_cookie()
66 66 if not self.login_available:
67 67 user_id = 'anonymous'
68 68 return user_id
69 69
70 70 @property
71 71 def cookie_name(self):
72 72 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
73 73 self.request.host
74 74 ))
75 75 return self.settings.get('cookie_name', default_cookie_name)
76 76
77 77 @property
78 78 def password(self):
79 79 """our password"""
80 80 return self.settings.get('password', '')
81 81
82 82 @property
83 83 def logged_in(self):
84 84 """Is a user currently logged in?
85 85
86 86 """
87 87 user = self.get_current_user()
88 88 return (user and not user == 'anonymous')
89 89
90 90 @property
91 91 def login_available(self):
92 92 """May a user proceed to log in?
93 93
94 94 This returns True if login capability is available, irrespective of
95 95 whether the user is already logged in or not.
96 96
97 97 """
98 98 return bool(self.settings.get('password', ''))
99 99
100 100
101 101 class IPythonHandler(AuthenticatedHandler):
102 102 """IPython-specific extensions to authenticated handling
103 103
104 104 Mostly property shortcuts to IPython-specific settings.
105 105 """
106 106
107 107 @property
108 108 def config(self):
109 109 return self.settings.get('config', None)
110 110
111 111 @property
112 112 def log(self):
113 113 """use the IPython log by default, falling back on tornado's logger"""
114 114 if Application.initialized():
115 115 return Application.instance().log
116 116 else:
117 117 return app_log
118 118
119 119 #---------------------------------------------------------------
120 120 # URLs
121 121 #---------------------------------------------------------------
122 122
123 123 @property
124 124 def mathjax_url(self):
125 125 return self.settings.get('mathjax_url', '')
126 126
127 127 @property
128 128 def base_url(self):
129 129 return self.settings.get('base_url', '/')
130 130
131 131 @property
132 132 def ws_url(self):
133 133 return self.settings.get('websocket_url', '')
134 134
135 135 #---------------------------------------------------------------
136 136 # Manager objects
137 137 #---------------------------------------------------------------
138 138
139 139 @property
140 140 def kernel_manager(self):
141 141 return self.settings['kernel_manager']
142 142
143 143 @property
144 def notebook_manager(self):
145 return self.settings['notebook_manager']
144 def contents_manager(self):
145 return self.settings['contents_manager']
146 146
147 147 @property
148 148 def cluster_manager(self):
149 149 return self.settings['cluster_manager']
150 150
151 151 @property
152 152 def session_manager(self):
153 153 return self.settings['session_manager']
154 154
155 155 @property
156 156 def kernel_spec_manager(self):
157 157 return self.settings['kernel_spec_manager']
158 158
159 159 @property
160 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 164 # CORS
165 165 #---------------------------------------------------------------
166 166
167 167 @property
168 168 def allow_origin(self):
169 169 """Normal Access-Control-Allow-Origin"""
170 170 return self.settings.get('allow_origin', '')
171 171
172 172 @property
173 173 def allow_origin_pat(self):
174 174 """Regular expression version of allow_origin"""
175 175 return self.settings.get('allow_origin_pat', None)
176 176
177 177 @property
178 178 def allow_credentials(self):
179 179 """Whether to set Access-Control-Allow-Credentials"""
180 180 return self.settings.get('allow_credentials', False)
181 181
182 182 def set_default_headers(self):
183 183 """Add CORS headers, if defined"""
184 184 super(IPythonHandler, self).set_default_headers()
185 185 if self.allow_origin:
186 186 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
187 187 elif self.allow_origin_pat:
188 188 origin = self.get_origin()
189 189 if origin and self.allow_origin_pat.match(origin):
190 190 self.set_header("Access-Control-Allow-Origin", origin)
191 191 if self.allow_credentials:
192 192 self.set_header("Access-Control-Allow-Credentials", 'true')
193 193
194 194 def get_origin(self):
195 195 # Handle WebSocket Origin naming convention differences
196 196 # The difference between version 8 and 13 is that in 8 the
197 197 # client sends a "Sec-Websocket-Origin" header and in 13 it's
198 198 # simply "Origin".
199 199 if "Origin" in self.request.headers:
200 200 origin = self.request.headers.get("Origin")
201 201 else:
202 202 origin = self.request.headers.get("Sec-Websocket-Origin", None)
203 203 return origin
204 204
205 205 #---------------------------------------------------------------
206 206 # template rendering
207 207 #---------------------------------------------------------------
208 208
209 209 def get_template(self, name):
210 210 """Return the jinja template object for a given name"""
211 211 return self.settings['jinja2_env'].get_template(name)
212 212
213 213 def render_template(self, name, **ns):
214 214 ns.update(self.template_namespace)
215 215 template = self.get_template(name)
216 216 return template.render(**ns)
217 217
218 218 @property
219 219 def template_namespace(self):
220 220 return dict(
221 221 base_url=self.base_url,
222 222 ws_url=self.ws_url,
223 223 logged_in=self.logged_in,
224 224 login_available=self.login_available,
225 225 static_url=self.static_url,
226 226 )
227 227
228 228 def get_json_body(self):
229 229 """Return the body of the request as JSON data."""
230 230 if not self.request.body:
231 231 return None
232 232 # Do we need to call body.decode('utf-8') here?
233 233 body = self.request.body.strip().decode(u'utf-8')
234 234 try:
235 235 model = json.loads(body)
236 236 except Exception:
237 237 self.log.debug("Bad JSON: %r", body)
238 238 self.log.error("Couldn't parse JSON", exc_info=True)
239 239 raise web.HTTPError(400, u'Invalid JSON in body of request')
240 240 return model
241 241
242 242 def get_error_html(self, status_code, **kwargs):
243 243 """render custom error pages"""
244 244 exception = kwargs.get('exception')
245 245 message = ''
246 246 status_message = responses.get(status_code, 'Unknown HTTP Error')
247 247 if exception:
248 248 # get the custom message, if defined
249 249 try:
250 250 message = exception.log_message % exception.args
251 251 except Exception:
252 252 pass
253 253
254 254 # construct the custom reason, if defined
255 255 reason = getattr(exception, 'reason', '')
256 256 if reason:
257 257 status_message = reason
258 258
259 259 # build template namespace
260 260 ns = dict(
261 261 status_code=status_code,
262 262 status_message=status_message,
263 263 message=message,
264 264 exception=exception,
265 265 )
266 266
267 267 # render the template
268 268 try:
269 269 html = self.render_template('%s.html' % status_code, **ns)
270 270 except TemplateNotFound:
271 271 self.log.debug("No template for %d", status_code)
272 272 html = self.render_template('error.html', **ns)
273 273 return html
274 274
275 275
276 276 class Template404(IPythonHandler):
277 277 """Render our 404 template"""
278 278 def prepare(self):
279 279 raise web.HTTPError(404)
280 280
281 281
282 282 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
283 283 """static files should only be accessible when logged in"""
284 284
285 285 @web.authenticated
286 286 def get(self, path):
287 287 if os.path.splitext(path)[1] == '.ipynb':
288 288 name = os.path.basename(path)
289 289 self.set_header('Content-Type', 'application/json')
290 290 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
291 291
292 292 return web.StaticFileHandler.get(self, path)
293 293
294 294 def compute_etag(self):
295 295 return None
296 296
297 297 def validate_absolute_path(self, root, absolute_path):
298 298 """Validate and return the absolute path.
299 299
300 300 Requires tornado 3.1
301 301
302 302 Adding to tornado's own handling, forbids the serving of hidden files.
303 303 """
304 304 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
305 305 abs_root = os.path.abspath(root)
306 306 if is_hidden(abs_path, abs_root):
307 307 self.log.info("Refusing to serve hidden file, via 404 Error")
308 308 raise web.HTTPError(404)
309 309 return abs_path
310 310
311 311
312 312 def json_errors(method):
313 313 """Decorate methods with this to return GitHub style JSON errors.
314 314
315 315 This should be used on any JSON API on any handler method that can raise HTTPErrors.
316 316
317 317 This will grab the latest HTTPError exception using sys.exc_info
318 318 and then:
319 319
320 320 1. Set the HTTP status code based on the HTTPError
321 321 2. Create and return a JSON body with a message field describing
322 322 the error in a human readable form.
323 323 """
324 324 @functools.wraps(method)
325 325 def wrapper(self, *args, **kwargs):
326 326 try:
327 327 result = method(self, *args, **kwargs)
328 328 except web.HTTPError as e:
329 329 status = e.status_code
330 330 message = e.log_message
331 331 self.log.warn(message)
332 332 self.set_status(e.status_code)
333 333 self.finish(json.dumps(dict(message=message)))
334 334 except Exception:
335 335 self.log.error("Unhandled error in API request", exc_info=True)
336 336 status = 500
337 337 message = "Unknown server error"
338 338 t, value, tb = sys.exc_info()
339 339 self.set_status(status)
340 340 tb_text = ''.join(traceback.format_exception(t, value, tb))
341 341 reply = dict(message=message, traceback=tb_text)
342 342 self.finish(json.dumps(reply))
343 343 else:
344 344 return result
345 345 return wrapper
346 346
347 347
348 348
349 349 #-----------------------------------------------------------------------------
350 350 # File handler
351 351 #-----------------------------------------------------------------------------
352 352
353 353 # to minimize subclass changes:
354 354 HTTPError = web.HTTPError
355 355
356 356 class FileFindHandler(web.StaticFileHandler):
357 357 """subclass of StaticFileHandler for serving files from a search path"""
358 358
359 359 # cache search results, don't search for files more than once
360 360 _static_paths = {}
361 361
362 362 def initialize(self, path, default_filename=None):
363 363 if isinstance(path, string_types):
364 364 path = [path]
365 365
366 366 self.root = tuple(
367 367 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
368 368 )
369 369 self.default_filename = default_filename
370 370
371 371 def compute_etag(self):
372 372 return None
373 373
374 374 @classmethod
375 375 def get_absolute_path(cls, roots, path):
376 376 """locate a file to serve on our static file search path"""
377 377 with cls._lock:
378 378 if path in cls._static_paths:
379 379 return cls._static_paths[path]
380 380 try:
381 381 abspath = os.path.abspath(filefind(path, roots))
382 382 except IOError:
383 383 # IOError means not found
384 384 return ''
385 385
386 386 cls._static_paths[path] = abspath
387 387 return abspath
388 388
389 389 def validate_absolute_path(self, root, absolute_path):
390 390 """check if the file should be served (raises 404, 403, etc.)"""
391 391 if absolute_path == '':
392 392 raise web.HTTPError(404)
393 393
394 394 for root in self.root:
395 395 if (absolute_path + os.sep).startswith(root):
396 396 break
397 397
398 398 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
399 399
400 400
401 401 class TrailingSlashHandler(web.RequestHandler):
402 402 """Simple redirect handler that strips trailing slashes
403 403
404 404 This should be the first, highest priority handler.
405 405 """
406 406
407 407 SUPPORTED_METHODS = ['GET']
408 408
409 409 def get(self):
410 410 self.redirect(self.request.uri.rstrip('/'))
411 411
412 412 #-----------------------------------------------------------------------------
413 413 # URL pattern fragments for re-use
414 414 #-----------------------------------------------------------------------------
415 415
416 416 path_regex = r"(?P<path>(?:/.*)*)"
417 417 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
418 418 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
419 419
420 420 #-----------------------------------------------------------------------------
421 421 # URL to handler mappings
422 422 #-----------------------------------------------------------------------------
423 423
424 424
425 425 default_handlers = [
426 426 (r".*/", TrailingSlashHandler)
427 427 ]
@@ -1,137 +1,137 b''
1 1 import io
2 2 import os
3 3 import zipfile
4 4
5 5 from tornado import web
6 6
7 7 from ..base.handlers import IPythonHandler, notebook_path_regex
8 8 from IPython.nbformat.current import to_notebook_json
9 9
10 10 from IPython.utils.py3compat import cast_bytes
11 11
12 12 def find_resource_files(output_files_dir):
13 13 files = []
14 14 for dirpath, dirnames, filenames in os.walk(output_files_dir):
15 15 files.extend([os.path.join(dirpath, f) for f in filenames])
16 16 return files
17 17
18 18 def respond_zip(handler, name, output, resources):
19 19 """Zip up the output and resource files and respond with the zip file.
20 20
21 21 Returns True if it has served a zip file, False if there are no resource
22 22 files, in which case we serve the plain output file.
23 23 """
24 24 # Check if we have resource files we need to zip
25 25 output_files = resources.get('outputs', None)
26 26 if not output_files:
27 27 return False
28 28
29 29 # Headers
30 30 zip_filename = os.path.splitext(name)[0] + '.zip'
31 31 handler.set_header('Content-Disposition',
32 32 'attachment; filename="%s"' % zip_filename)
33 33 handler.set_header('Content-Type', 'application/zip')
34 34
35 35 # Prepare the zip file
36 36 buffer = io.BytesIO()
37 37 zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
38 38 output_filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
39 39 zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
40 40 for filename, data in output_files.items():
41 41 zipf.writestr(os.path.basename(filename), data)
42 42 zipf.close()
43 43
44 44 handler.finish(buffer.getvalue())
45 45 return True
46 46
47 47 def get_exporter(format, **kwargs):
48 48 """get an exporter, raising appropriate errors"""
49 49 # if this fails, will raise 500
50 50 try:
51 51 from IPython.nbconvert.exporters.export import exporter_map
52 52 except ImportError as e:
53 53 raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
54 54
55 55 try:
56 56 Exporter = exporter_map[format]
57 57 except KeyError:
58 58 # should this be 400?
59 59 raise web.HTTPError(404, u"No exporter for format: %s" % format)
60 60
61 61 try:
62 62 return Exporter(**kwargs)
63 63 except Exception as e:
64 64 raise web.HTTPError(500, "Could not construct Exporter: %s" % e)
65 65
66 66 class NbconvertFileHandler(IPythonHandler):
67 67
68 68 SUPPORTED_METHODS = ('GET',)
69 69
70 70 @web.authenticated
71 71 def get(self, format, path='', name=None):
72 72
73 73 exporter = get_exporter(format, config=self.config, log=self.log)
74 74
75 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 78 self.set_header('Last-Modified', model['last_modified'])
79 79
80 80 try:
81 81 output, resources = exporter.from_notebook_node(model['content'])
82 82 except Exception as e:
83 83 raise web.HTTPError(500, "nbconvert failed: %s" % e)
84 84
85 85 if respond_zip(self, name, output, resources):
86 86 return
87 87
88 88 # Force download if requested
89 89 if self.get_argument('download', 'false').lower() == 'true':
90 90 filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
91 91 self.set_header('Content-Disposition',
92 92 'attachment; filename="%s"' % filename)
93 93
94 94 # MIME type
95 95 if exporter.output_mimetype:
96 96 self.set_header('Content-Type',
97 97 '%s; charset=utf-8' % exporter.output_mimetype)
98 98
99 99 self.finish(output)
100 100
101 101 class NbconvertPostHandler(IPythonHandler):
102 102 SUPPORTED_METHODS = ('POST',)
103 103
104 104 @web.authenticated
105 105 def post(self, format):
106 106 exporter = get_exporter(format, config=self.config)
107 107
108 108 model = self.get_json_body()
109 109 nbnode = to_notebook_json(model['content'])
110 110
111 111 try:
112 112 output, resources = exporter.from_notebook_node(nbnode)
113 113 except Exception as e:
114 114 raise web.HTTPError(500, "nbconvert failed: %s" % e)
115 115
116 116 if respond_zip(self, nbnode.metadata.name, output, resources):
117 117 return
118 118
119 119 # MIME type
120 120 if exporter.output_mimetype:
121 121 self.set_header('Content-Type',
122 122 '%s; charset=utf-8' % exporter.output_mimetype)
123 123
124 124 self.finish(output)
125 125
126 126 #-----------------------------------------------------------------------------
127 127 # URL to handler mappings
128 128 #-----------------------------------------------------------------------------
129 129
130 130 _format_regex = r"(?P<format>\w+)"
131 131
132 132
133 133 default_handlers = [
134 134 (r"/nbconvert/%s%s" % (_format_regex, notebook_path_regex),
135 135 NbconvertFileHandler),
136 136 (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
137 137 ]
@@ -1,129 +1,129 b''
1 1 # coding: utf-8
2 2 import base64
3 3 import io
4 4 import json
5 5 import os
6 6 from os.path import join as pjoin
7 7 import shutil
8 8
9 9 import requests
10 10
11 11 from IPython.html.utils import url_path_join
12 12 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
13 13 from IPython.nbformat.current import (new_notebook, write, new_worksheet,
14 14 new_heading_cell, new_code_cell,
15 15 new_output)
16 16
17 17 from IPython.testing.decorators import onlyif_cmds_exist
18 18
19 19
20 20 class NbconvertAPI(object):
21 21 """Wrapper for nbconvert API calls."""
22 22 def __init__(self, base_url):
23 23 self.base_url = base_url
24 24
25 25 def _req(self, verb, path, body=None, params=None):
26 26 response = requests.request(verb,
27 27 url_path_join(self.base_url, 'nbconvert', path),
28 28 data=body, params=params,
29 29 )
30 30 response.raise_for_status()
31 31 return response
32 32
33 33 def from_file(self, format, path, name, download=False):
34 34 return self._req('GET', url_path_join(format, path, name),
35 35 params={'download':download})
36 36
37 37 def from_post(self, format, nbmodel):
38 38 body = json.dumps(nbmodel)
39 39 return self._req('POST', format, body)
40 40
41 41 def list_formats(self):
42 42 return self._req('GET', '')
43 43
44 44 png_green_pixel = base64.encodestring(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00'
45 45 b'\x00\x00\x01\x00\x00x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDAT'
46 46 b'\x08\xd7c\x90\xfb\xcf\x00\x00\x02\\\x01\x1e.~d\x87\x00\x00\x00\x00IEND\xaeB`\x82')
47 47
48 48 class APITest(NotebookTestBase):
49 49 def setUp(self):
50 50 nbdir = self.notebook_dir.name
51 51
52 52 if not os.path.isdir(pjoin(nbdir, 'foo')):
53 53 os.mkdir(pjoin(nbdir, 'foo'))
54 54
55 55 nb = new_notebook(name='testnb')
56 56
57 57 ws = new_worksheet()
58 58 nb.worksheets = [ws]
59 59 ws.cells.append(new_heading_cell(u'Created by test Β³'))
60 60 cc1 = new_code_cell(input=u'print(2*6)')
61 61 cc1.outputs.append(new_output(output_text=u'12', output_type='stream'))
62 62 cc1.outputs.append(new_output(output_png=png_green_pixel, output_type='pyout'))
63 63 ws.cells.append(cc1)
64 64
65 65 with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w',
66 66 encoding='utf-8') as f:
67 67 write(nb, f, format='ipynb')
68 68
69 69 self.nbconvert_api = NbconvertAPI(self.base_url())
70 70
71 71 def tearDown(self):
72 72 nbdir = self.notebook_dir.name
73 73
74 74 for dname in ['foo']:
75 75 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
76 76
77 77 @onlyif_cmds_exist('pandoc')
78 78 def test_from_file(self):
79 79 r = self.nbconvert_api.from_file('html', 'foo', 'testnb.ipynb')
80 80 self.assertEqual(r.status_code, 200)
81 81 self.assertIn(u'text/html', r.headers['Content-Type'])
82 82 self.assertIn(u'Created by test', r.text)
83 83 self.assertIn(u'print', r.text)
84 84
85 85 r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb')
86 86 self.assertIn(u'text/x-python', r.headers['Content-Type'])
87 87 self.assertIn(u'print(2*6)', r.text)
88 88
89 89 @onlyif_cmds_exist('pandoc')
90 90 def test_from_file_404(self):
91 91 with assert_http_error(404):
92 92 self.nbconvert_api.from_file('html', 'foo', 'thisdoesntexist.ipynb')
93 93
94 94 @onlyif_cmds_exist('pandoc')
95 95 def test_from_file_download(self):
96 96 r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb', download=True)
97 97 content_disposition = r.headers['Content-Disposition']
98 98 self.assertIn('attachment', content_disposition)
99 99 self.assertIn('testnb.py', content_disposition)
100 100
101 101 @onlyif_cmds_exist('pandoc')
102 102 def test_from_file_zip(self):
103 103 r = self.nbconvert_api.from_file('latex', 'foo', 'testnb.ipynb', download=True)
104 104 self.assertIn(u'application/zip', r.headers['Content-Type'])
105 105 self.assertIn(u'.zip', r.headers['Content-Disposition'])
106 106
107 107 @onlyif_cmds_exist('pandoc')
108 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 110 nbmodel = requests.get(nbmodel_url).json()
111 111
112 112 r = self.nbconvert_api.from_post(format='html', nbmodel=nbmodel)
113 113 self.assertEqual(r.status_code, 200)
114 114 self.assertIn(u'text/html', r.headers['Content-Type'])
115 115 self.assertIn(u'Created by test', r.text)
116 116 self.assertIn(u'print', r.text)
117 117
118 118 r = self.nbconvert_api.from_post(format='python', nbmodel=nbmodel)
119 119 self.assertIn(u'text/x-python', r.headers['Content-Type'])
120 120 self.assertIn(u'print(2*6)', r.text)
121 121
122 122 @onlyif_cmds_exist('pandoc')
123 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 125 nbmodel = requests.get(nbmodel_url).json()
126 126
127 127 r = self.nbconvert_api.from_post(format='latex', nbmodel=nbmodel)
128 128 self.assertIn(u'application/zip', r.headers['Content-Type'])
129 129 self.assertIn(u'.zip', r.headers['Content-Disposition'])
@@ -1,90 +1,90 b''
1 1 """Tornado handlers for the live notebook view.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import os
20 20 from tornado import web
21 21 HTTPError = web.HTTPError
22 22
23 23 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
24 24 from ..utils import url_path_join, url_escape
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Handlers
28 28 #-----------------------------------------------------------------------------
29 29
30 30
31 31 class NotebookHandler(IPythonHandler):
32 32
33 33 @web.authenticated
34 34 def get(self, path='', name=None):
35 35 """get renders the notebook template if a name is given, or
36 36 redirects to the '/files/' handler if the name is not given."""
37 37 path = path.strip('/')
38 nbm = self.notebook_manager
38 cm = self.contents_manager
39 39 if name is None:
40 40 raise web.HTTPError(500, "This shouldn't be accessible: %s" % self.request.uri)
41 41
42 42 # a .ipynb filename was given
43 if not nbm.notebook_exists(name, path):
43 if not cm.file_exists(name, path):
44 44 raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name))
45 45 name = url_escape(name)
46 46 path = url_escape(path)
47 47 self.write(self.render_template('notebook.html',
48 48 project=self.project_dir,
49 49 notebook_path=path,
50 50 notebook_name=name,
51 51 kill_kernel=False,
52 52 mathjax_url=self.mathjax_url,
53 53 )
54 54 )
55 55
56 56 class NotebookRedirectHandler(IPythonHandler):
57 57 def get(self, path=''):
58 nbm = self.notebook_manager
59 if nbm.path_exists(path):
58 cm = self.contents_manager
59 if cm.path_exists(path):
60 60 # it's a *directory*, redirect to /tree
61 61 url = url_path_join(self.base_url, 'tree', path)
62 62 else:
63 63 # otherwise, redirect to /files
64 64 if '/files/' in path:
65 65 # redirect without files/ iff it would 404
66 66 # this preserves pre-2.0-style 'files/' links
67 67 # FIXME: this is hardcoded based on notebook_path,
68 68 # but so is the files handler itself,
69 69 # so it should work until both are cleaned up.
70 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 72 if not os.path.exists(files_path):
73 73 self.log.warn("Deprecated files/ URL: %s", path)
74 74 path = path.replace('/files/', '/', 1)
75 75
76 76 url = url_path_join(self.base_url, 'files', path)
77 77 url = url_escape(url)
78 78 self.log.debug("Redirecting %s to %s", self.request.path, url)
79 79 self.redirect(url)
80 80
81 81 #-----------------------------------------------------------------------------
82 82 # URL to handler mappings
83 83 #-----------------------------------------------------------------------------
84 84
85 85
86 86 default_handlers = [
87 87 (r"/notebooks%s" % notebook_path_regex, NotebookHandler),
88 88 (r"/notebooks%s" % path_regex, NotebookRedirectHandler),
89 89 ]
90 90
@@ -1,943 +1,940 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 from __future__ import print_function
8 8
9 9 import base64
10 10 import errno
11 11 import io
12 12 import json
13 13 import logging
14 14 import os
15 15 import random
16 16 import re
17 17 import select
18 18 import signal
19 19 import socket
20 20 import sys
21 21 import threading
22 22 import time
23 23 import webbrowser
24 24
25 25
26 26 # check for pyzmq 2.1.11
27 27 from IPython.utils.zmqrelated import check_for_zmq
28 28 check_for_zmq('2.1.11', 'IPython.html')
29 29
30 30 from jinja2 import Environment, FileSystemLoader
31 31
32 32 # Install the pyzmq ioloop. This has to be done before anything else from
33 33 # tornado is imported.
34 34 from zmq.eventloop import ioloop
35 35 ioloop.install()
36 36
37 37 # check for tornado 3.1.0
38 38 msg = "The IPython Notebook requires tornado >= 3.1.0"
39 39 try:
40 40 import tornado
41 41 except ImportError:
42 42 raise ImportError(msg)
43 43 try:
44 44 version_info = tornado.version_info
45 45 except AttributeError:
46 46 raise ImportError(msg + ", but you have < 1.1.0")
47 47 if version_info < (3,1,0):
48 48 raise ImportError(msg + ", but you have %s" % tornado.version)
49 49
50 50 from tornado import httpserver
51 51 from tornado import web
52 52 from tornado.log import LogFormatter
53 53
54 54 from IPython.html import DEFAULT_STATIC_FILES_PATH
55 55 from .base.handlers import Template404
56 56 from .log import log_request
57 57 from .services.kernels.kernelmanager import MappingKernelManager
58 from .services.notebooks.nbmanager import NotebookManager
59 from .services.notebooks.filenbmanager import FileNotebookManager
58 from .services.contents.manager import ContentsManager
59 from .services.contents.filemanager import FileContentsManager
60 60 from .services.clusters.clustermanager import ClusterManager
61 61 from .services.sessions.sessionmanager import SessionManager
62 62
63 63 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
64 64
65 65 from IPython.config import Config
66 66 from IPython.config.application import catch_config_error, boolean_flag
67 67 from IPython.core.application import (
68 68 BaseIPythonApplication, base_flags, base_aliases,
69 69 )
70 70 from IPython.core.profiledir import ProfileDir
71 71 from IPython.kernel import KernelManager
72 72 from IPython.kernel.kernelspec import KernelSpecManager
73 73 from IPython.kernel.zmq.session import default_secure, Session
74 74 from IPython.nbformat.sign import NotebookNotary
75 75 from IPython.utils.importstring import import_item
76 76 from IPython.utils import submodule
77 77 from IPython.utils.process import check_pid
78 78 from IPython.utils.traitlets import (
79 79 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
80 80 DottedObjectName, TraitError,
81 81 )
82 82 from IPython.utils import py3compat
83 83 from IPython.utils.path import filefind, get_ipython_dir
84 84
85 85 from .utils import url_path_join
86 86
87 87 #-----------------------------------------------------------------------------
88 88 # Module globals
89 89 #-----------------------------------------------------------------------------
90 90
91 91 _examples = """
92 92 ipython notebook # start the notebook
93 93 ipython notebook --profile=sympy # use the sympy profile
94 94 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
95 95 """
96 96
97 97 #-----------------------------------------------------------------------------
98 98 # Helper functions
99 99 #-----------------------------------------------------------------------------
100 100
101 101 def random_ports(port, n):
102 102 """Generate a list of n random ports near the given port.
103 103
104 104 The first 5 ports will be sequential, and the remaining n-5 will be
105 105 randomly selected in the range [port-2*n, port+2*n].
106 106 """
107 107 for i in range(min(5, n)):
108 108 yield port + i
109 109 for i in range(n-5):
110 110 yield max(1, port + random.randint(-2*n, 2*n))
111 111
112 112 def load_handlers(name):
113 113 """Load the (URL pattern, handler) tuples for each component."""
114 114 name = 'IPython.html.' + name
115 115 mod = __import__(name, fromlist=['default_handlers'])
116 116 return mod.default_handlers
117 117
118 118 #-----------------------------------------------------------------------------
119 119 # The Tornado web application
120 120 #-----------------------------------------------------------------------------
121 121
122 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 125 cluster_manager, session_manager, kernel_spec_manager, log,
126 126 base_url, settings_overrides, jinja_env_options):
127 127
128 128 settings = self.init_settings(
129 ipython_app, kernel_manager, notebook_manager, cluster_manager,
129 ipython_app, kernel_manager, contents_manager, cluster_manager,
130 130 session_manager, kernel_spec_manager, log, base_url,
131 131 settings_overrides, jinja_env_options)
132 132 handlers = self.init_handlers(settings)
133 133
134 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 137 cluster_manager, session_manager, kernel_spec_manager,
138 138 log, base_url, settings_overrides,
139 139 jinja_env_options=None):
140 140 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
141 141 # base_url will always be unicode, which will in turn
142 142 # make the patterns unicode, and ultimately result in unicode
143 143 # keys in kwargs to handler._execute(**kwargs) in tornado.
144 144 # This enforces that base_url be ascii in that situation.
145 145 #
146 146 # Note that the URLs these patterns check against are escaped,
147 147 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
148 148 base_url = py3compat.unicode_to_str(base_url, 'ascii')
149 149 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
150 150 jenv_opt = jinja_env_options if jinja_env_options else {}
151 151 env = Environment(loader=FileSystemLoader(template_path),**jenv_opt )
152 152 settings = dict(
153 153 # basics
154 154 log_function=log_request,
155 155 base_url=base_url,
156 156 template_path=template_path,
157 157 static_path=ipython_app.static_file_path,
158 158 static_handler_class = FileFindHandler,
159 159 static_url_prefix = url_path_join(base_url,'/static/'),
160 160
161 161 # authentication
162 162 cookie_secret=ipython_app.cookie_secret,
163 163 login_url=url_path_join(base_url,'/login'),
164 164 password=ipython_app.password,
165 165
166 166 # managers
167 167 kernel_manager=kernel_manager,
168 notebook_manager=notebook_manager,
168 contents_manager=contents_manager,
169 169 cluster_manager=cluster_manager,
170 170 session_manager=session_manager,
171 171 kernel_spec_manager=kernel_spec_manager,
172 172
173 173 # IPython stuff
174 174 nbextensions_path = ipython_app.nbextensions_path,
175 175 websocket_url=ipython_app.websocket_url,
176 176 mathjax_url=ipython_app.mathjax_url,
177 177 config=ipython_app.config,
178 178 jinja2_env=env,
179 179 )
180 180
181 181 # allow custom overrides for the tornado web app.
182 182 settings.update(settings_overrides)
183 183 return settings
184 184
185 185 def init_handlers(self, settings):
186 186 # Load the (URL pattern, handler) tuples for each component.
187 187 handlers = []
188 188 handlers.extend(load_handlers('base.handlers'))
189 189 handlers.extend(load_handlers('tree.handlers'))
190 190 handlers.extend(load_handlers('auth.login'))
191 191 handlers.extend(load_handlers('auth.logout'))
192 192 handlers.extend(load_handlers('notebook.handlers'))
193 193 handlers.extend(load_handlers('nbconvert.handlers'))
194 194 handlers.extend(load_handlers('kernelspecs.handlers'))
195 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 197 handlers.extend(load_handlers('services.clusters.handlers'))
198 198 handlers.extend(load_handlers('services.sessions.handlers'))
199 199 handlers.extend(load_handlers('services.nbconvert.handlers'))
200 200 handlers.extend(load_handlers('services.kernelspecs.handlers'))
201 201 # FIXME: /files/ should be handled by the Contents service when it exists
202 nbm = settings['notebook_manager']
203 if hasattr(nbm, 'notebook_dir'):
204 handlers.extend([
205 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : nbm.notebook_dir}),
202 cm = settings['contents_manager']
203 if hasattr(cm, 'root_dir'):
204 handlers.append(
205 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : cm.root_dir}),
206 )
207 handlers.append(
206 208 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
207 ])
209 )
208 210 # prepend base_url onto the patterns that we match
209 211 new_handlers = []
210 212 for handler in handlers:
211 213 pattern = url_path_join(settings['base_url'], handler[0])
212 214 new_handler = tuple([pattern] + list(handler[1:]))
213 215 new_handlers.append(new_handler)
214 216 # add 404 on the end, which will catch everything that falls through
215 217 new_handlers.append((r'(.*)', Template404))
216 218 return new_handlers
217 219
218 220
219 221 class NbserverListApp(BaseIPythonApplication):
220 222
221 223 description="List currently running notebook servers in this profile."
222 224
223 225 flags = dict(
224 226 json=({'NbserverListApp': {'json': True}},
225 227 "Produce machine-readable JSON output."),
226 228 )
227 229
228 230 json = Bool(False, config=True,
229 231 help="If True, each line of output will be a JSON object with the "
230 232 "details from the server info file.")
231 233
232 234 def start(self):
233 235 if not self.json:
234 236 print("Currently running servers:")
235 237 for serverinfo in list_running_servers(self.profile):
236 238 if self.json:
237 239 print(json.dumps(serverinfo))
238 240 else:
239 241 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
240 242
241 243 #-----------------------------------------------------------------------------
242 244 # Aliases and Flags
243 245 #-----------------------------------------------------------------------------
244 246
245 247 flags = dict(base_flags)
246 248 flags['no-browser']=(
247 249 {'NotebookApp' : {'open_browser' : False}},
248 250 "Don't open the notebook in a browser after startup."
249 251 )
250 252 flags['pylab']=(
251 253 {'NotebookApp' : {'pylab' : 'warn'}},
252 254 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
253 255 )
254 256 flags['no-mathjax']=(
255 257 {'NotebookApp' : {'enable_mathjax' : False}},
256 258 """Disable MathJax
257 259
258 260 MathJax is the javascript library IPython uses to render math/LaTeX. It is
259 261 very large, so you may want to disable it if you have a slow internet
260 262 connection, or for offline use of the notebook.
261 263
262 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 268 aliases = dict(base_aliases)
272 269
273 270 aliases.update({
274 271 'ip': 'NotebookApp.ip',
275 272 'port': 'NotebookApp.port',
276 273 'port-retries': 'NotebookApp.port_retries',
277 274 'transport': 'KernelManager.transport',
278 275 'keyfile': 'NotebookApp.keyfile',
279 276 'certfile': 'NotebookApp.certfile',
280 277 'notebook-dir': 'NotebookApp.notebook_dir',
281 278 'browser': 'NotebookApp.browser',
282 279 'pylab': 'NotebookApp.pylab',
283 280 })
284 281
285 282 #-----------------------------------------------------------------------------
286 283 # NotebookApp
287 284 #-----------------------------------------------------------------------------
288 285
289 286 class NotebookApp(BaseIPythonApplication):
290 287
291 288 name = 'ipython-notebook'
292 289
293 290 description = """
294 291 The IPython HTML Notebook.
295 292
296 293 This launches a Tornado based HTML Notebook Server that serves up an
297 294 HTML5/Javascript Notebook client.
298 295 """
299 296 examples = _examples
300 297 aliases = aliases
301 298 flags = flags
302 299
303 300 classes = [
304 301 KernelManager, ProfileDir, Session, MappingKernelManager,
305 NotebookManager, FileNotebookManager, NotebookNotary,
302 ContentsManager, FileContentsManager, NotebookNotary,
306 303 ]
307 304 flags = Dict(flags)
308 305 aliases = Dict(aliases)
309 306
310 307 subcommands = dict(
311 308 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
312 309 )
313 310
314 311 kernel_argv = List(Unicode)
315 312
316 313 _log_formatter_cls = LogFormatter
317 314
318 315 def _log_level_default(self):
319 316 return logging.INFO
320 317
321 318 def _log_datefmt_default(self):
322 319 """Exclude date from default date format"""
323 320 return "%H:%M:%S"
324 321
325 322 def _log_format_default(self):
326 323 """override default log format to include time"""
327 324 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
328 325
329 326 # create requested profiles by default, if they don't exist:
330 327 auto_create = Bool(True)
331 328
332 329 # file to be opened in the notebook server
333 330 file_to_run = Unicode('', config=True)
334 331 def _file_to_run_changed(self, name, old, new):
335 332 path, base = os.path.split(new)
336 333 if path:
337 334 self.file_to_run = base
338 335 self.notebook_dir = path
339 336
340 337 # Network related information
341 338
342 339 allow_origin = Unicode('', config=True,
343 340 help="""Set the Access-Control-Allow-Origin header
344 341
345 342 Use '*' to allow any origin to access your server.
346 343
347 344 Takes precedence over allow_origin_pat.
348 345 """
349 346 )
350 347
351 348 allow_origin_pat = Unicode('', config=True,
352 349 help="""Use a regular expression for the Access-Control-Allow-Origin header
353 350
354 351 Requests from an origin matching the expression will get replies with:
355 352
356 353 Access-Control-Allow-Origin: origin
357 354
358 355 where `origin` is the origin of the request.
359 356
360 357 Ignored if allow_origin is set.
361 358 """
362 359 )
363 360
364 361 allow_credentials = Bool(False, config=True,
365 362 help="Set the Access-Control-Allow-Credentials: true header"
366 363 )
367 364
368 365 ip = Unicode('localhost', config=True,
369 366 help="The IP address the notebook server will listen on."
370 367 )
371 368
372 369 def _ip_changed(self, name, old, new):
373 370 if new == u'*': self.ip = u''
374 371
375 372 port = Integer(8888, config=True,
376 373 help="The port the notebook server will listen on."
377 374 )
378 375 port_retries = Integer(50, config=True,
379 376 help="The number of additional ports to try if the specified port is not available."
380 377 )
381 378
382 379 certfile = Unicode(u'', config=True,
383 380 help="""The full path to an SSL/TLS certificate file."""
384 381 )
385 382
386 383 keyfile = Unicode(u'', config=True,
387 384 help="""The full path to a private key file for usage with SSL/TLS."""
388 385 )
389 386
390 387 cookie_secret_file = Unicode(config=True,
391 388 help="""The file where the cookie secret is stored."""
392 389 )
393 390 def _cookie_secret_file_default(self):
394 391 if self.profile_dir is None:
395 392 return ''
396 393 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
397 394
398 395 cookie_secret = Bytes(b'', config=True,
399 396 help="""The random bytes used to secure cookies.
400 397 By default this is a new random number every time you start the Notebook.
401 398 Set it to a value in a config file to enable logins to persist across server sessions.
402 399
403 400 Note: Cookie secrets should be kept private, do not share config files with
404 401 cookie_secret stored in plaintext (you can read the value from a file).
405 402 """
406 403 )
407 404 def _cookie_secret_default(self):
408 405 if os.path.exists(self.cookie_secret_file):
409 406 with io.open(self.cookie_secret_file, 'rb') as f:
410 407 return f.read()
411 408 else:
412 409 secret = base64.encodestring(os.urandom(1024))
413 410 self._write_cookie_secret_file(secret)
414 411 return secret
415 412
416 413 def _write_cookie_secret_file(self, secret):
417 414 """write my secret to my secret_file"""
418 415 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
419 416 with io.open(self.cookie_secret_file, 'wb') as f:
420 417 f.write(secret)
421 418 try:
422 419 os.chmod(self.cookie_secret_file, 0o600)
423 420 except OSError:
424 421 self.log.warn(
425 422 "Could not set permissions on %s",
426 423 self.cookie_secret_file
427 424 )
428 425
429 426 password = Unicode(u'', config=True,
430 427 help="""Hashed password to use for web authentication.
431 428
432 429 To generate, type in a python/IPython shell:
433 430
434 431 from IPython.lib import passwd; passwd()
435 432
436 433 The string should be of the form type:salt:hashed-password.
437 434 """
438 435 )
439 436
440 437 open_browser = Bool(True, config=True,
441 438 help="""Whether to open in a browser after starting.
442 439 The specific browser used is platform dependent and
443 440 determined by the python standard library `webbrowser`
444 441 module, unless it is overridden using the --browser
445 442 (NotebookApp.browser) configuration option.
446 443 """)
447 444
448 445 browser = Unicode(u'', config=True,
449 446 help="""Specify what command to use to invoke a web
450 447 browser when opening the notebook. If not specified, the
451 448 default browser will be determined by the `webbrowser`
452 449 standard library module, which allows setting of the
453 450 BROWSER environment variable to override it.
454 451 """)
455 452
456 453 webapp_settings = Dict(config=True,
457 454 help="Supply overrides for the tornado.web.Application that the "
458 455 "IPython notebook uses.")
459 456
460 457 jinja_environment_options = Dict(config=True,
461 458 help="Supply extra arguments that will be passed to Jinja environment.")
462 459
463 460
464 461 enable_mathjax = Bool(True, config=True,
465 462 help="""Whether to enable MathJax for typesetting math/TeX
466 463
467 464 MathJax is the javascript library IPython uses to render math/LaTeX. It is
468 465 very large, so you may want to disable it if you have a slow internet
469 466 connection, or for offline use of the notebook.
470 467
471 468 When disabled, equations etc. will appear as their untransformed TeX source.
472 469 """
473 470 )
474 471 def _enable_mathjax_changed(self, name, old, new):
475 472 """set mathjax url to empty if mathjax is disabled"""
476 473 if not new:
477 474 self.mathjax_url = u''
478 475
479 476 base_url = Unicode('/', config=True,
480 477 help='''The base URL for the notebook server.
481 478
482 479 Leading and trailing slashes can be omitted,
483 480 and will automatically be added.
484 481 ''')
485 482 def _base_url_changed(self, name, old, new):
486 483 if not new.startswith('/'):
487 484 self.base_url = '/'+new
488 485 elif not new.endswith('/'):
489 486 self.base_url = new+'/'
490 487
491 488 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
492 489 def _base_project_url_changed(self, name, old, new):
493 490 self.log.warn("base_project_url is deprecated, use base_url")
494 491 self.base_url = new
495 492
496 493 extra_static_paths = List(Unicode, config=True,
497 494 help="""Extra paths to search for serving static files.
498 495
499 496 This allows adding javascript/css to be available from the notebook server machine,
500 497 or overriding individual files in the IPython"""
501 498 )
502 499 def _extra_static_paths_default(self):
503 500 return [os.path.join(self.profile_dir.location, 'static')]
504 501
505 502 @property
506 503 def static_file_path(self):
507 504 """return extra paths + the default location"""
508 505 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
509 506
510 507 nbextensions_path = List(Unicode, config=True,
511 508 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
512 509 )
513 510 def _nbextensions_path_default(self):
514 511 return [os.path.join(get_ipython_dir(), 'nbextensions')]
515 512
516 513 websocket_url = Unicode("", config=True,
517 514 help="""The base URL for websockets,
518 515 if it differs from the HTTP server (hint: it almost certainly doesn't).
519 516
520 517 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
521 518 """
522 519 )
523 520 mathjax_url = Unicode("", config=True,
524 521 help="""The url for MathJax.js."""
525 522 )
526 523 def _mathjax_url_default(self):
527 524 if not self.enable_mathjax:
528 525 return u''
529 526 static_url_prefix = self.webapp_settings.get("static_url_prefix",
530 527 url_path_join(self.base_url, "static")
531 528 )
532 529
533 530 # try local mathjax, either in nbextensions/mathjax or static/mathjax
534 531 for (url_prefix, search_path) in [
535 532 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
536 533 (static_url_prefix, self.static_file_path),
537 534 ]:
538 535 self.log.debug("searching for local mathjax in %s", search_path)
539 536 try:
540 537 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
541 538 except IOError:
542 539 continue
543 540 else:
544 541 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
545 542 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
546 543 return url
547 544
548 545 # no local mathjax, serve from CDN
549 546 url = u"//cdn.mathjax.org/mathjax/latest/MathJax.js"
550 547 self.log.info("Using MathJax from CDN: %s", url)
551 548 return url
552 549
553 550 def _mathjax_url_changed(self, name, old, new):
554 551 if new and not self.enable_mathjax:
555 552 # enable_mathjax=False overrides mathjax_url
556 553 self.mathjax_url = u''
557 554 else:
558 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 558 config=True,
562 559 help='The notebook manager class to use.'
563 560 )
564 561 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
565 562 config=True,
566 563 help='The kernel manager class to use.'
567 564 )
568 565 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
569 566 config=True,
570 567 help='The session manager class to use.'
571 568 )
572 569 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
573 570 config=True,
574 571 help='The cluster manager class to use.'
575 572 )
576 573
577 574 kernel_spec_manager = Instance(KernelSpecManager)
578 575
579 576 def _kernel_spec_manager_default(self):
580 577 return KernelSpecManager(ipython_dir=self.ipython_dir)
581 578
582 579 trust_xheaders = Bool(False, config=True,
583 580 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
584 581 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
585 582 )
586 583
587 584 info_file = Unicode()
588 585
589 586 def _info_file_default(self):
590 587 info_file = "nbserver-%s.json"%os.getpid()
591 588 return os.path.join(self.profile_dir.security_dir, info_file)
592 589
593 590 notebook_dir = Unicode(py3compat.getcwd(), config=True,
594 591 help="The directory to use for notebooks and kernels."
595 592 )
596 593
597 594 pylab = Unicode('disabled', config=True,
598 595 help="""
599 596 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
600 597 """
601 598 )
602 599 def _pylab_changed(self, name, old, new):
603 600 """when --pylab is specified, display a warning and exit"""
604 601 if new != 'warn':
605 602 backend = ' %s' % new
606 603 else:
607 604 backend = ''
608 605 self.log.error("Support for specifying --pylab on the command line has been removed.")
609 606 self.log.error(
610 607 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
611 608 )
612 609 self.exit(1)
613 610
614 611 def _notebook_dir_changed(self, name, old, new):
615 612 """Do a bit of validation of the notebook dir."""
616 613 if not os.path.isabs(new):
617 614 # If we receive a non-absolute path, make it absolute.
618 615 self.notebook_dir = os.path.abspath(new)
619 616 return
620 617 if not os.path.isdir(new):
621 618 raise TraitError("No such notebook dir: %r" % new)
622 619
623 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 622 self.config.MappingKernelManager.root_dir = new
626 623
627 624
628 625 def parse_command_line(self, argv=None):
629 626 super(NotebookApp, self).parse_command_line(argv)
630 627
631 628 if self.extra_args:
632 629 arg0 = self.extra_args[0]
633 630 f = os.path.abspath(arg0)
634 631 self.argv.remove(arg0)
635 632 if not os.path.exists(f):
636 633 self.log.critical("No such file or directory: %s", f)
637 634 self.exit(1)
638 635
639 636 # Use config here, to ensure that it takes higher priority than
640 637 # anything that comes from the profile.
641 638 c = Config()
642 639 if os.path.isdir(f):
643 640 c.NotebookApp.notebook_dir = f
644 641 elif os.path.isfile(f):
645 642 c.NotebookApp.file_to_run = f
646 643 self.update_config(c)
647 644
648 645 def init_kernel_argv(self):
649 646 """construct the kernel arguments"""
650 647 # Kernel should get *absolute* path to profile directory
651 648 self.kernel_argv = ["--profile-dir", self.profile_dir.location]
652 649
653 650 def init_configurables(self):
654 651 # force Session default to be secure
655 652 default_secure(self.config)
656 653 kls = import_item(self.kernel_manager_class)
657 654 self.kernel_manager = kls(
658 655 parent=self, log=self.log, kernel_argv=self.kernel_argv,
659 656 connection_dir = self.profile_dir.security_dir,
660 657 )
661 kls = import_item(self.notebook_manager_class)
662 self.notebook_manager = kls(parent=self, log=self.log)
658 kls = import_item(self.contents_manager_class)
659 self.contents_manager = kls(parent=self, log=self.log)
663 660 kls = import_item(self.session_manager_class)
664 661 self.session_manager = kls(parent=self, log=self.log,
665 662 kernel_manager=self.kernel_manager,
666 notebook_manager=self.notebook_manager)
663 contents_manager=self.contents_manager)
667 664 kls = import_item(self.cluster_manager_class)
668 665 self.cluster_manager = kls(parent=self, log=self.log)
669 666 self.cluster_manager.update_profiles()
670 667
671 668 def init_logging(self):
672 669 # This prevents double log messages because tornado use a root logger that
673 670 # self.log is a child of. The logging module dipatches log messages to a log
674 671 # and all of its ancenstors until propagate is set to False.
675 672 self.log.propagate = False
676 673
677 674 # hook up tornado 3's loggers to our app handlers
678 675 logger = logging.getLogger('tornado')
679 676 logger.propagate = True
680 677 logger.parent = self.log
681 678 logger.setLevel(self.log.level)
682 679
683 680 def init_webapp(self):
684 681 """initialize tornado webapp and httpserver"""
685 682 self.webapp_settings['allow_origin'] = self.allow_origin
686 683 if self.allow_origin_pat:
687 684 self.webapp_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
688 685 self.webapp_settings['allow_credentials'] = self.allow_credentials
689 686
690 687 self.web_app = NotebookWebApplication(
691 self, self.kernel_manager, self.notebook_manager,
688 self, self.kernel_manager, self.contents_manager,
692 689 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
693 690 self.log, self.base_url, self.webapp_settings,
694 691 self.jinja_environment_options
695 692 )
696 693 if self.certfile:
697 694 ssl_options = dict(certfile=self.certfile)
698 695 if self.keyfile:
699 696 ssl_options['keyfile'] = self.keyfile
700 697 else:
701 698 ssl_options = None
702 699 self.web_app.password = self.password
703 700 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
704 701 xheaders=self.trust_xheaders)
705 702 if not self.ip:
706 703 warning = "WARNING: The notebook server is listening on all IP addresses"
707 704 if ssl_options is None:
708 705 self.log.critical(warning + " and not using encryption. This "
709 706 "is not recommended.")
710 707 if not self.password:
711 708 self.log.critical(warning + " and not using authentication. "
712 709 "This is highly insecure and not recommended.")
713 710 success = None
714 711 for port in random_ports(self.port, self.port_retries+1):
715 712 try:
716 713 self.http_server.listen(port, self.ip)
717 714 except socket.error as e:
718 715 if e.errno == errno.EADDRINUSE:
719 716 self.log.info('The port %i is already in use, trying another random port.' % port)
720 717 continue
721 718 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
722 719 self.log.warn("Permission to listen on port %i denied" % port)
723 720 continue
724 721 else:
725 722 raise
726 723 else:
727 724 self.port = port
728 725 success = True
729 726 break
730 727 if not success:
731 728 self.log.critical('ERROR: the notebook server could not be started because '
732 729 'no available port could be found.')
733 730 self.exit(1)
734 731
735 732 @property
736 733 def display_url(self):
737 734 ip = self.ip if self.ip else '[all ip addresses on your system]'
738 735 return self._url(ip)
739 736
740 737 @property
741 738 def connection_url(self):
742 739 ip = self.ip if self.ip else 'localhost'
743 740 return self._url(ip)
744 741
745 742 def _url(self, ip):
746 743 proto = 'https' if self.certfile else 'http'
747 744 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
748 745
749 746 def init_signal(self):
750 747 if not sys.platform.startswith('win'):
751 748 signal.signal(signal.SIGINT, self._handle_sigint)
752 749 signal.signal(signal.SIGTERM, self._signal_stop)
753 750 if hasattr(signal, 'SIGUSR1'):
754 751 # Windows doesn't support SIGUSR1
755 752 signal.signal(signal.SIGUSR1, self._signal_info)
756 753 if hasattr(signal, 'SIGINFO'):
757 754 # only on BSD-based systems
758 755 signal.signal(signal.SIGINFO, self._signal_info)
759 756
760 757 def _handle_sigint(self, sig, frame):
761 758 """SIGINT handler spawns confirmation dialog"""
762 759 # register more forceful signal handler for ^C^C case
763 760 signal.signal(signal.SIGINT, self._signal_stop)
764 761 # request confirmation dialog in bg thread, to avoid
765 762 # blocking the App
766 763 thread = threading.Thread(target=self._confirm_exit)
767 764 thread.daemon = True
768 765 thread.start()
769 766
770 767 def _restore_sigint_handler(self):
771 768 """callback for restoring original SIGINT handler"""
772 769 signal.signal(signal.SIGINT, self._handle_sigint)
773 770
774 771 def _confirm_exit(self):
775 772 """confirm shutdown on ^C
776 773
777 774 A second ^C, or answering 'y' within 5s will cause shutdown,
778 775 otherwise original SIGINT handler will be restored.
779 776
780 777 This doesn't work on Windows.
781 778 """
782 779 info = self.log.info
783 780 info('interrupted')
784 781 print(self.notebook_info())
785 782 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
786 783 sys.stdout.flush()
787 784 r,w,x = select.select([sys.stdin], [], [], 5)
788 785 if r:
789 786 line = sys.stdin.readline()
790 787 if line.lower().startswith('y') and 'n' not in line.lower():
791 788 self.log.critical("Shutdown confirmed")
792 789 ioloop.IOLoop.instance().stop()
793 790 return
794 791 else:
795 792 print("No answer for 5s:", end=' ')
796 793 print("resuming operation...")
797 794 # no answer, or answer is no:
798 795 # set it back to original SIGINT handler
799 796 # use IOLoop.add_callback because signal.signal must be called
800 797 # from main thread
801 798 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
802 799
803 800 def _signal_stop(self, sig, frame):
804 801 self.log.critical("received signal %s, stopping", sig)
805 802 ioloop.IOLoop.instance().stop()
806 803
807 804 def _signal_info(self, sig, frame):
808 805 print(self.notebook_info())
809 806
810 807 def init_components(self):
811 808 """Check the components submodule, and warn if it's unclean"""
812 809 status = submodule.check_submodule_status()
813 810 if status == 'missing':
814 811 self.log.warn("components submodule missing, running `git submodule update`")
815 812 submodule.update_submodules(submodule.ipython_parent())
816 813 elif status == 'unclean':
817 814 self.log.warn("components submodule unclean, you may see 404s on static/components")
818 815 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
819 816
820 817 @catch_config_error
821 818 def initialize(self, argv=None):
822 819 super(NotebookApp, self).initialize(argv)
823 820 self.init_logging()
824 821 self.init_kernel_argv()
825 822 self.init_configurables()
826 823 self.init_components()
827 824 self.init_webapp()
828 825 self.init_signal()
829 826
830 827 def cleanup_kernels(self):
831 828 """Shutdown all kernels.
832 829
833 830 The kernels will shutdown themselves when this process no longer exists,
834 831 but explicit shutdown allows the KernelManagers to cleanup the connection files.
835 832 """
836 833 self.log.info('Shutting down kernels')
837 834 self.kernel_manager.shutdown_all()
838 835
839 836 def notebook_info(self):
840 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 839 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
843 840 return info + "The IPython Notebook is running at: %s" % self.display_url
844 841
845 842 def server_info(self):
846 843 """Return a JSONable dict of information about this server."""
847 844 return {'url': self.connection_url,
848 845 'hostname': self.ip if self.ip else 'localhost',
849 846 'port': self.port,
850 847 'secure': bool(self.certfile),
851 848 'base_url': self.base_url,
852 849 'notebook_dir': os.path.abspath(self.notebook_dir),
853 850 'pid': os.getpid()
854 851 }
855 852
856 853 def write_server_info_file(self):
857 854 """Write the result of server_info() to the JSON file info_file."""
858 855 with open(self.info_file, 'w') as f:
859 856 json.dump(self.server_info(), f, indent=2)
860 857
861 858 def remove_server_info_file(self):
862 859 """Remove the nbserver-<pid>.json file created for this server.
863 860
864 861 Ignores the error raised when the file has already been removed.
865 862 """
866 863 try:
867 864 os.unlink(self.info_file)
868 865 except OSError as e:
869 866 if e.errno != errno.ENOENT:
870 867 raise
871 868
872 869 def start(self):
873 870 """ Start the IPython Notebook server app, after initialization
874 871
875 872 This method takes no arguments so all configuration and initialization
876 873 must be done prior to calling this method."""
877 874 if self.subapp is not None:
878 875 return self.subapp.start()
879 876
880 877 info = self.log.info
881 878 for line in self.notebook_info().split("\n"):
882 879 info(line)
883 880 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
884 881
885 882 self.write_server_info_file()
886 883
887 884 if self.open_browser or self.file_to_run:
888 885 try:
889 886 browser = webbrowser.get(self.browser or None)
890 887 except webbrowser.Error as e:
891 888 self.log.warn('No web browser found: %s.' % e)
892 889 browser = None
893 890
894 891 if self.file_to_run:
895 892 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
896 893 if not os.path.exists(fullpath):
897 894 self.log.critical("%s does not exist" % fullpath)
898 895 self.exit(1)
899 896
900 897 uri = url_path_join('notebooks', self.file_to_run)
901 898 else:
902 899 uri = 'tree'
903 900 if browser:
904 901 b = lambda : browser.open(url_path_join(self.connection_url, uri),
905 902 new=2)
906 903 threading.Thread(target=b).start()
907 904 try:
908 905 ioloop.IOLoop.instance().start()
909 906 except KeyboardInterrupt:
910 907 info("Interrupted...")
911 908 finally:
912 909 self.cleanup_kernels()
913 910 self.remove_server_info_file()
914 911
915 912
916 913 def list_running_servers(profile='default'):
917 914 """Iterate over the server info files of running notebook servers.
918 915
919 916 Given a profile name, find nbserver-* files in the security directory of
920 917 that profile, and yield dicts of their information, each one pertaining to
921 918 a currently running notebook server instance.
922 919 """
923 920 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
924 921 for file in os.listdir(pd.security_dir):
925 922 if file.startswith('nbserver-'):
926 923 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
927 924 info = json.load(f)
928 925
929 926 # Simple check whether that process is really still running
930 927 if check_pid(info['pid']):
931 928 yield info
932 929 else:
933 930 # If the process has died, try to delete its info file
934 931 try:
935 932 os.unlink(file)
936 933 except OSError:
937 934 pass # TODO: This should warn or log or something
938 935 #-----------------------------------------------------------------------------
939 936 # Main entry point
940 937 #-----------------------------------------------------------------------------
941 938
942 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 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import io
7 7 import os
8 8 import glob
9 9 import shutil
10 10
11 11 from tornado import web
12 12
13 from .nbmanager import NotebookManager
13 from .manager import ContentsManager
14 14 from IPython.nbformat import current
15 15 from IPython.utils.path import ensure_dir_exists
16 16 from IPython.utils.traitlets import Unicode, Bool, TraitError
17 17 from IPython.utils.py3compat import getcwd
18 18 from IPython.utils import tz
19 19 from IPython.html.utils import is_hidden, to_os_path
20 20
21 21 def sort_key(item):
22 22 """Case-insensitive sorting."""
23 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,
32 help="""Automatically create a Python script when saving the notebook.
28 root_dir = Unicode(getcwd(), config=True)
33 29
34 For easier use of import, %run and %load across notebooks, a
35 <notebook-name>.py script will be created next to any
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."""
30 def _root_dir_changed(self, name, old, new):
31 """Do a bit of validation of the root_dir."""
44 32 if not os.path.isabs(new):
45 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 35 return
48 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 39 checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
52 40 help="""The directory name in which to keep notebook checkpoints
53 41
54 42 This is a path relative to the notebook's own directory.
55 43
56 44 By default, it is .ipynb_checkpoints
57 45 """
58 46 )
59 47
60 48 def _copy(self, src, dest):
61 49 """copy src to dest
62 50
63 51 like shutil.copy2, but log errors in copystat
64 52 """
65 53 shutil.copyfile(src, dest)
66 54 try:
67 55 shutil.copystat(src, dest)
68 56 except OSError as e:
69 57 self.log.debug("copystat on %s failed", dest, exc_info=True)
70 58
71 def get_notebook_names(self, path=''):
72 """List all notebook names in the notebook dir and path."""
59 def get_names(self, path=''):
60 """List all filenames in the path (relative to root_dir)."""
73 61 path = path.strip('/')
74 62 if not os.path.isdir(self._get_os_path(path=path)):
75 63 raise web.HTTPError(404, 'Directory not found: ' + path)
76 names = glob.glob(self._get_os_path('*'+self.filename_ext, path))
77 names = [os.path.basename(name)
78 for name in names]
64 names = glob.glob(self._get_os_path('*', path))
65 names = [ os.path.basename(name) for name in names if os.path.isfile(name)]
79 66 return names
80 67
81 68 def path_exists(self, path):
82 69 """Does the API-style path (directory) actually exist?
83 70
84 71 Parameters
85 72 ----------
86 73 path : string
87 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 77 Returns
91 78 -------
92 79 exists : bool
93 80 Whether the path is indeed a directory.
94 81 """
95 82 path = path.strip('/')
96 83 os_path = self._get_os_path(path=path)
97 84 return os.path.isdir(os_path)
98 85
99 86 def is_hidden(self, path):
100 87 """Does the API style path correspond to a hidden directory or file?
101 88
102 89 Parameters
103 90 ----------
104 91 path : string
105 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 95 Returns
109 96 -------
110 97 exists : bool
111 98 Whether the path is hidden.
112 99
113 100 """
114 101 path = path.strip('/')
115 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 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 107 path.
121 108
122 109 Parameters
123 110 ----------
124 111 name : string
125 The name of a notebook file with the .ipynb extension
112 A filename
126 113 path : string
127 114 The relative URL path (with '/' as separator) to the named
128 notebook.
115 file.
129 116
130 117 Returns
131 118 -------
132 119 path : string
133 A file system path that combines notebook_dir (location where
134 server started), the relative path, and the filename with the
135 current operating system's url.
120 API path to be evaluated relative to root_dir.
136 121 """
137 122 if name is not None:
138 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=''):
142 """Returns a True if the notebook exists. Else, returns False.
126 def file_exists(self, name, path=''):
127 """Returns a True if the file exists, else returns False.
143 128
144 129 Parameters
145 130 ----------
146 131 name : string
147 The name of the notebook you are checking.
132 The name of the file you are checking.
148 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 136 Returns
152 137 -------
153 138 bool
154 139 """
155 140 path = path.strip('/')
156 141 nbpath = self._get_os_path(name, path=path)
157 142 return os.path.isfile(nbpath)
158 143
159 144 # TODO: Remove this after we create the contents web service and directories are
160 145 # no longer listed by the notebook web service.
161 146 def list_dirs(self, path):
162 147 """List the directories for a given API style path."""
163 148 path = path.strip('/')
164 149 os_path = self._get_os_path('', path)
165 150 if not os.path.isdir(os_path):
166 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 153 self.log.info("Refusing to serve hidden directory, via 404 Error")
169 154 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
170 155 dir_names = os.listdir(os_path)
171 156 dirs = []
172 157 for name in dir_names:
173 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 160 and self.should_list(name):
176 161 try:
177 162 model = self.get_dir_model(name, path)
178 163 except IOError:
179 164 pass
180 165 dirs.append(model)
181 166 dirs = sorted(dirs, key=sort_key)
182 167 return dirs
183 168
184 169 # TODO: Remove this after we create the contents web service and directories are
185 170 # no longer listed by the notebook web service.
186 171 def get_dir_model(self, name, path=''):
187 172 """Get the directory model given a directory name and its API style path"""
188 173 path = path.strip('/')
189 174 os_path = self._get_os_path(name, path)
190 175 if not os.path.isdir(os_path):
191 176 raise IOError('directory does not exist: %r' % os_path)
192 177 info = os.stat(os_path)
193 178 last_modified = tz.utcfromtimestamp(info.st_mtime)
194 179 created = tz.utcfromtimestamp(info.st_ctime)
195 180 # Create the notebook model.
196 181 model ={}
197 182 model['name'] = name
198 183 model['path'] = path
199 184 model['last_modified'] = last_modified
200 185 model['created'] = created
201 186 model['type'] = 'directory'
202 187 return model
203 188
204 def list_notebooks(self, path):
189 def list_files(self, path):
205 190 """Returns a list of dictionaries that are the standard model
206 191 for all notebooks in the relative 'path'.
207 192
208 193 Parameters
209 194 ----------
210 195 path : str
211 196 the URL path that describes the relative path for the
212 197 listed notebooks
213 198
214 199 Returns
215 200 -------
216 201 notebooks : list of dicts
217 202 a list of the notebook models without 'content'
218 203 """
219 204 path = path.strip('/')
220 notebook_names = self.get_notebook_names(path)
221 notebooks = [self.get_notebook(name, path, content=False)
222 for name in notebook_names if self.should_list(name)]
205 names = self.get_names(path)
206 notebooks = [self.get(name, path, content=False)
207 for name in names if self.should_list(name)]
223 208 notebooks = sorted(notebooks, key=sort_key)
224 209 return notebooks
225 210
226 def get_notebook(self, name, path='', content=True):
211 def get(self, name, path='', content=True):
227 212 """ Takes a path and name for a notebook and returns its model
228 213
229 214 Parameters
230 215 ----------
231 216 name : str
232 217 the name of the notebook
233 218 path : str
234 219 the URL path that describes the relative path for
235 220 the notebook
236 221
237 222 Returns
238 223 -------
239 224 model : dict
240 225 the notebook model. If contents=True, returns the 'contents'
241 226 dict in the model as well.
242 227 """
243 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 230 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
246 231 os_path = self._get_os_path(name, path)
247 232 info = os.stat(os_path)
248 233 last_modified = tz.utcfromtimestamp(info.st_mtime)
249 234 created = tz.utcfromtimestamp(info.st_ctime)
250 235 # Create the notebook model.
251 236 model ={}
252 237 model['name'] = name
253 238 model['path'] = path
254 239 model['last_modified'] = last_modified
255 240 model['created'] = created
256 241 model['type'] = 'notebook'
257 242 if content:
258 243 with io.open(os_path, 'r', encoding='utf-8') as f:
259 244 try:
260 245 nb = current.read(f, u'json')
261 246 except Exception as e:
262 247 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
263 248 self.mark_trusted_cells(nb, name, path)
264 249 model['content'] = nb
265 250 return model
266 251
267 def save_notebook(self, model, name='', path=''):
252 def save(self, model, name='', path=''):
268 253 """Save the notebook model and return the model with no content."""
269 254 path = path.strip('/')
270 255
271 256 if 'content' not in model:
272 257 raise web.HTTPError(400, u'No notebook JSON data provided')
273 258
274 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 261 self.create_checkpoint(name, path)
277 262
278 263 new_path = model.get('path', path).strip('/')
279 264 new_name = model.get('name', name)
280 265
281 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 269 # Save the notebook file
285 270 os_path = self._get_os_path(new_name, new_path)
286 271 nb = current.to_notebook_json(model['content'])
287 272
288 273 self.check_and_sign(nb, new_name, new_path)
289 274
290 275 if 'name' in nb['metadata']:
291 276 nb['metadata']['name'] = u''
292 277 try:
293 278 self.log.debug("Autosaving notebook %s", os_path)
294 279 with io.open(os_path, 'w', encoding='utf-8') as f:
295 280 current.write(nb, f, u'json')
296 281 except Exception as e:
297 282 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
298 283
299 # Save .py script as well
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)
284 model = self.get(new_name, new_path, content=False)
310 285 return model
311 286
312 def update_notebook(self, model, name, path=''):
313 """Update the notebook's path and/or name"""
287 def update(self, model, name, path=''):
288 """Update the file's path and/or name"""
314 289 path = path.strip('/')
315 290 new_name = model.get('name', name)
316 291 new_path = model.get('path', path).strip('/')
317 292 if path != new_path or name != new_name:
318 self.rename_notebook(name, path, new_name, new_path)
319 model = self.get_notebook(new_name, new_path, content=False)
293 self.rename(name, path, new_name, new_path)
294 model = self.get(new_name, new_path, content=False)
320 295 return model
321 296
322 def delete_notebook(self, name, path=''):
323 """Delete notebook by name and path."""
297 def delete(self, name, path=''):
298 """Delete file by name and path."""
324 299 path = path.strip('/')
325 300 os_path = self._get_os_path(name, path)
326 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 304 # clear checkpoints
330 305 for checkpoint in self.list_checkpoints(name, path):
331 306 checkpoint_id = checkpoint['id']
332 307 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
333 308 if os.path.isfile(cp_path):
334 309 self.log.debug("Unlinking checkpoint %s", cp_path)
335 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 313 os.unlink(os_path)
339 314
340 def rename_notebook(self, old_name, old_path, new_name, new_path):
341 """Rename a notebook."""
315 def rename(self, old_name, old_path, new_name, new_path):
316 """Rename a file."""
342 317 old_path = old_path.strip('/')
343 318 new_path = new_path.strip('/')
344 319 if new_name == old_name and new_path == old_path:
345 320 return
346 321
347 322 new_os_path = self._get_os_path(new_name, new_path)
348 323 old_os_path = self._get_os_path(old_name, old_path)
349 324
350 325 # Should we proceed with the move?
351 326 if os.path.isfile(new_os_path):
352 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 330 try:
361 331 shutil.move(old_os_path, new_os_path)
362 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 335 # Move the checkpoints
366 336 old_checkpoints = self.list_checkpoints(old_name, old_path)
367 337 for cp in old_checkpoints:
368 338 checkpoint_id = cp['id']
369 339 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
370 340 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
371 341 if os.path.isfile(old_cp_path):
372 342 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
373 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 345 # Checkpoint-related utilities
380 346
381 347 def get_checkpoint_path(self, checkpoint_id, name, path=''):
382 348 """find the path to a checkpoint"""
383 349 path = path.strip('/')
384 basename, _ = os.path.splitext(name)
350 basename, ext = os.path.splitext(name)
385 351 filename = u"{name}-{checkpoint_id}{ext}".format(
386 352 name=basename,
387 353 checkpoint_id=checkpoint_id,
388 ext=self.filename_ext,
354 ext=ext,
389 355 )
390 356 os_path = self._get_os_path(path=path)
391 357 cp_dir = os.path.join(os_path, self.checkpoint_dir)
392 358 ensure_dir_exists(cp_dir)
393 359 cp_path = os.path.join(cp_dir, filename)
394 360 return cp_path
395 361
396 362 def get_checkpoint_model(self, checkpoint_id, name, path=''):
397 363 """construct the info dict for a given checkpoint"""
398 364 path = path.strip('/')
399 365 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
400 366 stats = os.stat(cp_path)
401 367 last_modified = tz.utcfromtimestamp(stats.st_mtime)
402 368 info = dict(
403 369 id = checkpoint_id,
404 370 last_modified = last_modified,
405 371 )
406 372 return info
407 373
408 374 # public checkpoint API
409 375
410 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 378 path = path.strip('/')
413 nb_path = self._get_os_path(name, path)
379 src_path = self._get_os_path(name, path)
414 380 # only the one checkpoint ID:
415 381 checkpoint_id = u"checkpoint"
416 382 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
417 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 386 # return the checkpoint info
421 387 return self.get_checkpoint_model(checkpoint_id, name, path)
422 388
423 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 394 path = path.strip('/')
429 395 checkpoint_id = "checkpoint"
430 396 os_path = self.get_checkpoint_path(checkpoint_id, name, path)
431 397 if not os.path.exists(os_path):
432 398 return []
433 399 else:
434 400 return [self.get_checkpoint_model(checkpoint_id, name, path)]
435 401
436 402
437 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 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 407 nb_path = self._get_os_path(name, path)
442 408 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
443 409 if not os.path.isfile(cp_path):
444 410 self.log.debug("checkpoint file does not exist: %s", cp_path)
445 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 414 # ensure notebook is readable (never restore from an unreadable notebook)
415 if cp_path.endswith('.ipynb'):
449 416 with io.open(cp_path, 'r', encoding='utf-8') as f:
450 417 current.read(f, u'json')
451 418 self._copy(cp_path, nb_path)
452 419 self.log.debug("copying %s -> %s", cp_path, nb_path)
453 420
454 421 def delete_checkpoint(self, checkpoint_id, name, path=''):
455 """delete a notebook's checkpoint"""
422 """delete a file's checkpoint"""
456 423 path = path.strip('/')
457 424 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
458 425 if not os.path.isfile(cp_path):
459 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 429 self.log.debug("unlinking %s", cp_path)
463 430 os.unlink(cp_path)
464 431
465 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 435 def get_kernel_path(self, name, path='', model=None):
469 """ Return the path to start kernel in """
470 return os.path.join(self.notebook_dir, path)
436 """Return the initial working dir a kernel associated with a given notebook"""
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:
4
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 #-----------------------------------------------------------------------------
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
18 5
19 6 import json
20 7
21 8 from tornado import web
22 9
23 10 from IPython.html.utils import url_path_join, url_escape
24 11 from IPython.utils.jsonutil import date_default
25 12
26 13 from IPython.html.base.handlers import (IPythonHandler, json_errors,
27 14 notebook_path_regex, path_regex,
28 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 20 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
38 21
39 def notebook_location(self, name, path=''):
40 """Return the full URL location of a notebook based.
22 def location_url(self, name, path=''):
23 """Return the full URL location of a file.
41 24
42 25 Parameters
43 26 ----------
44 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 29 path : unicode
47 The URL path of the notebook.
30 The API path of the file, such as "foo/bar".
48 31 """
49 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 36 def _finish_model(self, model, location=True):
54 37 """Finish a JSON request with a model, setting relevant headers, etc."""
55 38 if location:
56 location = self.notebook_location(model['name'], model['path'])
39 location = self.location_url(model['name'], model['path'])
57 40 self.set_header('Location', location)
58 41 self.set_header('Last-Modified', model['last_modified'])
59 42 self.finish(json.dumps(model, default=date_default))
60 43
61 44 @web.authenticated
62 45 @json_errors
63 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
67 * GET with path and notebook name returns notebook JSON
49 * GET with path and no filename lists files in a directory
50 * GET with path and filename returns file contents model
68 51 """
69 nbm = self.notebook_manager
70 # Check to see if a notebook name was given
52 cm = self.contents_manager
53 # Check to see if a filename was given
71 54 if name is None:
72 55 # TODO: Remove this after we create the contents web service and directories are
73 56 # no longer listed by the notebook web service. This should only handle notebooks
74 57 # and not directories.
75 dirs = nbm.list_dirs(path)
76 notebooks = []
58 dirs = cm.list_dirs(path)
59 files = []
77 60 index = []
78 for nb in nbm.list_notebooks(path):
61 for nb in cm.list_files(path):
79 62 if nb['name'].lower() == 'index.ipynb':
80 63 index.append(nb)
81 64 else:
82 notebooks.append(nb)
83 notebooks = index + dirs + notebooks
84 self.finish(json.dumps(notebooks, default=date_default))
65 files.append(nb)
66 files = index + dirs + files
67 self.finish(json.dumps(files, default=date_default))
85 68 return
86 69 # get and return notebook representation
87 model = nbm.get_notebook(name, path)
70 model = cm.get(name, path)
88 71 self._finish_model(model, location=False)
89 72
90 73 @web.authenticated
91 74 @json_errors
92 75 def patch(self, path='', name=None):
93 76 """PATCH renames a notebook without re-uploading content."""
94 nbm = self.notebook_manager
77 cm = self.contents_manager
95 78 if name is None:
96 raise web.HTTPError(400, u'Notebook name missing')
79 raise web.HTTPError(400, u'Filename missing')
97 80 model = self.get_json_body()
98 81 if model is None:
99 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 84 self._finish_model(model)
102 85
103 def _copy_notebook(self, copy_from, path, copy_to=None):
104 """Copy a notebook in path, optionally specifying the new name.
86 def _copy(self, copy_from, path, copy_to=None):
87 """Copy a file in path, optionally specifying the new name.
105 88
106 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 92 path, copy_from,
110 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 96 self.set_status(201)
114 97 self._finish_model(model)
115 98
116 def _upload_notebook(self, model, path, name=None):
117 """Upload a notebook
99 def _upload(self, model, path, name=None):
100 """Upload a file
118 101
119 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 105 if name:
123 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 109 self.set_status(201)
127 110 self._finish_model(model)
128 111
129 112 def _create_empty_notebook(self, path, name=None):
130 113 """Create an empty notebook in path
131 114
132 115 If name specified, create it in path/name.
133 116 """
134 117 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
135 118 model = {}
136 119 if name:
137 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 122 self.set_status(201)
140 123 self._finish_model(model)
141 124
142 def _save_notebook(self, model, path, name):
143 """Save an existing notebook."""
144 self.log.info(u"Saving notebook at %s/%s", path, name)
145 model = self.notebook_manager.save_notebook(model, name, path)
125 def _save(self, model, path, name):
126 """Save an existing file."""
127 self.log.info(u"Saving file at %s/%s", path, name)
128 model = self.contents_manager.save(model, name, path)
146 129 if model['path'] != path.strip('/') or model['name'] != name:
147 130 # a rename happened, set Location header
148 131 location = True
149 132 else:
150 133 location = False
151 134 self._finish_model(model, location)
152 135
153 136 @web.authenticated
154 137 @json_errors
155 138 def post(self, path='', name=None):
156 139 """Create a new notebook in the specified path.
157 140
158 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 144 New untitled notebook in path. If content specified, upload a
162 145 notebook, otherwise start empty.
163 POST /api/notebooks/path?copy=OtherNotebook.ipynb
146 POST /api/contents/path?copy=OtherNotebook.ipynb
164 147 New copy of OtherNotebook in path
165 148 """
166 149
167 150 if name is not None:
168 151 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
169 152
170 153 model = self.get_json_body()
171 154
172 155 if model is not None:
173 156 copy_from = model.get('copy_from')
174 157 if copy_from:
175 158 if model.get('content'):
176 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 161 else:
179 self._upload_notebook(model, path)
162 self._upload(model, path)
180 163 else:
181 164 self._create_empty_notebook(path)
182 165
183 166 @web.authenticated
184 167 @json_errors
185 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 171 PUT is very similar to POST, but the requester specifies the name,
189 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 175 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
193 176 in `content` key of JSON request body. If content is not specified,
194 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 179 Copy OtherNotebook to Name
197 180 """
198 181 if name is None:
199 182 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
200 183
201 184 model = self.get_json_body()
202 185 if model:
203 186 copy_from = model.get('copy_from')
204 187 if copy_from:
205 188 if model.get('content'):
206 189 raise web.HTTPError(400, "Can't upload and copy at the same time.")
207 self._copy_notebook(copy_from, path, name)
208 elif self.notebook_manager.notebook_exists(name, path):
209 self._save_notebook(model, path, name)
190 self._copy(copy_from, path, name)
191 elif self.contents_manager.file_exists(name, path):
192 self._save(model, path, name)
210 193 else:
211 self._upload_notebook(model, path, name)
194 self._upload(model, path, name)
212 195 else:
213 196 self._create_empty_notebook(path, name)
214 197
215 198 @web.authenticated
216 199 @json_errors
217 200 def delete(self, path='', name=None):
218 """delete the notebook in the given notebook path"""
219 nbm = self.notebook_manager
220 nbm.delete_notebook(name, path)
201 """delete a file in the given path"""
202 cm = self.contents_manager
203 cm.delete(name, path)
221 204 self.set_status(204)
222 205 self.finish()
223 206
224 207
225 class NotebookCheckpointsHandler(IPythonHandler):
208 class CheckpointsHandler(IPythonHandler):
226 209
227 210 SUPPORTED_METHODS = ('GET', 'POST')
228 211
229 212 @web.authenticated
230 213 @json_errors
231 214 def get(self, path='', name=None):
232 """get lists checkpoints for a notebook"""
233 nbm = self.notebook_manager
234 checkpoints = nbm.list_checkpoints(name, path)
215 """get lists checkpoints for a file"""
216 cm = self.contents_manager
217 checkpoints = cm.list_checkpoints(name, path)
235 218 data = json.dumps(checkpoints, default=date_default)
236 219 self.finish(data)
237 220
238 221 @web.authenticated
239 222 @json_errors
240 223 def post(self, path='', name=None):
241 224 """post creates a new checkpoint"""
242 nbm = self.notebook_manager
243 checkpoint = nbm.create_checkpoint(name, path)
225 cm = self.contents_manager
226 checkpoint = cm.create_checkpoint(name, path)
244 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 229 path, name, 'checkpoints', checkpoint['id'])
247 230 self.set_header('Location', url_escape(location))
248 231 self.set_status(201)
249 232 self.finish(data)
250 233
251 234
252 class ModifyNotebookCheckpointsHandler(IPythonHandler):
235 class ModifyCheckpointsHandler(IPythonHandler):
253 236
254 237 SUPPORTED_METHODS = ('POST', 'DELETE')
255 238
256 239 @web.authenticated
257 240 @json_errors
258 241 def post(self, path, name, checkpoint_id):
259 """post restores a notebook from a checkpoint"""
260 nbm = self.notebook_manager
261 nbm.restore_checkpoint(checkpoint_id, name, path)
242 """post restores a file from a checkpoint"""
243 cm = self.contents_manager
244 cm.restore_checkpoint(checkpoint_id, name, path)
262 245 self.set_status(204)
263 246 self.finish()
264 247
265 248 @web.authenticated
266 249 @json_errors
267 250 def delete(self, path, name, checkpoint_id):
268 """delete clears a checkpoint for a given notebook"""
269 nbm = self.notebook_manager
270 nbm.delete_checkpoint(checkpoint_id, name, path)
251 """delete clears a checkpoint for a given file"""
252 cm = self.contents_manager
253 cm.delete_checkpoint(checkpoint_id, name, path)
271 254 self.set_status(204)
272 255 self.finish()
273 256
274 257 #-----------------------------------------------------------------------------
275 258 # URL to handler mappings
276 259 #-----------------------------------------------------------------------------
277 260
278 261
279 262 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
280 263
281 264 default_handlers = [
282 (r"/api/notebooks%s/checkpoints" % notebook_path_regex, NotebookCheckpointsHandler),
283 (r"/api/notebooks%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex),
284 ModifyNotebookCheckpointsHandler),
285 (r"/api/notebooks%s" % notebook_path_regex, NotebookHandler),
286 (r"/api/notebooks%s" % path_regex, NotebookHandler),
265 (r"/api/contents%s/checkpoints" % notebook_path_regex, CheckpointsHandler),
266 (r"/api/contents%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex),
267 ModifyCheckpointsHandler),
268 (r"/api/contents%s" % notebook_path_regex, ContentsHandler),
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:
4
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 #-----------------------------------------------------------------------------
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
19 5
20 6 from fnmatch import fnmatch
21 7 import itertools
22 8 import os
23 9
24 10 from IPython.config.configurable import LoggingConfigurable
25 11 from IPython.nbformat import current, sign
26 12 from IPython.utils.traitlets import Instance, Unicode, List
27 13
28 #-----------------------------------------------------------------------------
29 # Classes
30 #-----------------------------------------------------------------------------
31
32 class NotebookManager(LoggingConfigurable):
33 14
34 filename_ext = Unicode(u'.ipynb')
15 class ContentsManager(LoggingConfigurable):
35 16
36 17 notary = Instance(sign.NotebookNotary)
37 18 def _notary_default(self):
38 19 return sign.NotebookNotary(parent=self)
39 20
40 21 hide_globs = List(Unicode, [u'__pycache__'], config=True, help="""
41 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 26 # implemented in subclasses.
46 27
47 28 def path_exists(self, path):
48 29 """Does the API-style path (directory) actually exist?
49 30
50 31 Override this method in subclasses.
51 32
52 33 Parameters
53 34 ----------
54 35 path : string
55 36 The path to check
56 37
57 38 Returns
58 39 -------
59 40 exists : bool
60 41 Whether the path does indeed exist.
61 42 """
62 43 raise NotImplementedError
63 44
64 45 def is_hidden(self, path):
65 46 """Does the API style path correspond to a hidden directory or file?
66 47
67 48 Parameters
68 49 ----------
69 50 path : string
70 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 54 Returns
74 55 -------
75 56 exists : bool
76 57 Whether the path is hidden.
77 58
78 59 """
79 60 raise NotImplementedError
80 61
81 def notebook_exists(self, name, path=''):
62 def file_exists(self, name, path=''):
82 63 """Returns a True if the notebook exists. Else, returns False.
83 64
84 65 Parameters
85 66 ----------
86 67 name : string
87 68 The name of the notebook you are checking.
88 69 path : string
89 70 The relative path to the notebook (with '/' as separator)
90 71
91 72 Returns
92 73 -------
93 74 bool
94 75 """
95 76 raise NotImplementedError('must be implemented in a subclass')
96 77
97 78 # TODO: Remove this after we create the contents web service and directories are
98 79 # no longer listed by the notebook web service.
99 80 def list_dirs(self, path):
100 81 """List the directory models for a given API style path."""
101 82 raise NotImplementedError('must be implemented in a subclass')
102 83
103 84 # TODO: Remove this after we create the contents web service and directories are
104 85 # no longer listed by the notebook web service.
105 86 def get_dir_model(self, name, path=''):
106 87 """Get the directory model given a directory name and its API style path.
107 88
108 89 The keys in the model should be:
109 90 * name
110 91 * path
111 92 * last_modified
112 93 * created
113 94 * type='directory'
114 95 """
115 96 raise NotImplementedError('must be implemented in a subclass')
116 97
117 def list_notebooks(self, path=''):
118 """Return a list of notebook dicts without content.
119
120 This returns a list of dicts, each of the form::
98 def list_files(self, path=''):
99 """Return a list of contents dicts without content.
121 100
122 dict(notebook_id=notebook,name=name)
101 This returns a list of dicts
123 102
124 103 This list of dicts should be sorted by name::
125 104
126 105 data = sorted(data, key=lambda item: item['name'])
127 106 """
128 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 110 """Get the notebook model with or without content."""
132 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 114 """Save the notebook and return the model with no content."""
136 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 118 """Update the notebook and return the model with no content."""
140 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 122 """Delete notebook by name and path."""
144 123 raise NotImplementedError('must be implemented in a subclass')
145 124
146 125 def create_checkpoint(self, name, path=''):
147 126 """Create a checkpoint of the current state of a notebook
148 127
149 128 Returns a checkpoint_id for the new checkpoint.
150 129 """
151 130 raise NotImplementedError("must be implemented in a subclass")
152 131
153 132 def list_checkpoints(self, name, path=''):
154 133 """Return a list of checkpoints for a given notebook"""
155 134 return []
156 135
157 136 def restore_checkpoint(self, checkpoint_id, name, path=''):
158 137 """Restore a notebook from one of its checkpoints"""
159 138 raise NotImplementedError("must be implemented in a subclass")
160 139
161 140 def delete_checkpoint(self, checkpoint_id, name, path=''):
162 141 """delete a checkpoint for a notebook"""
163 142 raise NotImplementedError("must be implemented in a subclass")
164 143
165 144 def info_string(self):
166 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 148 # implementations, but can be overridden in subclasses.
170 149
171 150 def get_kernel_path(self, name, path='', model=None):
172 151 """ Return the path to start kernel in """
173 152 return path
174 153
175 def increment_filename(self, basename, path=''):
176 """Increment a notebook filename without the .ipynb to make it unique.
154 def increment_filename(self, filename, path=''):
155 """Increment a filename until it is unique.
177 156
178 157 Parameters
179 158 ----------
180 basename : unicode
181 The name of a notebook without the ``.ipynb`` file extension.
159 filename : unicode
160 The name of a file, including extension
182 161 path : unicode
183 162 The URL path of the notebooks directory
184 163
185 164 Returns
186 165 -------
187 166 name : unicode
188 A notebook name (with the .ipynb extension) that starts
189 with basename and does not refer to any existing notebook.
167 A filename that is unique, based on the input filename.
190 168 """
191 169 path = path.strip('/')
170 basename, ext = os.path.splitext(filename)
192 171 for i in itertools.count():
193 172 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
194 ext=self.filename_ext)
195 if not self.notebook_exists(name, path):
173 ext=ext)
174 if not self.file_exists(name, path):
196 175 break
197 176 return name
198 177
199 178 def create_notebook(self, model=None, path=''):
200 179 """Create a new notebook and return its model with no content."""
201 180 path = path.strip('/')
202 181 if model is None:
203 182 model = {}
204 183 if 'content' not in model:
205 184 metadata = current.new_metadata(name=u'')
206 185 model['content'] = current.new_notebook(metadata=metadata)
207 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 189 model['path'] = path
211 model = self.save_notebook(model, model['name'], model['path'])
190 model = self.save(model, model['name'], model['path'])
212 191 return model
213 192
214 def copy_notebook(self, from_name, to_name=None, path=''):
215 """Copy an existing notebook and return its new model.
193 def copy(self, from_name, to_name=None, path=''):
194 """Copy an existing file and return its new model.
216 195
217 196 If to_name not specified, increment `from_name-Copy#.ipynb`.
218 197 """
219 198 path = path.strip('/')
220 model = self.get_notebook(from_name, path)
199 model = self.get(from_name, path)
221 200 if not to_name:
222 base = os.path.splitext(from_name)[0] + '-Copy'
223 to_name = self.increment_filename(base, path)
201 base, ext = os.path.splitext(from_name)
202 copy_name = u'{0}-Copy{1}'.format(base, ext)
203 to_name = self.increment_filename(copy_name, path)
224 204 model['name'] = to_name
225 model = self.save_notebook(model, to_name, path)
205 model = self.save(model, to_name, path)
226 206 return model
227 207
228 208 def log_info(self):
229 209 self.log.info(self.info_string())
230 210
231 211 def trust_notebook(self, name, path=''):
232 212 """Explicitly trust a notebook
233 213
234 214 Parameters
235 215 ----------
236 216 name : string
237 217 The filename of the notebook
238 218 path : string
239 219 The notebook's directory
240 220 """
241 model = self.get_notebook(name, path)
221 model = self.get(name, path)
242 222 nb = model['content']
243 223 self.log.warn("Trusting notebook %s/%s", path, name)
244 224 self.notary.mark_cells(nb, True)
245 self.save_notebook(model, name, path)
225 self.save(model, name, path)
246 226
247 227 def check_and_sign(self, nb, name, path=''):
248 228 """Check for trusted cells, and sign the notebook.
249 229
250 230 Called as a part of saving notebooks.
251 231
252 232 Parameters
253 233 ----------
254 234 nb : dict
255 235 The notebook structure
256 236 name : string
257 237 The filename of the notebook
258 238 path : string
259 239 The notebook's directory
260 240 """
261 241 if self.notary.check_cells(nb):
262 242 self.notary.sign(nb)
263 243 else:
264 244 self.log.warn("Saving untrusted notebook %s/%s", path, name)
265 245
266 246 def mark_trusted_cells(self, nb, name, path=''):
267 247 """Mark cells as trusted if the notebook signature matches.
268 248
269 249 Called as a part of loading notebooks.
270 250
271 251 Parameters
272 252 ----------
273 253 nb : dict
274 254 The notebook structure
275 255 name : string
276 256 The filename of the notebook
277 257 path : string
278 258 The notebook's directory
279 259 """
280 260 trusted = self.notary.check_signature(nb)
281 261 if not trusted:
282 262 self.log.warn("Notebook %s/%s is not trusted", path, name)
283 263 self.notary.mark_cells(nb, trusted)
284 264
285 265 def should_list(self, name):
286 266 """Should this file/directory name be displayed in a listing?"""
287 267 return not any(fnmatch(name, glob) for glob in self.hide_globs)
@@ -1,346 +1,346 b''
1 1 # coding: utf-8
2 """Test the notebooks webservice API."""
2 """Test the contents webservice API."""
3 3
4 4 import io
5 5 import json
6 6 import os
7 7 import shutil
8 8 from unicodedata import normalize
9 9
10 10 pjoin = os.path.join
11 11
12 12 import requests
13 13
14 14 from IPython.html.utils import url_path_join, url_escape
15 15 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
16 16 from IPython.nbformat import current
17 17 from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
18 18 new_heading_cell, to_notebook_json)
19 19 from IPython.nbformat import v2
20 20 from IPython.utils import py3compat
21 21 from IPython.utils.data import uniq_stable
22 22
23 23
24 24 # TODO: Remove this after we create the contents web service and directories are
25 25 # no longer listed by the notebook web service.
26 26 def notebooks_only(nb_list):
27 27 return [nb for nb in nb_list if nb['type']=='notebook']
28 28
29 29 def dirs_only(nb_list):
30 30 return [x for x in nb_list if x['type']=='directory']
31 31
32 32
33 class NBAPI(object):
34 """Wrapper for notebook API calls."""
33 class API(object):
34 """Wrapper for contents API calls."""
35 35 def __init__(self, base_url):
36 36 self.base_url = base_url
37 37
38 38 def _req(self, verb, path, body=None):
39 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 41 data=body,
42 42 )
43 43 response.raise_for_status()
44 44 return response
45 45
46 46 def list(self, path='/'):
47 47 return self._req('GET', path)
48 48
49 49 def read(self, name, path='/'):
50 50 return self._req('GET', url_path_join(path, name))
51 51
52 52 def create_untitled(self, path='/'):
53 53 return self._req('POST', path)
54 54
55 55 def upload_untitled(self, body, path='/'):
56 56 return self._req('POST', path, body)
57 57
58 58 def copy_untitled(self, copy_from, path='/'):
59 59 body = json.dumps({'copy_from':copy_from})
60 60 return self._req('POST', path, body)
61 61
62 62 def create(self, name, path='/'):
63 63 return self._req('PUT', url_path_join(path, name))
64 64
65 65 def upload(self, name, body, path='/'):
66 66 return self._req('PUT', url_path_join(path, name), body)
67 67
68 68 def copy(self, copy_from, copy_to, path='/'):
69 69 body = json.dumps({'copy_from':copy_from})
70 70 return self._req('PUT', url_path_join(path, copy_to), body)
71 71
72 72 def save(self, name, body, path='/'):
73 73 return self._req('PUT', url_path_join(path, name), body)
74 74
75 75 def delete(self, name, path='/'):
76 76 return self._req('DELETE', url_path_join(path, name))
77 77
78 78 def rename(self, name, path, new_name):
79 79 body = json.dumps({'name': new_name})
80 80 return self._req('PATCH', url_path_join(path, name), body)
81 81
82 82 def get_checkpoints(self, name, path):
83 83 return self._req('GET', url_path_join(path, name, 'checkpoints'))
84 84
85 85 def new_checkpoint(self, name, path):
86 86 return self._req('POST', url_path_join(path, name, 'checkpoints'))
87 87
88 88 def restore_checkpoint(self, name, path, checkpoint_id):
89 89 return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
90 90
91 91 def delete_checkpoint(self, name, path, checkpoint_id):
92 92 return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
93 93
94 94 class APITest(NotebookTestBase):
95 95 """Test the kernels web service API"""
96 96 dirs_nbs = [('', 'inroot'),
97 97 ('Directory with spaces in', 'inspace'),
98 98 (u'unicodΓ©', 'innonascii'),
99 99 ('foo', 'a'),
100 100 ('foo', 'b'),
101 101 ('foo', 'name with spaces'),
102 102 ('foo', u'unicodΓ©'),
103 103 ('foo/bar', 'baz'),
104 104 ('ordering', 'A'),
105 105 ('ordering', 'b'),
106 106 ('ordering', 'C'),
107 107 (u'Γ₯ b', u'Γ§ d'),
108 108 ]
109 109 hidden_dirs = ['.hidden', '__pycache__']
110 110
111 111 dirs = uniq_stable([py3compat.cast_unicode(d) for (d,n) in dirs_nbs])
112 112 del dirs[0] # remove ''
113 113 top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs}
114 114
115 115 def setUp(self):
116 116 nbdir = self.notebook_dir.name
117 117
118 118 for d in (self.dirs + self.hidden_dirs):
119 119 d.replace('/', os.sep)
120 120 if not os.path.isdir(pjoin(nbdir, d)):
121 121 os.mkdir(pjoin(nbdir, d))
122 122
123 123 for d, name in self.dirs_nbs:
124 124 d = d.replace('/', os.sep)
125 125 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w',
126 126 encoding='utf-8') as f:
127 127 nb = new_notebook(name=name)
128 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 132 def tearDown(self):
133 133 nbdir = self.notebook_dir.name
134 134
135 135 for dname in (list(self.top_level_dirs) + self.hidden_dirs):
136 136 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
137 137
138 138 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
139 139 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
140 140
141 141 def test_list_notebooks(self):
142 nbs = notebooks_only(self.nb_api.list().json())
142 nbs = notebooks_only(self.api.list().json())
143 143 self.assertEqual(len(nbs), 1)
144 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 147 self.assertEqual(len(nbs), 1)
148 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 151 self.assertEqual(len(nbs), 1)
152 152 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
153 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 156 self.assertEqual(len(nbs), 1)
157 157 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
158 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 161 self.assertEqual(len(nbs), 4)
162 162 nbnames = { normalize('NFC', n['name']) for n in nbs }
163 163 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
164 164 expected = { normalize('NFC', name) for name in expected }
165 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 168 nbnames = [n['name'] for n in nbs]
169 169 expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
170 170 self.assertEqual(nbnames, expected)
171 171
172 172 def test_list_dirs(self):
173 dirs = dirs_only(self.nb_api.list().json())
173 dirs = dirs_only(self.api.list().json())
174 174 dir_names = {normalize('NFC', d['name']) for d in dirs}
175 175 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
176 176
177 177 def test_list_nonexistant_dir(self):
178 178 with assert_http_error(404):
179 self.nb_api.list('nonexistant')
179 self.api.list('nonexistant')
180 180
181 181 def test_get_contents(self):
182 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 184 self.assertEqual(nb['name'], u'%s.ipynb' % name)
185 185 self.assertIn('content', nb)
186 186 self.assertIn('metadata', nb['content'])
187 187 self.assertIsInstance(nb['content']['metadata'], dict)
188 188
189 189 # Name that doesn't exist - should be a 404
190 190 with assert_http_error(404):
191 self.nb_api.read('q.ipynb', 'foo')
191 self.api.read('q.ipynb', 'foo')
192 192
193 193 def _check_nb_created(self, resp, name, path):
194 194 self.assertEqual(resp.status_code, 201)
195 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 197 self.assertEqual(resp.json()['name'], name)
198 198 assert os.path.isfile(pjoin(
199 199 self.notebook_dir.name,
200 200 path.replace('/', os.sep),
201 201 name,
202 202 ))
203 203
204 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 206 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
207 207
208 208 # Second time
209 resp = self.nb_api.create_untitled(path=u'Γ₯ b')
209 resp = self.api.create_untitled(path=u'Γ₯ b')
210 210 self._check_nb_created(resp, 'Untitled1.ipynb', u'Γ₯ b')
211 211
212 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 214 self._check_nb_created(resp, 'Untitled0.ipynb', 'foo/bar')
215 215
216 216 def test_upload_untitled(self):
217 217 nb = new_notebook(name='Upload test')
218 218 nbmodel = {'content': nb}
219 resp = self.nb_api.upload_untitled(path=u'Γ₯ b',
219 resp = self.api.upload_untitled(path=u'Γ₯ b',
220 220 body=json.dumps(nbmodel))
221 221 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
222 222
223 223 def test_upload(self):
224 224 nb = new_notebook(name=u'ignored')
225 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 227 body=json.dumps(nbmodel))
228 228 self._check_nb_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
229 229
230 230 def test_upload_v2(self):
231 231 nb = v2.new_notebook()
232 232 ws = v2.new_worksheet()
233 233 nb.worksheets.append(ws)
234 234 ws.cells.append(v2.new_code_cell(input='print("hi")'))
235 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 237 body=json.dumps(nbmodel))
238 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 240 data = resp.json()
241 241 self.assertEqual(data['content']['nbformat'], current.nbformat)
242 242 self.assertEqual(data['content']['orig_nbformat'], 2)
243 243
244 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 246 self._check_nb_created(resp, u'Γ§ d-Copy0.ipynb', u'Γ₯ b')
247 247
248 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 250 self._check_nb_created(resp, u'cΓΈpy.ipynb', u'Γ₯ b')
251 251
252 252 def test_delete(self):
253 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 255 self.assertEqual(resp.status_code, 204)
256 256
257 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 259 self.assertEqual(len(nbs), 0)
260 260
261 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 263 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
264 264 self.assertEqual(resp.json()['name'], 'z.ipynb')
265 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 268 nbnames = set(n['name'] for n in nbs)
269 269 self.assertIn('z.ipynb', nbnames)
270 270 self.assertNotIn('a.ipynb', nbnames)
271 271
272 272 def test_rename_existing(self):
273 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 276 def test_save(self):
277 resp = self.nb_api.read('a.ipynb', 'foo')
277 resp = self.api.read('a.ipynb', 'foo')
278 278 nbcontent = json.loads(resp.text)['content']
279 279 nb = to_notebook_json(nbcontent)
280 280 ws = new_worksheet()
281 281 nb.worksheets = [ws]
282 282 ws.cells.append(new_heading_cell(u'Created by test Β³'))
283 283
284 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 287 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
288 288 with io.open(nbfile, 'r', encoding='utf-8') as f:
289 289 newnb = read(f, format='ipynb')
290 290 self.assertEqual(newnb.worksheets[0].cells[0].source,
291 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 293 newnb = to_notebook_json(nbcontent)
294 294 self.assertEqual(newnb.worksheets[0].cells[0].source,
295 295 u'Created by test Β³')
296 296
297 297 # Save and rename
298 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 300 saved = resp.json()
301 301 self.assertEqual(saved['name'], 'a2.ipynb')
302 302 self.assertEqual(saved['path'], 'foo/bar')
303 303 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
304 304 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
305 305 with assert_http_error(404):
306 self.nb_api.read('a.ipynb', 'foo')
306 self.api.read('a.ipynb', 'foo')
307 307
308 308 def test_checkpoints(self):
309 resp = self.nb_api.read('a.ipynb', 'foo')
310 r = self.nb_api.new_checkpoint('a.ipynb', 'foo')
309 resp = self.api.read('a.ipynb', 'foo')
310 r = self.api.new_checkpoint('a.ipynb', 'foo')
311 311 self.assertEqual(r.status_code, 201)
312 312 cp1 = r.json()
313 313 self.assertEqual(set(cp1), {'id', 'last_modified'})
314 314 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
315 315
316 316 # Modify it
317 317 nbcontent = json.loads(resp.text)['content']
318 318 nb = to_notebook_json(nbcontent)
319 319 ws = new_worksheet()
320 320 nb.worksheets = [ws]
321 321 hcell = new_heading_cell('Created by test')
322 322 ws.cells.append(hcell)
323 323 # Save
324 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 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 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 332 nb = to_notebook_json(nbcontent)
333 333 self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test')
334 334
335 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 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 339 nb = to_notebook_json(nbcontent)
340 340 self.assertEqual(nb.worksheets, [])
341 341
342 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 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 346 self.assertEqual(cps, [])
@@ -1,320 +1,301 b''
1 1 # coding: utf-8
2 2 """Tests for the notebook manager."""
3 3 from __future__ import print_function
4 4
5 5 import logging
6 6 import os
7 7
8 8 from tornado.web import HTTPError
9 9 from unittest import TestCase
10 10 from tempfile import NamedTemporaryFile
11 11
12 12 from IPython.nbformat import current
13 13
14 14 from IPython.utils.tempdir import TemporaryDirectory
15 15 from IPython.utils.traitlets import TraitError
16 16 from IPython.html.utils import url_path_join
17 17
18 from ..filenbmanager import FileNotebookManager
19 from ..nbmanager import NotebookManager
18 from ..filemanager import FileContentsManager
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 25 with TemporaryDirectory() as td:
26 fm = FileNotebookManager(notebook_dir=td)
27 self.assertEqual(fm.notebook_dir, td)
26 fm = FileContentsManager(root_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 30 with TemporaryDirectory() as td:
31 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
32 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
31 root = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
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 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 38 def test_get_os_path(self):
39 39 # full filesystem path should be returned with correct operating system
40 40 # separators.
41 41 with TemporaryDirectory() as td:
42 nbdir = td
43 fm = FileNotebookManager(notebook_dir=nbdir)
42 root = td
43 fm = FileContentsManager(root_dir=root)
44 44 path = fm._get_os_path('test.ipynb', '/path/to/notebook/')
45 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 47 self.assertEqual(path, fs_path)
48 48
49 fm = FileNotebookManager(notebook_dir=nbdir)
49 fm = FileContentsManager(root_dir=root)
50 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 52 self.assertEqual(path, fs_path)
53 53
54 fm = FileNotebookManager(notebook_dir=nbdir)
54 fm = FileContentsManager(root_dir=root)
55 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 57 self.assertEqual(path, fs_path)
58 58
59 59 def test_checkpoint_subdir(self):
60 60 subd = u'sub βˆ‚ir'
61 61 cp_name = 'test-cp.ipynb'
62 62 with TemporaryDirectory() as td:
63 nbdir = td
63 root = td
64 64 os.mkdir(os.path.join(td, subd))
65 fm = FileNotebookManager(notebook_dir=nbdir)
65 fm = FileContentsManager(root_dir=root)
66 66 cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb', '/')
67 67 cp_subdir = fm.get_checkpoint_path('cp', 'test.ipynb', '/%s/' % subd)
68 68 self.assertNotEqual(cp_dir, cp_subdir)
69 self.assertEqual(cp_dir, os.path.join(nbdir, fm.checkpoint_dir, cp_name))
70 self.assertEqual(cp_subdir, os.path.join(nbdir, subd, 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(root, subd, fm.checkpoint_dir, cp_name))
71 71
72 72
73 73 class TestNotebookManager(TestCase):
74 74
75 75 def setUp(self):
76 76 self._temp_dir = TemporaryDirectory()
77 77 self.td = self._temp_dir.name
78 self.notebook_manager = FileNotebookManager(
79 notebook_dir=self.td,
78 self.contents_manager = FileContentsManager(
79 root_dir=self.td,
80 80 log=logging.getLogger()
81 81 )
82 82
83 83 def tearDown(self):
84 84 self._temp_dir.cleanup()
85 85
86 86 def make_dir(self, abs_path, rel_path):
87 87 """make subdirectory, rel_path is the relative path
88 88 to that directory from the location where the server started"""
89 89 os_path = os.path.join(abs_path, rel_path)
90 90 try:
91 91 os.makedirs(os_path)
92 92 except OSError:
93 93 print("Directory already exists: %r" % os_path)
94 94
95 95 def add_code_cell(self, nb):
96 96 output = current.new_output("display_data", output_javascript="alert('hi');")
97 97 cell = current.new_code_cell("print('hi')", outputs=[output])
98 98 if not nb.worksheets:
99 99 nb.worksheets.append(current.new_worksheet())
100 100 nb.worksheets[0].cells.append(cell)
101 101
102 102 def new_notebook(self):
103 nbm = self.notebook_manager
104 model = nbm.create_notebook()
103 cm = self.contents_manager
104 model = cm.create_notebook()
105 105 name = model['name']
106 106 path = model['path']
107 107
108 full_model = nbm.get_notebook(name, path)
108 full_model = cm.get(name, path)
109 109 nb = full_model['content']
110 110 self.add_code_cell(nb)
111 111
112 nbm.save_notebook(full_model, name, path)
112 cm.save(full_model, name, path)
113 113 return nb, name, path
114 114
115 115 def test_create_notebook(self):
116 nm = self.notebook_manager
116 cm = self.contents_manager
117 117 # Test in root directory
118 model = nm.create_notebook()
118 model = cm.create_notebook()
119 119 assert isinstance(model, dict)
120 120 self.assertIn('name', model)
121 121 self.assertIn('path', model)
122 122 self.assertEqual(model['name'], 'Untitled0.ipynb')
123 123 self.assertEqual(model['path'], '')
124 124
125 125 # Test in sub-directory
126 126 sub_dir = '/foo/'
127 self.make_dir(nm.notebook_dir, 'foo')
128 model = nm.create_notebook(None, sub_dir)
127 self.make_dir(cm.root_dir, 'foo')
128 model = cm.create_notebook(None, sub_dir)
129 129 assert isinstance(model, dict)
130 130 self.assertIn('name', model)
131 131 self.assertIn('path', model)
132 132 self.assertEqual(model['name'], 'Untitled0.ipynb')
133 133 self.assertEqual(model['path'], sub_dir.strip('/'))
134 134
135 def test_get_notebook(self):
136 nm = self.notebook_manager
135 def test_get(self):
136 cm = self.contents_manager
137 137 # Create a notebook
138 model = nm.create_notebook()
138 model = cm.create_notebook()
139 139 name = model['name']
140 140 path = model['path']
141 141
142 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 144 assert isinstance(model2, dict)
145 145 self.assertIn('name', model2)
146 146 self.assertIn('path', model2)
147 147 self.assertEqual(model['name'], name)
148 148 self.assertEqual(model['path'], path)
149 149
150 150 # Test in sub-directory
151 151 sub_dir = '/foo/'
152 self.make_dir(nm.notebook_dir, 'foo')
153 model = nm.create_notebook(None, sub_dir)
154 model2 = nm.get_notebook(name, sub_dir)
152 self.make_dir(cm.root_dir, 'foo')
153 model = cm.create_notebook(None, sub_dir)
154 model2 = cm.get(name, sub_dir)
155 155 assert isinstance(model2, dict)
156 156 self.assertIn('name', model2)
157 157 self.assertIn('path', model2)
158 158 self.assertIn('content', model2)
159 159 self.assertEqual(model2['name'], 'Untitled0.ipynb')
160 160 self.assertEqual(model2['path'], sub_dir.strip('/'))
161 161
162 def test_update_notebook(self):
163 nm = self.notebook_manager
162 def test_update(self):
163 cm = self.contents_manager
164 164 # Create a notebook
165 model = nm.create_notebook()
165 model = cm.create_notebook()
166 166 name = model['name']
167 167 path = model['path']
168 168
169 169 # Change the name in the model for rename
170 170 model['name'] = 'test.ipynb'
171 model = nm.update_notebook(model, name, path)
171 model = cm.update(model, name, path)
172 172 assert isinstance(model, dict)
173 173 self.assertIn('name', model)
174 174 self.assertIn('path', model)
175 175 self.assertEqual(model['name'], 'test.ipynb')
176 176
177 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 180 # Test in sub-directory
181 181 # Create a directory and notebook in that directory
182 182 sub_dir = '/foo/'
183 self.make_dir(nm.notebook_dir, 'foo')
184 model = nm.create_notebook(None, sub_dir)
183 self.make_dir(cm.root_dir, 'foo')
184 model = cm.create_notebook(None, sub_dir)
185 185 name = model['name']
186 186 path = model['path']
187 187
188 188 # Change the name in the model for rename
189 189 model['name'] = 'test_in_sub.ipynb'
190 model = nm.update_notebook(model, name, path)
190 model = cm.update(model, name, path)
191 191 assert isinstance(model, dict)
192 192 self.assertIn('name', model)
193 193 self.assertIn('path', model)
194 194 self.assertEqual(model['name'], 'test_in_sub.ipynb')
195 195 self.assertEqual(model['path'], sub_dir.strip('/'))
196 196
197 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):
201 nm = self.notebook_manager
200 def test_save(self):
201 cm = self.contents_manager
202 202 # Create a notebook
203 model = nm.create_notebook()
203 model = cm.create_notebook()
204 204 name = model['name']
205 205 path = model['path']
206 206
207 207 # Get the model with 'content'
208 full_model = nm.get_notebook(name, path)
208 full_model = cm.get(name, path)
209 209
210 210 # Save the notebook
211 model = nm.save_notebook(full_model, name, path)
211 model = cm.save(full_model, name, path)
212 212 assert isinstance(model, dict)
213 213 self.assertIn('name', model)
214 214 self.assertIn('path', model)
215 215 self.assertEqual(model['name'], name)
216 216 self.assertEqual(model['path'], path)
217 217
218 218 # Test in sub-directory
219 219 # Create a directory and notebook in that directory
220 220 sub_dir = '/foo/'
221 self.make_dir(nm.notebook_dir, 'foo')
222 model = nm.create_notebook(None, sub_dir)
221 self.make_dir(cm.root_dir, 'foo')
222 model = cm.create_notebook(None, sub_dir)
223 223 name = model['name']
224 224 path = model['path']
225 model = nm.get_notebook(name, path)
225 model = cm.get(name, path)
226 226
227 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 229 assert isinstance(model, dict)
230 230 self.assertIn('name', model)
231 231 self.assertIn('path', model)
232 232 self.assertEqual(model['name'], 'Untitled0.ipynb')
233 233 self.assertEqual(model['path'], sub_dir.strip('/'))
234 234
235 def test_save_notebook_with_script(self):
236 nm = self.notebook_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
235 def test_delete(self):
236 cm = self.contents_manager
256 237 # Create a notebook
257 238 nb, name, path = self.new_notebook()
258 239
259 240 # Delete the notebook
260 nm.delete_notebook(name, path)
241 cm.delete(name, path)
261 242
262 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):
266 nm = self.notebook_manager
246 def test_copy(self):
247 cm = self.contents_manager
267 248 path = u'Γ₯ b'
268 249 name = u'nb √.ipynb'
269 os.mkdir(os.path.join(nm.notebook_dir, path))
270 orig = nm.create_notebook({'name' : name}, path=path)
250 os.mkdir(os.path.join(cm.root_dir, path))
251 orig = cm.create_notebook({'name' : name}, path=path)
271 252
272 253 # copy with unspecified name
273 copy = nm.copy_notebook(name, path=path)
254 copy = cm.copy(name, path=path)
274 255 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
275 256
276 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 259 self.assertEqual(copy2['name'], u'copy 2.ipynb')
279 260
280 261 def test_trust_notebook(self):
281 nbm = self.notebook_manager
262 cm = self.contents_manager
282 263 nb, name, path = self.new_notebook()
283 264
284 untrusted = nbm.get_notebook(name, path)['content']
285 assert not nbm.notary.check_cells(untrusted)
265 untrusted = cm.get(name, path)['content']
266 assert not cm.notary.check_cells(untrusted)
286 267
287 268 # print(untrusted)
288 nbm.trust_notebook(name, path)
289 trusted = nbm.get_notebook(name, path)['content']
269 cm.trust_notebook(name, path)
270 trusted = cm.get(name, path)['content']
290 271 # print(trusted)
291 assert nbm.notary.check_cells(trusted)
272 assert cm.notary.check_cells(trusted)
292 273
293 274 def test_mark_trusted_cells(self):
294 nbm = self.notebook_manager
275 cm = self.contents_manager
295 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 279 for cell in nb.worksheets[0].cells:
299 280 if cell.cell_type == 'code':
300 281 assert not cell.trusted
301 282
302 nbm.trust_notebook(name, path)
303 nb = nbm.get_notebook(name, path)['content']
283 cm.trust_notebook(name, path)
284 nb = cm.get(name, path)['content']
304 285 for cell in nb.worksheets[0].cells:
305 286 if cell.cell_type == 'code':
306 287 assert cell.trusted
307 288
308 289 def test_check_and_sign(self):
309 nbm = self.notebook_manager
290 cm = self.contents_manager
310 291 nb, name, path = self.new_notebook()
311 292
312 nbm.mark_trusted_cells(nb, name, path)
313 nbm.check_and_sign(nb, name, path)
314 assert not nbm.notary.check_signature(nb)
293 cm.mark_trusted_cells(nb, name, path)
294 cm.check_and_sign(nb, name, path)
295 assert not cm.notary.check_signature(nb)
315 296
316 nbm.trust_notebook(name, path)
317 nb = nbm.get_notebook(name, path)['content']
318 nbm.mark_trusted_cells(nb, name, path)
319 nbm.check_and_sign(nb, name, path)
320 assert nbm.notary.check_signature(nb)
297 cm.trust_notebook(name, path)
298 nb = cm.get(name, path)['content']
299 cm.mark_trusted_cells(nb, name, path)
300 cm.check_and_sign(nb, name, path)
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:
4
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 #-----------------------------------------------------------------------------
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
18 5
19 6 import json
20 7
21 8 from tornado import web
22 9
23 10 from ...base.handlers import IPythonHandler, json_errors
24 11 from IPython.utils.jsonutil import date_default
25 12 from IPython.html.utils import url_path_join, url_escape
26 13
27 #-----------------------------------------------------------------------------
28 # Session web service handlers
29 #-----------------------------------------------------------------------------
30
31 14
32 15 class SessionRootHandler(IPythonHandler):
33 16
34 17 @web.authenticated
35 18 @json_errors
36 19 def get(self):
37 20 # Return a list of running sessions
38 21 sm = self.session_manager
39 22 sessions = sm.list_sessions()
40 23 self.finish(json.dumps(sessions, default=date_default))
41 24
42 25 @web.authenticated
43 26 @json_errors
44 27 def post(self):
45 28 # Creates a new session
46 29 #(unless a session already exists for the named nb)
47 30 sm = self.session_manager
31 cm = self.contents_manager
32 km = self.kernel_manager
48 33
49 34 model = self.get_json_body()
50 35 if model is None:
51 36 raise web.HTTPError(400, "No JSON data provided")
52 37 try:
53 38 name = model['notebook']['name']
54 39 except KeyError:
55 40 raise web.HTTPError(400, "Missing field in JSON data: notebook.name")
56 41 try:
57 42 path = model['notebook']['path']
58 43 except KeyError:
59 44 raise web.HTTPError(400, "Missing field in JSON data: notebook.path")
60 45 try:
61 46 kernel_name = model['kernel']['name']
62 47 except KeyError:
63 48 raise web.HTTPError(400, "Missing field in JSON data: kernel.name")
64 49
65 50 # Check to see if session exists
66 51 if sm.session_exists(name=name, path=path):
67 52 model = sm.get_session(name=name, path=path)
68 53 else:
69 54 model = sm.create_session(name=name, path=path, kernel_name=kernel_name)
70 55 location = url_path_join(self.base_url, 'api', 'sessions', model['id'])
71 56 self.set_header('Location', url_escape(location))
72 57 self.set_status(201)
73 58 self.finish(json.dumps(model, default=date_default))
74 59
75 60 class SessionHandler(IPythonHandler):
76 61
77 62 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
78 63
79 64 @web.authenticated
80 65 @json_errors
81 66 def get(self, session_id):
82 67 # Returns the JSON model for a single session
83 68 sm = self.session_manager
84 69 model = sm.get_session(session_id=session_id)
85 70 self.finish(json.dumps(model, default=date_default))
86 71
87 72 @web.authenticated
88 73 @json_errors
89 74 def patch(self, session_id):
90 75 # Currently, this handler is strictly for renaming notebooks
91 76 sm = self.session_manager
92 77 model = self.get_json_body()
93 78 if model is None:
94 79 raise web.HTTPError(400, "No JSON data provided")
95 80 changes = {}
96 81 if 'notebook' in model:
97 82 notebook = model['notebook']
98 83 if 'name' in notebook:
99 84 changes['name'] = notebook['name']
100 85 if 'path' in notebook:
101 86 changes['path'] = notebook['path']
102 87
103 88 sm.update_session(session_id, **changes)
104 89 model = sm.get_session(session_id=session_id)
105 90 self.finish(json.dumps(model, default=date_default))
106 91
107 92 @web.authenticated
108 93 @json_errors
109 94 def delete(self, session_id):
110 95 # Deletes the session with given session_id
111 96 sm = self.session_manager
112 97 sm.delete_session(session_id)
113 98 self.set_status(204)
114 99 self.finish()
115 100
116 101
117 102 #-----------------------------------------------------------------------------
118 103 # URL to handler mappings
119 104 #-----------------------------------------------------------------------------
120 105
121 106 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
122 107
123 108 default_handlers = [
124 109 (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
125 110 (r"/api/sessions", SessionRootHandler)
126 111 ]
127 112
@@ -1,206 +1,206 b''
1 1 """A base class session manager.
2 2
3 3 Authors:
4 4
5 5 * Zach Sailer
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2013 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import uuid
20 20 import sqlite3
21 21
22 22 from tornado import web
23 23
24 24 from IPython.config.configurable import LoggingConfigurable
25 25 from IPython.utils.py3compat import unicode_type
26 26 from IPython.utils.traitlets import Instance
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Classes
30 30 #-----------------------------------------------------------------------------
31 31
32 32 class SessionManager(LoggingConfigurable):
33 33
34 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 37 # Session database initialized below
38 38 _cursor = None
39 39 _connection = None
40 40 _columns = {'session_id', 'name', 'path', 'kernel_id'}
41 41
42 42 @property
43 43 def cursor(self):
44 44 """Start a cursor and create a database called 'session'"""
45 45 if self._cursor is None:
46 46 self._cursor = self.connection.cursor()
47 47 self._cursor.execute("""CREATE TABLE session
48 48 (session_id, name, path, kernel_id)""")
49 49 return self._cursor
50 50
51 51 @property
52 52 def connection(self):
53 53 """Start a database connection"""
54 54 if self._connection is None:
55 55 self._connection = sqlite3.connect(':memory:')
56 56 self._connection.row_factory = self.row_factory
57 57 return self._connection
58 58
59 59 def __del__(self):
60 60 """Close connection once SessionManager closes"""
61 61 self.cursor.close()
62 62
63 63 def session_exists(self, name, path):
64 64 """Check to see if the session for a given notebook exists"""
65 65 self.cursor.execute("SELECT * FROM session WHERE name=? AND path=?", (name, path))
66 66 reply = self.cursor.fetchone()
67 67 if reply is None:
68 68 return False
69 69 else:
70 70 return True
71 71
72 72 def new_session_id(self):
73 73 "Create a uuid for a new session"
74 74 return unicode_type(uuid.uuid4())
75 75
76 76 def create_session(self, name=None, path=None, kernel_name='python'):
77 77 """Creates a session and returns its model"""
78 78 session_id = self.new_session_id()
79 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 81 kernel_id = self.kernel_manager.start_kernel(path=kernel_path,
82 82 kernel_name=kernel_name)
83 83 return self.save_session(session_id, name=name, path=path,
84 84 kernel_id=kernel_id)
85 85
86 86 def save_session(self, session_id, name=None, path=None, kernel_id=None):
87 87 """Saves the items for the session with the given session_id
88 88
89 89 Given a session_id (and any other of the arguments), this method
90 90 creates a row in the sqlite session database that holds the information
91 91 for a session.
92 92
93 93 Parameters
94 94 ----------
95 95 session_id : str
96 96 uuid for the session; this method must be given a session_id
97 97 name : str
98 98 the .ipynb notebook name that started the session
99 99 path : str
100 100 the path to the named notebook
101 101 kernel_id : str
102 102 a uuid for the kernel associated with this session
103 103
104 104 Returns
105 105 -------
106 106 model : dict
107 107 a dictionary of the session model
108 108 """
109 109 self.cursor.execute("INSERT INTO session VALUES (?,?,?,?)",
110 110 (session_id, name, path, kernel_id)
111 111 )
112 112 return self.get_session(session_id=session_id)
113 113
114 114 def get_session(self, **kwargs):
115 115 """Returns the model for a particular session.
116 116
117 117 Takes a keyword argument and searches for the value in the session
118 118 database, then returns the rest of the session's info.
119 119
120 120 Parameters
121 121 ----------
122 122 **kwargs : keyword argument
123 123 must be given one of the keywords and values from the session database
124 124 (i.e. session_id, name, path, kernel_id)
125 125
126 126 Returns
127 127 -------
128 128 model : dict
129 129 returns a dictionary that includes all the information from the
130 130 session described by the kwarg.
131 131 """
132 132 if not kwargs:
133 133 raise TypeError("must specify a column to query")
134 134
135 135 conditions = []
136 136 for column in kwargs.keys():
137 137 if column not in self._columns:
138 138 raise TypeError("No such column: %r", column)
139 139 conditions.append("%s=?" % column)
140 140
141 141 query = "SELECT * FROM session WHERE %s" % (' AND '.join(conditions))
142 142
143 143 self.cursor.execute(query, list(kwargs.values()))
144 144 model = self.cursor.fetchone()
145 145 if model is None:
146 146 q = []
147 147 for key, value in kwargs.items():
148 148 q.append("%s=%r" % (key, value))
149 149
150 150 raise web.HTTPError(404, u'Session not found: %s' % (', '.join(q)))
151 151 return model
152 152
153 153 def update_session(self, session_id, **kwargs):
154 154 """Updates the values in the session database.
155 155
156 156 Changes the values of the session with the given session_id
157 157 with the values from the keyword arguments.
158 158
159 159 Parameters
160 160 ----------
161 161 session_id : str
162 162 a uuid that identifies a session in the sqlite3 database
163 163 **kwargs : str
164 164 the key must correspond to a column title in session database,
165 165 and the value replaces the current value in the session
166 166 with session_id.
167 167 """
168 168 self.get_session(session_id=session_id)
169 169
170 170 if not kwargs:
171 171 # no changes
172 172 return
173 173
174 174 sets = []
175 175 for column in kwargs.keys():
176 176 if column not in self._columns:
177 177 raise TypeError("No such column: %r" % column)
178 178 sets.append("%s=?" % column)
179 179 query = "UPDATE session SET %s WHERE session_id=?" % (', '.join(sets))
180 180 self.cursor.execute(query, list(kwargs.values()) + [session_id])
181 181
182 182 def row_factory(self, cursor, row):
183 183 """Takes sqlite database session row and turns it into a dictionary"""
184 184 row = sqlite3.Row(cursor, row)
185 185 model = {
186 186 'id': row['session_id'],
187 187 'notebook': {
188 188 'name': row['name'],
189 189 'path': row['path']
190 190 },
191 191 'kernel': self.kernel_manager.kernel_model(row['kernel_id'])
192 192 }
193 193 return model
194 194
195 195 def list_sessions(self):
196 196 """Returns a list of dictionaries containing all the information from
197 197 the session database"""
198 198 c = self.cursor.execute("SELECT * FROM session")
199 199 return list(c.fetchall())
200 200
201 201 def delete_session(self, session_id):
202 202 """Deletes the row in the session database with given session_id"""
203 203 # Check that session exists before deleting
204 204 session = self.get_session(session_id=session_id)
205 205 self.kernel_manager.shutdown_kernel(session['kernel']['id'])
206 206 self.cursor.execute("DELETE FROM session WHERE session_id=?", (session_id,))
@@ -1,2579 +1,2579 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'base/js/dialog',
9 9 'notebook/js/textcell',
10 10 'notebook/js/codecell',
11 11 'services/sessions/js/session',
12 12 'notebook/js/celltoolbar',
13 13 'components/marked/lib/marked',
14 14 'highlight',
15 15 'notebook/js/mathjaxutils',
16 16 'base/js/keyboard',
17 17 'notebook/js/tooltip',
18 18 'notebook/js/celltoolbarpresets/default',
19 19 'notebook/js/celltoolbarpresets/rawcell',
20 20 'notebook/js/celltoolbarpresets/slideshow',
21 21 ], function (
22 22 IPython,
23 23 $,
24 24 utils,
25 25 dialog,
26 26 textcell,
27 27 codecell,
28 28 session,
29 29 celltoolbar,
30 30 marked,
31 31 hljs,
32 32 mathjaxutils,
33 33 keyboard,
34 34 tooltip,
35 35 default_celltoolbar,
36 36 rawcell_celltoolbar,
37 37 slideshow_celltoolbar
38 38 ) {
39 39
40 40 var Notebook = function (selector, options) {
41 41 // Constructor
42 42 //
43 43 // A notebook contains and manages cells.
44 44 //
45 45 // Parameters:
46 46 // selector: string
47 47 // options: dictionary
48 48 // Dictionary of keyword arguments.
49 49 // events: $(Events) instance
50 50 // keyboard_manager: KeyboardManager instance
51 51 // save_widget: SaveWidget instance
52 52 // config: dictionary
53 53 // base_url : string
54 54 // notebook_path : string
55 55 // notebook_name : string
56 56 this.config = options.config || {};
57 57 this.base_url = options.base_url;
58 58 this.notebook_path = options.notebook_path;
59 59 this.notebook_name = options.notebook_name;
60 60 this.events = options.events;
61 61 this.keyboard_manager = options.keyboard_manager;
62 62 this.save_widget = options.save_widget;
63 63 this.tooltip = new tooltip.Tooltip(this.events);
64 64 this.ws_url = options.ws_url;
65 65 // default_kernel_name is a temporary measure while we implement proper
66 66 // kernel selection and delayed start. Do not rely on it.
67 67 this.default_kernel_name = 'python';
68 68 // TODO: This code smells (and the other `= this` line a couple lines down)
69 69 // We need a better way to deal with circular instance references.
70 70 this.keyboard_manager.notebook = this;
71 71 this.save_widget.notebook = this;
72 72
73 73 mathjaxutils.init();
74 74
75 75 if (marked) {
76 76 marked.setOptions({
77 77 gfm : true,
78 78 tables: true,
79 79 langPrefix: "language-",
80 80 highlight: function(code, lang) {
81 81 if (!lang) {
82 82 // no language, no highlight
83 83 return code;
84 84 }
85 85 var highlighted;
86 86 try {
87 87 highlighted = hljs.highlight(lang, code, false);
88 88 } catch(err) {
89 89 highlighted = hljs.highlightAuto(code);
90 90 }
91 91 return highlighted.value;
92 92 }
93 93 });
94 94 }
95 95
96 96 this.element = $(selector);
97 97 this.element.scroll();
98 98 this.element.data("notebook", this);
99 99 this.next_prompt_number = 1;
100 100 this.session = null;
101 101 this.kernel = null;
102 102 this.clipboard = null;
103 103 this.undelete_backup = null;
104 104 this.undelete_index = null;
105 105 this.undelete_below = false;
106 106 this.paste_enabled = false;
107 107 // It is important to start out in command mode to match the intial mode
108 108 // of the KeyboardManager.
109 109 this.mode = 'command';
110 110 this.set_dirty(false);
111 111 this.metadata = {};
112 112 this._checkpoint_after_save = false;
113 113 this.last_checkpoint = null;
114 114 this.checkpoints = [];
115 115 this.autosave_interval = 0;
116 116 this.autosave_timer = null;
117 117 // autosave *at most* every two minutes
118 118 this.minimum_autosave_interval = 120000;
119 119 // single worksheet for now
120 120 this.worksheet_metadata = {};
121 121 this.notebook_name_blacklist_re = /[\/\\:]/;
122 122 this.nbformat = 3; // Increment this when changing the nbformat
123 123 this.nbformat_minor = 0; // Increment this when changing the nbformat
124 124 this.codemirror_mode = 'ipython';
125 125 this.create_elements();
126 126 this.bind_events();
127 127 this.save_notebook = function() { // don't allow save until notebook_loaded
128 128 this.save_notebook_error(null, null, "Load failed, save is disabled");
129 129 };
130 130
131 131 // Trigger cell toolbar registration.
132 132 default_celltoolbar.register(this);
133 133 rawcell_celltoolbar.register(this);
134 134 slideshow_celltoolbar.register(this);
135 135 };
136 136
137 137
138 138 /**
139 139 * Create an HTML and CSS representation of the notebook.
140 140 *
141 141 * @method create_elements
142 142 */
143 143 Notebook.prototype.create_elements = function () {
144 144 var that = this;
145 145 this.element.attr('tabindex','-1');
146 146 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
147 147 // We add this end_space div to the end of the notebook div to:
148 148 // i) provide a margin between the last cell and the end of the notebook
149 149 // ii) to prevent the div from scrolling up when the last cell is being
150 150 // edited, but is too low on the page, which browsers will do automatically.
151 151 var end_space = $('<div/>').addClass('end_space');
152 152 end_space.dblclick(function (e) {
153 153 var ncells = that.ncells();
154 154 that.insert_cell_below('code',ncells-1);
155 155 });
156 156 this.element.append(this.container);
157 157 this.container.append(end_space);
158 158 };
159 159
160 160 /**
161 161 * Bind JavaScript events: key presses and custom IPython events.
162 162 *
163 163 * @method bind_events
164 164 */
165 165 Notebook.prototype.bind_events = function () {
166 166 var that = this;
167 167
168 168 this.events.on('set_next_input.Notebook', function (event, data) {
169 169 var index = that.find_cell_index(data.cell);
170 170 var new_cell = that.insert_cell_below('code',index);
171 171 new_cell.set_text(data.text);
172 172 that.dirty = true;
173 173 });
174 174
175 175 this.events.on('set_dirty.Notebook', function (event, data) {
176 176 that.dirty = data.value;
177 177 });
178 178
179 179 this.events.on('trust_changed.Notebook', function (event, data) {
180 180 that.trusted = data.value;
181 181 });
182 182
183 183 this.events.on('select.Cell', function (event, data) {
184 184 var index = that.find_cell_index(data.cell);
185 185 that.select(index);
186 186 });
187 187
188 188 this.events.on('edit_mode.Cell', function (event, data) {
189 189 that.handle_edit_mode(data.cell);
190 190 });
191 191
192 192 this.events.on('command_mode.Cell', function (event, data) {
193 193 that.handle_command_mode(data.cell);
194 194 });
195 195
196 196 this.events.on('status_autorestarting.Kernel', function () {
197 197 dialog.modal({
198 198 notebook: that,
199 199 keyboard_manager: that.keyboard_manager,
200 200 title: "Kernel Restarting",
201 201 body: "The kernel appears to have died. It will restart automatically.",
202 202 buttons: {
203 203 OK : {
204 204 class : "btn-primary"
205 205 }
206 206 }
207 207 });
208 208 });
209 209
210 210 this.events.on('spec_changed.Kernel', function(event, data) {
211 211 that.set_kernelspec_metadata(data);
212 212 if (data.codemirror_mode) {
213 213 that.set_codemirror_mode(data.codemirror_mode);
214 214 }
215 215 });
216 216
217 217 var collapse_time = function (time) {
218 218 var app_height = $('#ipython-main-app').height(); // content height
219 219 var splitter_height = $('div#pager_splitter').outerHeight(true);
220 220 var new_height = app_height - splitter_height;
221 221 that.element.animate({height : new_height + 'px'}, time);
222 222 };
223 223
224 224 this.element.bind('collapse_pager', function (event, extrap) {
225 225 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
226 226 collapse_time(time);
227 227 });
228 228
229 229 var expand_time = function (time) {
230 230 var app_height = $('#ipython-main-app').height(); // content height
231 231 var splitter_height = $('div#pager_splitter').outerHeight(true);
232 232 var pager_height = $('div#pager').outerHeight(true);
233 233 var new_height = app_height - pager_height - splitter_height;
234 234 that.element.animate({height : new_height + 'px'}, time);
235 235 };
236 236
237 237 this.element.bind('expand_pager', function (event, extrap) {
238 238 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
239 239 expand_time(time);
240 240 });
241 241
242 242 // Firefox 22 broke $(window).on("beforeunload")
243 243 // I'm not sure why or how.
244 244 window.onbeforeunload = function (e) {
245 245 // TODO: Make killing the kernel configurable.
246 246 var kill_kernel = false;
247 247 if (kill_kernel) {
248 248 that.session.kill_kernel();
249 249 }
250 250 // if we are autosaving, trigger an autosave on nav-away.
251 251 // still warn, because if we don't the autosave may fail.
252 252 if (that.dirty) {
253 253 if ( that.autosave_interval ) {
254 254 // schedule autosave in a timeout
255 255 // this gives you a chance to forcefully discard changes
256 256 // by reloading the page if you *really* want to.
257 257 // the timer doesn't start until you *dismiss* the dialog.
258 258 setTimeout(function () {
259 259 if (that.dirty) {
260 260 that.save_notebook();
261 261 }
262 262 }, 1000);
263 263 return "Autosave in progress, latest changes may be lost.";
264 264 } else {
265 265 return "Unsaved changes will be lost.";
266 266 }
267 267 }
268 268 // Null is the *only* return value that will make the browser not
269 269 // pop up the "don't leave" dialog.
270 270 return null;
271 271 };
272 272 };
273 273
274 274 /**
275 275 * Set the dirty flag, and trigger the set_dirty.Notebook event
276 276 *
277 277 * @method set_dirty
278 278 */
279 279 Notebook.prototype.set_dirty = function (value) {
280 280 if (value === undefined) {
281 281 value = true;
282 282 }
283 283 if (this.dirty == value) {
284 284 return;
285 285 }
286 286 this.events.trigger('set_dirty.Notebook', {value: value});
287 287 };
288 288
289 289 /**
290 290 * Scroll the top of the page to a given cell.
291 291 *
292 292 * @method scroll_to_cell
293 293 * @param {Number} cell_number An index of the cell to view
294 294 * @param {Number} time Animation time in milliseconds
295 295 * @return {Number} Pixel offset from the top of the container
296 296 */
297 297 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
298 298 var cells = this.get_cells();
299 299 time = time || 0;
300 300 cell_number = Math.min(cells.length-1,cell_number);
301 301 cell_number = Math.max(0 ,cell_number);
302 302 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
303 303 this.element.animate({scrollTop:scroll_value}, time);
304 304 return scroll_value;
305 305 };
306 306
307 307 /**
308 308 * Scroll to the bottom of the page.
309 309 *
310 310 * @method scroll_to_bottom
311 311 */
312 312 Notebook.prototype.scroll_to_bottom = function () {
313 313 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
314 314 };
315 315
316 316 /**
317 317 * Scroll to the top of the page.
318 318 *
319 319 * @method scroll_to_top
320 320 */
321 321 Notebook.prototype.scroll_to_top = function () {
322 322 this.element.animate({scrollTop:0}, 0);
323 323 };
324 324
325 325 // Edit Notebook metadata
326 326
327 327 Notebook.prototype.edit_metadata = function () {
328 328 var that = this;
329 329 dialog.edit_metadata({
330 330 md: this.metadata,
331 331 callback: function (md) {
332 332 that.metadata = md;
333 333 },
334 334 name: 'Notebook',
335 335 notebook: this,
336 336 keyboard_manager: this.keyboard_manager});
337 337 };
338 338
339 339 Notebook.prototype.set_kernelspec_metadata = function(ks) {
340 340 var tostore = {};
341 341 $.map(ks, function(value, field) {
342 342 if (field !== 'argv' && field !== 'env') {
343 343 tostore[field] = value;
344 344 }
345 345 });
346 346 this.metadata.kernelspec = tostore;
347 347 }
348 348
349 349 // Cell indexing, retrieval, etc.
350 350
351 351 /**
352 352 * Get all cell elements in the notebook.
353 353 *
354 354 * @method get_cell_elements
355 355 * @return {jQuery} A selector of all cell elements
356 356 */
357 357 Notebook.prototype.get_cell_elements = function () {
358 358 return this.container.children("div.cell");
359 359 };
360 360
361 361 /**
362 362 * Get a particular cell element.
363 363 *
364 364 * @method get_cell_element
365 365 * @param {Number} index An index of a cell to select
366 366 * @return {jQuery} A selector of the given cell.
367 367 */
368 368 Notebook.prototype.get_cell_element = function (index) {
369 369 var result = null;
370 370 var e = this.get_cell_elements().eq(index);
371 371 if (e.length !== 0) {
372 372 result = e;
373 373 }
374 374 return result;
375 375 };
376 376
377 377 /**
378 378 * Try to get a particular cell by msg_id.
379 379 *
380 380 * @method get_msg_cell
381 381 * @param {String} msg_id A message UUID
382 382 * @return {Cell} Cell or null if no cell was found.
383 383 */
384 384 Notebook.prototype.get_msg_cell = function (msg_id) {
385 385 return codecell.CodeCell.msg_cells[msg_id] || null;
386 386 };
387 387
388 388 /**
389 389 * Count the cells in this notebook.
390 390 *
391 391 * @method ncells
392 392 * @return {Number} The number of cells in this notebook
393 393 */
394 394 Notebook.prototype.ncells = function () {
395 395 return this.get_cell_elements().length;
396 396 };
397 397
398 398 /**
399 399 * Get all Cell objects in this notebook.
400 400 *
401 401 * @method get_cells
402 402 * @return {Array} This notebook's Cell objects
403 403 */
404 404 // TODO: we are often calling cells as cells()[i], which we should optimize
405 405 // to cells(i) or a new method.
406 406 Notebook.prototype.get_cells = function () {
407 407 return this.get_cell_elements().toArray().map(function (e) {
408 408 return $(e).data("cell");
409 409 });
410 410 };
411 411
412 412 /**
413 413 * Get a Cell object from this notebook.
414 414 *
415 415 * @method get_cell
416 416 * @param {Number} index An index of a cell to retrieve
417 417 * @return {Cell} A particular cell
418 418 */
419 419 Notebook.prototype.get_cell = function (index) {
420 420 var result = null;
421 421 var ce = this.get_cell_element(index);
422 422 if (ce !== null) {
423 423 result = ce.data('cell');
424 424 }
425 425 return result;
426 426 };
427 427
428 428 /**
429 429 * Get the cell below a given cell.
430 430 *
431 431 * @method get_next_cell
432 432 * @param {Cell} cell The provided cell
433 433 * @return {Cell} The next cell
434 434 */
435 435 Notebook.prototype.get_next_cell = function (cell) {
436 436 var result = null;
437 437 var index = this.find_cell_index(cell);
438 438 if (this.is_valid_cell_index(index+1)) {
439 439 result = this.get_cell(index+1);
440 440 }
441 441 return result;
442 442 };
443 443
444 444 /**
445 445 * Get the cell above a given cell.
446 446 *
447 447 * @method get_prev_cell
448 448 * @param {Cell} cell The provided cell
449 449 * @return {Cell} The previous cell
450 450 */
451 451 Notebook.prototype.get_prev_cell = function (cell) {
452 452 // TODO: off-by-one
453 453 // nb.get_prev_cell(nb.get_cell(1)) is null
454 454 var result = null;
455 455 var index = this.find_cell_index(cell);
456 456 if (index !== null && index > 1) {
457 457 result = this.get_cell(index-1);
458 458 }
459 459 return result;
460 460 };
461 461
462 462 /**
463 463 * Get the numeric index of a given cell.
464 464 *
465 465 * @method find_cell_index
466 466 * @param {Cell} cell The provided cell
467 467 * @return {Number} The cell's numeric index
468 468 */
469 469 Notebook.prototype.find_cell_index = function (cell) {
470 470 var result = null;
471 471 this.get_cell_elements().filter(function (index) {
472 472 if ($(this).data("cell") === cell) {
473 473 result = index;
474 474 }
475 475 });
476 476 return result;
477 477 };
478 478
479 479 /**
480 480 * Get a given index , or the selected index if none is provided.
481 481 *
482 482 * @method index_or_selected
483 483 * @param {Number} index A cell's index
484 484 * @return {Number} The given index, or selected index if none is provided.
485 485 */
486 486 Notebook.prototype.index_or_selected = function (index) {
487 487 var i;
488 488 if (index === undefined || index === null) {
489 489 i = this.get_selected_index();
490 490 if (i === null) {
491 491 i = 0;
492 492 }
493 493 } else {
494 494 i = index;
495 495 }
496 496 return i;
497 497 };
498 498
499 499 /**
500 500 * Get the currently selected cell.
501 501 * @method get_selected_cell
502 502 * @return {Cell} The selected cell
503 503 */
504 504 Notebook.prototype.get_selected_cell = function () {
505 505 var index = this.get_selected_index();
506 506 return this.get_cell(index);
507 507 };
508 508
509 509 /**
510 510 * Check whether a cell index is valid.
511 511 *
512 512 * @method is_valid_cell_index
513 513 * @param {Number} index A cell index
514 514 * @return True if the index is valid, false otherwise
515 515 */
516 516 Notebook.prototype.is_valid_cell_index = function (index) {
517 517 if (index !== null && index >= 0 && index < this.ncells()) {
518 518 return true;
519 519 } else {
520 520 return false;
521 521 }
522 522 };
523 523
524 524 /**
525 525 * Get the index of the currently selected cell.
526 526
527 527 * @method get_selected_index
528 528 * @return {Number} The selected cell's numeric index
529 529 */
530 530 Notebook.prototype.get_selected_index = function () {
531 531 var result = null;
532 532 this.get_cell_elements().filter(function (index) {
533 533 if ($(this).data("cell").selected === true) {
534 534 result = index;
535 535 }
536 536 });
537 537 return result;
538 538 };
539 539
540 540
541 541 // Cell selection.
542 542
543 543 /**
544 544 * Programmatically select a cell.
545 545 *
546 546 * @method select
547 547 * @param {Number} index A cell's index
548 548 * @return {Notebook} This notebook
549 549 */
550 550 Notebook.prototype.select = function (index) {
551 551 if (this.is_valid_cell_index(index)) {
552 552 var sindex = this.get_selected_index();
553 553 if (sindex !== null && index !== sindex) {
554 554 // If we are about to select a different cell, make sure we are
555 555 // first in command mode.
556 556 if (this.mode !== 'command') {
557 557 this.command_mode();
558 558 }
559 559 this.get_cell(sindex).unselect();
560 560 }
561 561 var cell = this.get_cell(index);
562 562 cell.select();
563 563 if (cell.cell_type === 'heading') {
564 564 this.events.trigger('selected_cell_type_changed.Notebook',
565 565 {'cell_type':cell.cell_type,level:cell.level}
566 566 );
567 567 } else {
568 568 this.events.trigger('selected_cell_type_changed.Notebook',
569 569 {'cell_type':cell.cell_type}
570 570 );
571 571 }
572 572 }
573 573 return this;
574 574 };
575 575
576 576 /**
577 577 * Programmatically select the next cell.
578 578 *
579 579 * @method select_next
580 580 * @return {Notebook} This notebook
581 581 */
582 582 Notebook.prototype.select_next = function () {
583 583 var index = this.get_selected_index();
584 584 this.select(index+1);
585 585 return this;
586 586 };
587 587
588 588 /**
589 589 * Programmatically select the previous cell.
590 590 *
591 591 * @method select_prev
592 592 * @return {Notebook} This notebook
593 593 */
594 594 Notebook.prototype.select_prev = function () {
595 595 var index = this.get_selected_index();
596 596 this.select(index-1);
597 597 return this;
598 598 };
599 599
600 600
601 601 // Edit/Command mode
602 602
603 603 /**
604 604 * Gets the index of the cell that is in edit mode.
605 605 *
606 606 * @method get_edit_index
607 607 *
608 608 * @return index {int}
609 609 **/
610 610 Notebook.prototype.get_edit_index = function () {
611 611 var result = null;
612 612 this.get_cell_elements().filter(function (index) {
613 613 if ($(this).data("cell").mode === 'edit') {
614 614 result = index;
615 615 }
616 616 });
617 617 return result;
618 618 };
619 619
620 620 /**
621 621 * Handle when a a cell blurs and the notebook should enter command mode.
622 622 *
623 623 * @method handle_command_mode
624 624 * @param [cell] {Cell} Cell to enter command mode on.
625 625 **/
626 626 Notebook.prototype.handle_command_mode = function (cell) {
627 627 if (this.mode !== 'command') {
628 628 cell.command_mode();
629 629 this.mode = 'command';
630 630 this.events.trigger('command_mode.Notebook');
631 631 this.keyboard_manager.command_mode();
632 632 }
633 633 };
634 634
635 635 /**
636 636 * Make the notebook enter command mode.
637 637 *
638 638 * @method command_mode
639 639 **/
640 640 Notebook.prototype.command_mode = function () {
641 641 var cell = this.get_cell(this.get_edit_index());
642 642 if (cell && this.mode !== 'command') {
643 643 // We don't call cell.command_mode, but rather call cell.focus_cell()
644 644 // which will blur and CM editor and trigger the call to
645 645 // handle_command_mode.
646 646 cell.focus_cell();
647 647 }
648 648 };
649 649
650 650 /**
651 651 * Handle when a cell fires it's edit_mode event.
652 652 *
653 653 * @method handle_edit_mode
654 654 * @param [cell] {Cell} Cell to enter edit mode on.
655 655 **/
656 656 Notebook.prototype.handle_edit_mode = function (cell) {
657 657 if (cell && this.mode !== 'edit') {
658 658 cell.edit_mode();
659 659 this.mode = 'edit';
660 660 this.events.trigger('edit_mode.Notebook');
661 661 this.keyboard_manager.edit_mode();
662 662 }
663 663 };
664 664
665 665 /**
666 666 * Make a cell enter edit mode.
667 667 *
668 668 * @method edit_mode
669 669 **/
670 670 Notebook.prototype.edit_mode = function () {
671 671 var cell = this.get_selected_cell();
672 672 if (cell && this.mode !== 'edit') {
673 673 cell.unrender();
674 674 cell.focus_editor();
675 675 }
676 676 };
677 677
678 678 /**
679 679 * Focus the currently selected cell.
680 680 *
681 681 * @method focus_cell
682 682 **/
683 683 Notebook.prototype.focus_cell = function () {
684 684 var cell = this.get_selected_cell();
685 685 if (cell === null) {return;} // No cell is selected
686 686 cell.focus_cell();
687 687 };
688 688
689 689 // Cell movement
690 690
691 691 /**
692 692 * Move given (or selected) cell up and select it.
693 693 *
694 694 * @method move_cell_up
695 695 * @param [index] {integer} cell index
696 696 * @return {Notebook} This notebook
697 697 **/
698 698 Notebook.prototype.move_cell_up = function (index) {
699 699 var i = this.index_or_selected(index);
700 700 if (this.is_valid_cell_index(i) && i > 0) {
701 701 var pivot = this.get_cell_element(i-1);
702 702 var tomove = this.get_cell_element(i);
703 703 if (pivot !== null && tomove !== null) {
704 704 tomove.detach();
705 705 pivot.before(tomove);
706 706 this.select(i-1);
707 707 var cell = this.get_selected_cell();
708 708 cell.focus_cell();
709 709 }
710 710 this.set_dirty(true);
711 711 }
712 712 return this;
713 713 };
714 714
715 715
716 716 /**
717 717 * Move given (or selected) cell down and select it
718 718 *
719 719 * @method move_cell_down
720 720 * @param [index] {integer} cell index
721 721 * @return {Notebook} This notebook
722 722 **/
723 723 Notebook.prototype.move_cell_down = function (index) {
724 724 var i = this.index_or_selected(index);
725 725 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
726 726 var pivot = this.get_cell_element(i+1);
727 727 var tomove = this.get_cell_element(i);
728 728 if (pivot !== null && tomove !== null) {
729 729 tomove.detach();
730 730 pivot.after(tomove);
731 731 this.select(i+1);
732 732 var cell = this.get_selected_cell();
733 733 cell.focus_cell();
734 734 }
735 735 }
736 736 this.set_dirty();
737 737 return this;
738 738 };
739 739
740 740
741 741 // Insertion, deletion.
742 742
743 743 /**
744 744 * Delete a cell from the notebook.
745 745 *
746 746 * @method delete_cell
747 747 * @param [index] A cell's numeric index
748 748 * @return {Notebook} This notebook
749 749 */
750 750 Notebook.prototype.delete_cell = function (index) {
751 751 var i = this.index_or_selected(index);
752 752 var cell = this.get_selected_cell();
753 753 this.undelete_backup = cell.toJSON();
754 754 $('#undelete_cell').removeClass('disabled');
755 755 if (this.is_valid_cell_index(i)) {
756 756 var old_ncells = this.ncells();
757 757 var ce = this.get_cell_element(i);
758 758 ce.remove();
759 759 if (i === 0) {
760 760 // Always make sure we have at least one cell.
761 761 if (old_ncells === 1) {
762 762 this.insert_cell_below('code');
763 763 }
764 764 this.select(0);
765 765 this.undelete_index = 0;
766 766 this.undelete_below = false;
767 767 } else if (i === old_ncells-1 && i !== 0) {
768 768 this.select(i-1);
769 769 this.undelete_index = i - 1;
770 770 this.undelete_below = true;
771 771 } else {
772 772 this.select(i);
773 773 this.undelete_index = i;
774 774 this.undelete_below = false;
775 775 }
776 776 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
777 777 this.set_dirty(true);
778 778 }
779 779 return this;
780 780 };
781 781
782 782 /**
783 783 * Restore the most recently deleted cell.
784 784 *
785 785 * @method undelete
786 786 */
787 787 Notebook.prototype.undelete_cell = function() {
788 788 if (this.undelete_backup !== null && this.undelete_index !== null) {
789 789 var current_index = this.get_selected_index();
790 790 if (this.undelete_index < current_index) {
791 791 current_index = current_index + 1;
792 792 }
793 793 if (this.undelete_index >= this.ncells()) {
794 794 this.select(this.ncells() - 1);
795 795 }
796 796 else {
797 797 this.select(this.undelete_index);
798 798 }
799 799 var cell_data = this.undelete_backup;
800 800 var new_cell = null;
801 801 if (this.undelete_below) {
802 802 new_cell = this.insert_cell_below(cell_data.cell_type);
803 803 } else {
804 804 new_cell = this.insert_cell_above(cell_data.cell_type);
805 805 }
806 806 new_cell.fromJSON(cell_data);
807 807 if (this.undelete_below) {
808 808 this.select(current_index+1);
809 809 } else {
810 810 this.select(current_index);
811 811 }
812 812 this.undelete_backup = null;
813 813 this.undelete_index = null;
814 814 }
815 815 $('#undelete_cell').addClass('disabled');
816 816 };
817 817
818 818 /**
819 819 * Insert a cell so that after insertion the cell is at given index.
820 820 *
821 821 * If cell type is not provided, it will default to the type of the
822 822 * currently active cell.
823 823 *
824 824 * Similar to insert_above, but index parameter is mandatory
825 825 *
826 826 * Index will be brought back into the accessible range [0,n]
827 827 *
828 828 * @method insert_cell_at_index
829 829 * @param [type] {string} in ['code','markdown','heading'], defaults to 'code'
830 830 * @param [index] {int} a valid index where to insert cell
831 831 *
832 832 * @return cell {cell|null} created cell or null
833 833 **/
834 834 Notebook.prototype.insert_cell_at_index = function(type, index){
835 835
836 836 var ncells = this.ncells();
837 837 index = Math.min(index,ncells);
838 838 index = Math.max(index,0);
839 839 var cell = null;
840 840 type = type || this.get_selected_cell().cell_type;
841 841
842 842 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
843 843 var cell_options = {
844 844 events: this.events,
845 845 config: this.config,
846 846 keyboard_manager: this.keyboard_manager,
847 847 notebook: this,
848 848 tooltip: this.tooltip,
849 849 };
850 850 if (type === 'code') {
851 851 cell = new codecell.CodeCell(this.kernel, cell_options);
852 852 cell.set_input_prompt();
853 853 } else if (type === 'markdown') {
854 854 cell = new textcell.MarkdownCell(cell_options);
855 855 } else if (type === 'raw') {
856 856 cell = new textcell.RawCell(cell_options);
857 857 } else if (type === 'heading') {
858 858 cell = new textcell.HeadingCell(cell_options);
859 859 }
860 860
861 861 if(this._insert_element_at_index(cell.element,index)) {
862 862 cell.render();
863 863 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
864 864 cell.refresh();
865 865 // We used to select the cell after we refresh it, but there
866 866 // are now cases were this method is called where select is
867 867 // not appropriate. The selection logic should be handled by the
868 868 // caller of the the top level insert_cell methods.
869 869 this.set_dirty(true);
870 870 }
871 871 }
872 872 return cell;
873 873
874 874 };
875 875
876 876 /**
877 877 * Insert an element at given cell index.
878 878 *
879 879 * @method _insert_element_at_index
880 880 * @param element {dom element} a cell element
881 881 * @param [index] {int} a valid index where to inser cell
882 882 * @private
883 883 *
884 884 * return true if everything whent fine.
885 885 **/
886 886 Notebook.prototype._insert_element_at_index = function(element, index){
887 887 if (element === undefined){
888 888 return false;
889 889 }
890 890
891 891 var ncells = this.ncells();
892 892
893 893 if (ncells === 0) {
894 894 // special case append if empty
895 895 this.element.find('div.end_space').before(element);
896 896 } else if ( ncells === index ) {
897 897 // special case append it the end, but not empty
898 898 this.get_cell_element(index-1).after(element);
899 899 } else if (this.is_valid_cell_index(index)) {
900 900 // otherwise always somewhere to append to
901 901 this.get_cell_element(index).before(element);
902 902 } else {
903 903 return false;
904 904 }
905 905
906 906 if (this.undelete_index !== null && index <= this.undelete_index) {
907 907 this.undelete_index = this.undelete_index + 1;
908 908 this.set_dirty(true);
909 909 }
910 910 return true;
911 911 };
912 912
913 913 /**
914 914 * Insert a cell of given type above given index, or at top
915 915 * of notebook if index smaller than 0.
916 916 *
917 917 * default index value is the one of currently selected cell
918 918 *
919 919 * @method insert_cell_above
920 920 * @param [type] {string} cell type
921 921 * @param [index] {integer}
922 922 *
923 923 * @return handle to created cell or null
924 924 **/
925 925 Notebook.prototype.insert_cell_above = function (type, index) {
926 926 index = this.index_or_selected(index);
927 927 return this.insert_cell_at_index(type, index);
928 928 };
929 929
930 930 /**
931 931 * Insert a cell of given type below given index, or at bottom
932 932 * of notebook if index greater than number of cells
933 933 *
934 934 * default index value is the one of currently selected cell
935 935 *
936 936 * @method insert_cell_below
937 937 * @param [type] {string} cell type
938 938 * @param [index] {integer}
939 939 *
940 940 * @return handle to created cell or null
941 941 *
942 942 **/
943 943 Notebook.prototype.insert_cell_below = function (type, index) {
944 944 index = this.index_or_selected(index);
945 945 return this.insert_cell_at_index(type, index+1);
946 946 };
947 947
948 948
949 949 /**
950 950 * Insert cell at end of notebook
951 951 *
952 952 * @method insert_cell_at_bottom
953 953 * @param {String} type cell type
954 954 *
955 955 * @return the added cell; or null
956 956 **/
957 957 Notebook.prototype.insert_cell_at_bottom = function (type){
958 958 var len = this.ncells();
959 959 return this.insert_cell_below(type,len-1);
960 960 };
961 961
962 962 /**
963 963 * Turn a cell into a code cell.
964 964 *
965 965 * @method to_code
966 966 * @param {Number} [index] A cell's index
967 967 */
968 968 Notebook.prototype.to_code = function (index) {
969 969 var i = this.index_or_selected(index);
970 970 if (this.is_valid_cell_index(i)) {
971 971 var source_element = this.get_cell_element(i);
972 972 var source_cell = source_element.data("cell");
973 973 if (!(source_cell instanceof codecell.CodeCell)) {
974 974 var target_cell = this.insert_cell_below('code',i);
975 975 var text = source_cell.get_text();
976 976 if (text === source_cell.placeholder) {
977 977 text = '';
978 978 }
979 979 target_cell.set_text(text);
980 980 // make this value the starting point, so that we can only undo
981 981 // to this state, instead of a blank cell
982 982 target_cell.code_mirror.clearHistory();
983 983 source_element.remove();
984 984 this.select(i);
985 985 var cursor = source_cell.code_mirror.getCursor();
986 986 target_cell.code_mirror.setCursor(cursor);
987 987 this.set_dirty(true);
988 988 }
989 989 }
990 990 };
991 991
992 992 /**
993 993 * Turn a cell into a Markdown cell.
994 994 *
995 995 * @method to_markdown
996 996 * @param {Number} [index] A cell's index
997 997 */
998 998 Notebook.prototype.to_markdown = function (index) {
999 999 var i = this.index_or_selected(index);
1000 1000 if (this.is_valid_cell_index(i)) {
1001 1001 var source_element = this.get_cell_element(i);
1002 1002 var source_cell = source_element.data("cell");
1003 1003 if (!(source_cell instanceof textcell.MarkdownCell)) {
1004 1004 var target_cell = this.insert_cell_below('markdown',i);
1005 1005 var text = source_cell.get_text();
1006 1006 if (text === source_cell.placeholder) {
1007 1007 text = '';
1008 1008 }
1009 1009 // We must show the editor before setting its contents
1010 1010 target_cell.unrender();
1011 1011 target_cell.set_text(text);
1012 1012 // make this value the starting point, so that we can only undo
1013 1013 // to this state, instead of a blank cell
1014 1014 target_cell.code_mirror.clearHistory();
1015 1015 source_element.remove();
1016 1016 this.select(i);
1017 1017 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1018 1018 target_cell.render();
1019 1019 }
1020 1020 var cursor = source_cell.code_mirror.getCursor();
1021 1021 target_cell.code_mirror.setCursor(cursor);
1022 1022 this.set_dirty(true);
1023 1023 }
1024 1024 }
1025 1025 };
1026 1026
1027 1027 /**
1028 1028 * Turn a cell into a raw text cell.
1029 1029 *
1030 1030 * @method to_raw
1031 1031 * @param {Number} [index] A cell's index
1032 1032 */
1033 1033 Notebook.prototype.to_raw = function (index) {
1034 1034 var i = this.index_or_selected(index);
1035 1035 if (this.is_valid_cell_index(i)) {
1036 1036 var source_element = this.get_cell_element(i);
1037 1037 var source_cell = source_element.data("cell");
1038 1038 var target_cell = null;
1039 1039 if (!(source_cell instanceof textcell.RawCell)) {
1040 1040 target_cell = this.insert_cell_below('raw',i);
1041 1041 var text = source_cell.get_text();
1042 1042 if (text === source_cell.placeholder) {
1043 1043 text = '';
1044 1044 }
1045 1045 // We must show the editor before setting its contents
1046 1046 target_cell.unrender();
1047 1047 target_cell.set_text(text);
1048 1048 // make this value the starting point, so that we can only undo
1049 1049 // to this state, instead of a blank cell
1050 1050 target_cell.code_mirror.clearHistory();
1051 1051 source_element.remove();
1052 1052 this.select(i);
1053 1053 var cursor = source_cell.code_mirror.getCursor();
1054 1054 target_cell.code_mirror.setCursor(cursor);
1055 1055 this.set_dirty(true);
1056 1056 }
1057 1057 }
1058 1058 };
1059 1059
1060 1060 /**
1061 1061 * Turn a cell into a heading cell.
1062 1062 *
1063 1063 * @method to_heading
1064 1064 * @param {Number} [index] A cell's index
1065 1065 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
1066 1066 */
1067 1067 Notebook.prototype.to_heading = function (index, level) {
1068 1068 level = level || 1;
1069 1069 var i = this.index_or_selected(index);
1070 1070 if (this.is_valid_cell_index(i)) {
1071 1071 var source_element = this.get_cell_element(i);
1072 1072 var source_cell = source_element.data("cell");
1073 1073 var target_cell = null;
1074 1074 if (source_cell instanceof textcell.HeadingCell) {
1075 1075 source_cell.set_level(level);
1076 1076 } else {
1077 1077 target_cell = this.insert_cell_below('heading',i);
1078 1078 var text = source_cell.get_text();
1079 1079 if (text === source_cell.placeholder) {
1080 1080 text = '';
1081 1081 }
1082 1082 // We must show the editor before setting its contents
1083 1083 target_cell.set_level(level);
1084 1084 target_cell.unrender();
1085 1085 target_cell.set_text(text);
1086 1086 // make this value the starting point, so that we can only undo
1087 1087 // to this state, instead of a blank cell
1088 1088 target_cell.code_mirror.clearHistory();
1089 1089 source_element.remove();
1090 1090 this.select(i);
1091 1091 var cursor = source_cell.code_mirror.getCursor();
1092 1092 target_cell.code_mirror.setCursor(cursor);
1093 1093 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1094 1094 target_cell.render();
1095 1095 }
1096 1096 }
1097 1097 this.set_dirty(true);
1098 1098 this.events.trigger('selected_cell_type_changed.Notebook',
1099 1099 {'cell_type':'heading',level:level}
1100 1100 );
1101 1101 }
1102 1102 };
1103 1103
1104 1104
1105 1105 // Cut/Copy/Paste
1106 1106
1107 1107 /**
1108 1108 * Enable UI elements for pasting cells.
1109 1109 *
1110 1110 * @method enable_paste
1111 1111 */
1112 1112 Notebook.prototype.enable_paste = function () {
1113 1113 var that = this;
1114 1114 if (!this.paste_enabled) {
1115 1115 $('#paste_cell_replace').removeClass('disabled')
1116 1116 .on('click', function () {that.paste_cell_replace();});
1117 1117 $('#paste_cell_above').removeClass('disabled')
1118 1118 .on('click', function () {that.paste_cell_above();});
1119 1119 $('#paste_cell_below').removeClass('disabled')
1120 1120 .on('click', function () {that.paste_cell_below();});
1121 1121 this.paste_enabled = true;
1122 1122 }
1123 1123 };
1124 1124
1125 1125 /**
1126 1126 * Disable UI elements for pasting cells.
1127 1127 *
1128 1128 * @method disable_paste
1129 1129 */
1130 1130 Notebook.prototype.disable_paste = function () {
1131 1131 if (this.paste_enabled) {
1132 1132 $('#paste_cell_replace').addClass('disabled').off('click');
1133 1133 $('#paste_cell_above').addClass('disabled').off('click');
1134 1134 $('#paste_cell_below').addClass('disabled').off('click');
1135 1135 this.paste_enabled = false;
1136 1136 }
1137 1137 };
1138 1138
1139 1139 /**
1140 1140 * Cut a cell.
1141 1141 *
1142 1142 * @method cut_cell
1143 1143 */
1144 1144 Notebook.prototype.cut_cell = function () {
1145 1145 this.copy_cell();
1146 1146 this.delete_cell();
1147 1147 };
1148 1148
1149 1149 /**
1150 1150 * Copy a cell.
1151 1151 *
1152 1152 * @method copy_cell
1153 1153 */
1154 1154 Notebook.prototype.copy_cell = function () {
1155 1155 var cell = this.get_selected_cell();
1156 1156 this.clipboard = cell.toJSON();
1157 1157 this.enable_paste();
1158 1158 };
1159 1159
1160 1160 /**
1161 1161 * Replace the selected cell with a cell in the clipboard.
1162 1162 *
1163 1163 * @method paste_cell_replace
1164 1164 */
1165 1165 Notebook.prototype.paste_cell_replace = function () {
1166 1166 if (this.clipboard !== null && this.paste_enabled) {
1167 1167 var cell_data = this.clipboard;
1168 1168 var new_cell = this.insert_cell_above(cell_data.cell_type);
1169 1169 new_cell.fromJSON(cell_data);
1170 1170 var old_cell = this.get_next_cell(new_cell);
1171 1171 this.delete_cell(this.find_cell_index(old_cell));
1172 1172 this.select(this.find_cell_index(new_cell));
1173 1173 }
1174 1174 };
1175 1175
1176 1176 /**
1177 1177 * Paste a cell from the clipboard above the selected cell.
1178 1178 *
1179 1179 * @method paste_cell_above
1180 1180 */
1181 1181 Notebook.prototype.paste_cell_above = function () {
1182 1182 if (this.clipboard !== null && this.paste_enabled) {
1183 1183 var cell_data = this.clipboard;
1184 1184 var new_cell = this.insert_cell_above(cell_data.cell_type);
1185 1185 new_cell.fromJSON(cell_data);
1186 1186 new_cell.focus_cell();
1187 1187 }
1188 1188 };
1189 1189
1190 1190 /**
1191 1191 * Paste a cell from the clipboard below the selected cell.
1192 1192 *
1193 1193 * @method paste_cell_below
1194 1194 */
1195 1195 Notebook.prototype.paste_cell_below = function () {
1196 1196 if (this.clipboard !== null && this.paste_enabled) {
1197 1197 var cell_data = this.clipboard;
1198 1198 var new_cell = this.insert_cell_below(cell_data.cell_type);
1199 1199 new_cell.fromJSON(cell_data);
1200 1200 new_cell.focus_cell();
1201 1201 }
1202 1202 };
1203 1203
1204 1204 // Split/merge
1205 1205
1206 1206 /**
1207 1207 * Split the selected cell into two, at the cursor.
1208 1208 *
1209 1209 * @method split_cell
1210 1210 */
1211 1211 Notebook.prototype.split_cell = function () {
1212 1212 var mdc = textcell.MarkdownCell;
1213 1213 var rc = textcell.RawCell;
1214 1214 var cell = this.get_selected_cell();
1215 1215 if (cell.is_splittable()) {
1216 1216 var texta = cell.get_pre_cursor();
1217 1217 var textb = cell.get_post_cursor();
1218 1218 cell.set_text(textb);
1219 1219 var new_cell = this.insert_cell_above(cell.cell_type);
1220 1220 // Unrender the new cell so we can call set_text.
1221 1221 new_cell.unrender();
1222 1222 new_cell.set_text(texta);
1223 1223 }
1224 1224 };
1225 1225
1226 1226 /**
1227 1227 * Combine the selected cell into the cell above it.
1228 1228 *
1229 1229 * @method merge_cell_above
1230 1230 */
1231 1231 Notebook.prototype.merge_cell_above = function () {
1232 1232 var mdc = textcell.MarkdownCell;
1233 1233 var rc = textcell.RawCell;
1234 1234 var index = this.get_selected_index();
1235 1235 var cell = this.get_cell(index);
1236 1236 var render = cell.rendered;
1237 1237 if (!cell.is_mergeable()) {
1238 1238 return;
1239 1239 }
1240 1240 if (index > 0) {
1241 1241 var upper_cell = this.get_cell(index-1);
1242 1242 if (!upper_cell.is_mergeable()) {
1243 1243 return;
1244 1244 }
1245 1245 var upper_text = upper_cell.get_text();
1246 1246 var text = cell.get_text();
1247 1247 if (cell instanceof codecell.CodeCell) {
1248 1248 cell.set_text(upper_text+'\n'+text);
1249 1249 } else {
1250 1250 cell.unrender(); // Must unrender before we set_text.
1251 1251 cell.set_text(upper_text+'\n\n'+text);
1252 1252 if (render) {
1253 1253 // The rendered state of the final cell should match
1254 1254 // that of the original selected cell;
1255 1255 cell.render();
1256 1256 }
1257 1257 }
1258 1258 this.delete_cell(index-1);
1259 1259 this.select(this.find_cell_index(cell));
1260 1260 }
1261 1261 };
1262 1262
1263 1263 /**
1264 1264 * Combine the selected cell into the cell below it.
1265 1265 *
1266 1266 * @method merge_cell_below
1267 1267 */
1268 1268 Notebook.prototype.merge_cell_below = function () {
1269 1269 var mdc = textcell.MarkdownCell;
1270 1270 var rc = textcell.RawCell;
1271 1271 var index = this.get_selected_index();
1272 1272 var cell = this.get_cell(index);
1273 1273 var render = cell.rendered;
1274 1274 if (!cell.is_mergeable()) {
1275 1275 return;
1276 1276 }
1277 1277 if (index < this.ncells()-1) {
1278 1278 var lower_cell = this.get_cell(index+1);
1279 1279 if (!lower_cell.is_mergeable()) {
1280 1280 return;
1281 1281 }
1282 1282 var lower_text = lower_cell.get_text();
1283 1283 var text = cell.get_text();
1284 1284 if (cell instanceof codecell.CodeCell) {
1285 1285 cell.set_text(text+'\n'+lower_text);
1286 1286 } else {
1287 1287 cell.unrender(); // Must unrender before we set_text.
1288 1288 cell.set_text(text+'\n\n'+lower_text);
1289 1289 if (render) {
1290 1290 // The rendered state of the final cell should match
1291 1291 // that of the original selected cell;
1292 1292 cell.render();
1293 1293 }
1294 1294 }
1295 1295 this.delete_cell(index+1);
1296 1296 this.select(this.find_cell_index(cell));
1297 1297 }
1298 1298 };
1299 1299
1300 1300
1301 1301 // Cell collapsing and output clearing
1302 1302
1303 1303 /**
1304 1304 * Hide a cell's output.
1305 1305 *
1306 1306 * @method collapse_output
1307 1307 * @param {Number} index A cell's numeric index
1308 1308 */
1309 1309 Notebook.prototype.collapse_output = function (index) {
1310 1310 var i = this.index_or_selected(index);
1311 1311 var cell = this.get_cell(i);
1312 1312 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1313 1313 cell.collapse_output();
1314 1314 this.set_dirty(true);
1315 1315 }
1316 1316 };
1317 1317
1318 1318 /**
1319 1319 * Hide each code cell's output area.
1320 1320 *
1321 1321 * @method collapse_all_output
1322 1322 */
1323 1323 Notebook.prototype.collapse_all_output = function () {
1324 1324 $.map(this.get_cells(), function (cell, i) {
1325 1325 if (cell instanceof codecell.CodeCell) {
1326 1326 cell.collapse_output();
1327 1327 }
1328 1328 });
1329 1329 // this should not be set if the `collapse` key is removed from nbformat
1330 1330 this.set_dirty(true);
1331 1331 };
1332 1332
1333 1333 /**
1334 1334 * Show a cell's output.
1335 1335 *
1336 1336 * @method expand_output
1337 1337 * @param {Number} index A cell's numeric index
1338 1338 */
1339 1339 Notebook.prototype.expand_output = function (index) {
1340 1340 var i = this.index_or_selected(index);
1341 1341 var cell = this.get_cell(i);
1342 1342 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1343 1343 cell.expand_output();
1344 1344 this.set_dirty(true);
1345 1345 }
1346 1346 };
1347 1347
1348 1348 /**
1349 1349 * Expand each code cell's output area, and remove scrollbars.
1350 1350 *
1351 1351 * @method expand_all_output
1352 1352 */
1353 1353 Notebook.prototype.expand_all_output = function () {
1354 1354 $.map(this.get_cells(), function (cell, i) {
1355 1355 if (cell instanceof codecell.CodeCell) {
1356 1356 cell.expand_output();
1357 1357 }
1358 1358 });
1359 1359 // this should not be set if the `collapse` key is removed from nbformat
1360 1360 this.set_dirty(true);
1361 1361 };
1362 1362
1363 1363 /**
1364 1364 * Clear the selected CodeCell's output area.
1365 1365 *
1366 1366 * @method clear_output
1367 1367 * @param {Number} index A cell's numeric index
1368 1368 */
1369 1369 Notebook.prototype.clear_output = function (index) {
1370 1370 var i = this.index_or_selected(index);
1371 1371 var cell = this.get_cell(i);
1372 1372 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1373 1373 cell.clear_output();
1374 1374 this.set_dirty(true);
1375 1375 }
1376 1376 };
1377 1377
1378 1378 /**
1379 1379 * Clear each code cell's output area.
1380 1380 *
1381 1381 * @method clear_all_output
1382 1382 */
1383 1383 Notebook.prototype.clear_all_output = function () {
1384 1384 $.map(this.get_cells(), function (cell, i) {
1385 1385 if (cell instanceof codecell.CodeCell) {
1386 1386 cell.clear_output();
1387 1387 }
1388 1388 });
1389 1389 this.set_dirty(true);
1390 1390 };
1391 1391
1392 1392 /**
1393 1393 * Scroll the selected CodeCell's output area.
1394 1394 *
1395 1395 * @method scroll_output
1396 1396 * @param {Number} index A cell's numeric index
1397 1397 */
1398 1398 Notebook.prototype.scroll_output = function (index) {
1399 1399 var i = this.index_or_selected(index);
1400 1400 var cell = this.get_cell(i);
1401 1401 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1402 1402 cell.scroll_output();
1403 1403 this.set_dirty(true);
1404 1404 }
1405 1405 };
1406 1406
1407 1407 /**
1408 1408 * Expand each code cell's output area, and add a scrollbar for long output.
1409 1409 *
1410 1410 * @method scroll_all_output
1411 1411 */
1412 1412 Notebook.prototype.scroll_all_output = function () {
1413 1413 $.map(this.get_cells(), function (cell, i) {
1414 1414 if (cell instanceof codecell.CodeCell) {
1415 1415 cell.scroll_output();
1416 1416 }
1417 1417 });
1418 1418 // this should not be set if the `collapse` key is removed from nbformat
1419 1419 this.set_dirty(true);
1420 1420 };
1421 1421
1422 1422 /** Toggle whether a cell's output is collapsed or expanded.
1423 1423 *
1424 1424 * @method toggle_output
1425 1425 * @param {Number} index A cell's numeric index
1426 1426 */
1427 1427 Notebook.prototype.toggle_output = function (index) {
1428 1428 var i = this.index_or_selected(index);
1429 1429 var cell = this.get_cell(i);
1430 1430 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1431 1431 cell.toggle_output();
1432 1432 this.set_dirty(true);
1433 1433 }
1434 1434 };
1435 1435
1436 1436 /**
1437 1437 * Hide/show the output of all cells.
1438 1438 *
1439 1439 * @method toggle_all_output
1440 1440 */
1441 1441 Notebook.prototype.toggle_all_output = function () {
1442 1442 $.map(this.get_cells(), function (cell, i) {
1443 1443 if (cell instanceof codecell.CodeCell) {
1444 1444 cell.toggle_output();
1445 1445 }
1446 1446 });
1447 1447 // this should not be set if the `collapse` key is removed from nbformat
1448 1448 this.set_dirty(true);
1449 1449 };
1450 1450
1451 1451 /**
1452 1452 * Toggle a scrollbar for long cell outputs.
1453 1453 *
1454 1454 * @method toggle_output_scroll
1455 1455 * @param {Number} index A cell's numeric index
1456 1456 */
1457 1457 Notebook.prototype.toggle_output_scroll = function (index) {
1458 1458 var i = this.index_or_selected(index);
1459 1459 var cell = this.get_cell(i);
1460 1460 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1461 1461 cell.toggle_output_scroll();
1462 1462 this.set_dirty(true);
1463 1463 }
1464 1464 };
1465 1465
1466 1466 /**
1467 1467 * Toggle the scrolling of long output on all cells.
1468 1468 *
1469 1469 * @method toggle_all_output_scrolling
1470 1470 */
1471 1471 Notebook.prototype.toggle_all_output_scroll = function () {
1472 1472 $.map(this.get_cells(), function (cell, i) {
1473 1473 if (cell instanceof codecell.CodeCell) {
1474 1474 cell.toggle_output_scroll();
1475 1475 }
1476 1476 });
1477 1477 // this should not be set if the `collapse` key is removed from nbformat
1478 1478 this.set_dirty(true);
1479 1479 };
1480 1480
1481 1481 // Other cell functions: line numbers, ...
1482 1482
1483 1483 /**
1484 1484 * Toggle line numbers in the selected cell's input area.
1485 1485 *
1486 1486 * @method cell_toggle_line_numbers
1487 1487 */
1488 1488 Notebook.prototype.cell_toggle_line_numbers = function() {
1489 1489 this.get_selected_cell().toggle_line_numbers();
1490 1490 };
1491 1491
1492 1492 /**
1493 1493 * Set the codemirror mode for all code cells, including the default for
1494 1494 * new code cells.
1495 1495 *
1496 1496 * @method set_codemirror_mode
1497 1497 */
1498 1498 Notebook.prototype.set_codemirror_mode = function(newmode){
1499 1499 if (newmode === this.codemirror_mode) {
1500 1500 return;
1501 1501 }
1502 1502 this.codemirror_mode = newmode;
1503 1503 codecell.CodeCell.options_default.cm_config.mode = newmode;
1504 1504 modename = newmode.name || newmode
1505 1505
1506 1506 that = this;
1507 1507 CodeMirror.requireMode(modename, function(){
1508 1508 $.map(that.get_cells(), function(cell, i) {
1509 1509 if (cell.cell_type === 'code'){
1510 1510 cell.code_mirror.setOption('mode', newmode);
1511 1511 // This is currently redundant, because cm_config ends up as
1512 1512 // codemirror's own .options object, but I don't want to
1513 1513 // rely on that.
1514 1514 cell.cm_config.mode = newmode;
1515 1515 }
1516 1516 });
1517 1517 })
1518 1518 };
1519 1519
1520 1520 // Session related things
1521 1521
1522 1522 /**
1523 1523 * Start a new session and set it on each code cell.
1524 1524 *
1525 1525 * @method start_session
1526 1526 */
1527 1527 Notebook.prototype.start_session = function (kernel_name) {
1528 1528 if (kernel_name === undefined) {
1529 1529 kernel_name = this.default_kernel_name;
1530 1530 }
1531 1531 this.session = new session.Session({
1532 1532 base_url: this.base_url,
1533 1533 ws_url: this.ws_url,
1534 1534 notebook_path: this.notebook_path,
1535 1535 notebook_name: this.notebook_name,
1536 1536 // For now, create all sessions with the 'python' kernel, which is the
1537 1537 // default. Later, the user will be able to select kernels. This is
1538 1538 // overridden if KernelManager.kernel_cmd is specified for the server.
1539 1539 kernel_name: kernel_name,
1540 1540 notebook: this});
1541 1541
1542 1542 this.session.start($.proxy(this._session_started, this));
1543 1543 };
1544 1544
1545 1545
1546 1546 /**
1547 1547 * Once a session is started, link the code cells to the kernel and pass the
1548 1548 * comm manager to the widget manager
1549 1549 *
1550 1550 */
1551 1551 Notebook.prototype._session_started = function(){
1552 1552 this.kernel = this.session.kernel;
1553 1553 var ncells = this.ncells();
1554 1554 for (var i=0; i<ncells; i++) {
1555 1555 var cell = this.get_cell(i);
1556 1556 if (cell instanceof codecell.CodeCell) {
1557 1557 cell.set_kernel(this.session.kernel);
1558 1558 }
1559 1559 }
1560 1560 };
1561 1561
1562 1562 /**
1563 1563 * Prompt the user to restart the IPython kernel.
1564 1564 *
1565 1565 * @method restart_kernel
1566 1566 */
1567 1567 Notebook.prototype.restart_kernel = function () {
1568 1568 var that = this;
1569 1569 dialog.modal({
1570 1570 notebook: this,
1571 1571 keyboard_manager: this.keyboard_manager,
1572 1572 title : "Restart kernel or continue running?",
1573 1573 body : $("<p/>").text(
1574 1574 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1575 1575 ),
1576 1576 buttons : {
1577 1577 "Continue running" : {},
1578 1578 "Restart" : {
1579 1579 "class" : "btn-danger",
1580 1580 "click" : function() {
1581 1581 that.session.restart_kernel();
1582 1582 }
1583 1583 }
1584 1584 }
1585 1585 });
1586 1586 };
1587 1587
1588 1588 /**
1589 1589 * Execute or render cell outputs and go into command mode.
1590 1590 *
1591 1591 * @method execute_cell
1592 1592 */
1593 1593 Notebook.prototype.execute_cell = function () {
1594 1594 // mode = shift, ctrl, alt
1595 1595 var cell = this.get_selected_cell();
1596 1596 var cell_index = this.find_cell_index(cell);
1597 1597
1598 1598 cell.execute();
1599 1599 this.command_mode();
1600 1600 this.set_dirty(true);
1601 1601 };
1602 1602
1603 1603 /**
1604 1604 * Execute or render cell outputs and insert a new cell below.
1605 1605 *
1606 1606 * @method execute_cell_and_insert_below
1607 1607 */
1608 1608 Notebook.prototype.execute_cell_and_insert_below = function () {
1609 1609 var cell = this.get_selected_cell();
1610 1610 var cell_index = this.find_cell_index(cell);
1611 1611
1612 1612 cell.execute();
1613 1613
1614 1614 // If we are at the end always insert a new cell and return
1615 1615 if (cell_index === (this.ncells()-1)) {
1616 1616 this.command_mode();
1617 1617 this.insert_cell_below();
1618 1618 this.select(cell_index+1);
1619 1619 this.edit_mode();
1620 1620 this.scroll_to_bottom();
1621 1621 this.set_dirty(true);
1622 1622 return;
1623 1623 }
1624 1624
1625 1625 this.command_mode();
1626 1626 this.insert_cell_below();
1627 1627 this.select(cell_index+1);
1628 1628 this.edit_mode();
1629 1629 this.set_dirty(true);
1630 1630 };
1631 1631
1632 1632 /**
1633 1633 * Execute or render cell outputs and select the next cell.
1634 1634 *
1635 1635 * @method execute_cell_and_select_below
1636 1636 */
1637 1637 Notebook.prototype.execute_cell_and_select_below = function () {
1638 1638
1639 1639 var cell = this.get_selected_cell();
1640 1640 var cell_index = this.find_cell_index(cell);
1641 1641
1642 1642 cell.execute();
1643 1643
1644 1644 // If we are at the end always insert a new cell and return
1645 1645 if (cell_index === (this.ncells()-1)) {
1646 1646 this.command_mode();
1647 1647 this.insert_cell_below();
1648 1648 this.select(cell_index+1);
1649 1649 this.edit_mode();
1650 1650 this.scroll_to_bottom();
1651 1651 this.set_dirty(true);
1652 1652 return;
1653 1653 }
1654 1654
1655 1655 this.command_mode();
1656 1656 this.select(cell_index+1);
1657 1657 this.focus_cell();
1658 1658 this.set_dirty(true);
1659 1659 };
1660 1660
1661 1661 /**
1662 1662 * Execute all cells below the selected cell.
1663 1663 *
1664 1664 * @method execute_cells_below
1665 1665 */
1666 1666 Notebook.prototype.execute_cells_below = function () {
1667 1667 this.execute_cell_range(this.get_selected_index(), this.ncells());
1668 1668 this.scroll_to_bottom();
1669 1669 };
1670 1670
1671 1671 /**
1672 1672 * Execute all cells above the selected cell.
1673 1673 *
1674 1674 * @method execute_cells_above
1675 1675 */
1676 1676 Notebook.prototype.execute_cells_above = function () {
1677 1677 this.execute_cell_range(0, this.get_selected_index());
1678 1678 };
1679 1679
1680 1680 /**
1681 1681 * Execute all cells.
1682 1682 *
1683 1683 * @method execute_all_cells
1684 1684 */
1685 1685 Notebook.prototype.execute_all_cells = function () {
1686 1686 this.execute_cell_range(0, this.ncells());
1687 1687 this.scroll_to_bottom();
1688 1688 };
1689 1689
1690 1690 /**
1691 1691 * Execute a contiguous range of cells.
1692 1692 *
1693 1693 * @method execute_cell_range
1694 1694 * @param {Number} start Index of the first cell to execute (inclusive)
1695 1695 * @param {Number} end Index of the last cell to execute (exclusive)
1696 1696 */
1697 1697 Notebook.prototype.execute_cell_range = function (start, end) {
1698 1698 this.command_mode();
1699 1699 for (var i=start; i<end; i++) {
1700 1700 this.select(i);
1701 1701 this.execute_cell();
1702 1702 }
1703 1703 };
1704 1704
1705 1705 // Persistance and loading
1706 1706
1707 1707 /**
1708 1708 * Getter method for this notebook's name.
1709 1709 *
1710 1710 * @method get_notebook_name
1711 1711 * @return {String} This notebook's name (excluding file extension)
1712 1712 */
1713 1713 Notebook.prototype.get_notebook_name = function () {
1714 1714 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1715 1715 return nbname;
1716 1716 };
1717 1717
1718 1718 /**
1719 1719 * Setter method for this notebook's name.
1720 1720 *
1721 1721 * @method set_notebook_name
1722 1722 * @param {String} name A new name for this notebook
1723 1723 */
1724 1724 Notebook.prototype.set_notebook_name = function (name) {
1725 1725 this.notebook_name = name;
1726 1726 };
1727 1727
1728 1728 /**
1729 1729 * Check that a notebook's name is valid.
1730 1730 *
1731 1731 * @method test_notebook_name
1732 1732 * @param {String} nbname A name for this notebook
1733 1733 * @return {Boolean} True if the name is valid, false if invalid
1734 1734 */
1735 1735 Notebook.prototype.test_notebook_name = function (nbname) {
1736 1736 nbname = nbname || '';
1737 1737 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1738 1738 return true;
1739 1739 } else {
1740 1740 return false;
1741 1741 }
1742 1742 };
1743 1743
1744 1744 /**
1745 1745 * Load a notebook from JSON (.ipynb).
1746 1746 *
1747 1747 * This currently handles one worksheet: others are deleted.
1748 1748 *
1749 1749 * @method fromJSON
1750 1750 * @param {Object} data JSON representation of a notebook
1751 1751 */
1752 1752 Notebook.prototype.fromJSON = function (data) {
1753 1753 var content = data.content;
1754 1754 var ncells = this.ncells();
1755 1755 var i;
1756 1756 for (i=0; i<ncells; i++) {
1757 1757 // Always delete cell 0 as they get renumbered as they are deleted.
1758 1758 this.delete_cell(0);
1759 1759 }
1760 1760 // Save the metadata and name.
1761 1761 this.metadata = content.metadata;
1762 1762 this.notebook_name = data.name;
1763 1763 var trusted = true;
1764 1764
1765 1765 // Trigger an event changing the kernel spec - this will set the default
1766 1766 // codemirror mode
1767 1767 if (this.metadata.kernelspec !== undefined) {
1768 1768 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1769 1769 }
1770 1770
1771 1771 // Only handle 1 worksheet for now.
1772 1772 var worksheet = content.worksheets[0];
1773 1773 if (worksheet !== undefined) {
1774 1774 if (worksheet.metadata) {
1775 1775 this.worksheet_metadata = worksheet.metadata;
1776 1776 }
1777 1777 var new_cells = worksheet.cells;
1778 1778 ncells = new_cells.length;
1779 1779 var cell_data = null;
1780 1780 var new_cell = null;
1781 1781 for (i=0; i<ncells; i++) {
1782 1782 cell_data = new_cells[i];
1783 1783 // VERSIONHACK: plaintext -> raw
1784 1784 // handle never-released plaintext name for raw cells
1785 1785 if (cell_data.cell_type === 'plaintext'){
1786 1786 cell_data.cell_type = 'raw';
1787 1787 }
1788 1788
1789 1789 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1790 1790 new_cell.fromJSON(cell_data);
1791 1791 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1792 1792 trusted = false;
1793 1793 }
1794 1794 }
1795 1795 }
1796 1796 if (trusted != this.trusted) {
1797 1797 this.trusted = trusted;
1798 1798 this.events.trigger("trust_changed.Notebook", trusted);
1799 1799 }
1800 1800 if (content.worksheets.length > 1) {
1801 1801 dialog.modal({
1802 1802 notebook: this,
1803 1803 keyboard_manager: this.keyboard_manager,
1804 1804 title : "Multiple worksheets",
1805 1805 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1806 1806 "but this version of IPython can only handle the first. " +
1807 1807 "If you save this notebook, worksheets after the first will be lost.",
1808 1808 buttons : {
1809 1809 OK : {
1810 1810 class : "btn-danger"
1811 1811 }
1812 1812 }
1813 1813 });
1814 1814 }
1815 1815 };
1816 1816
1817 1817 /**
1818 1818 * Dump this notebook into a JSON-friendly object.
1819 1819 *
1820 1820 * @method toJSON
1821 1821 * @return {Object} A JSON-friendly representation of this notebook.
1822 1822 */
1823 1823 Notebook.prototype.toJSON = function () {
1824 1824 var cells = this.get_cells();
1825 1825 var ncells = cells.length;
1826 1826 var cell_array = new Array(ncells);
1827 1827 var trusted = true;
1828 1828 for (var i=0; i<ncells; i++) {
1829 1829 var cell = cells[i];
1830 1830 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1831 1831 trusted = false;
1832 1832 }
1833 1833 cell_array[i] = cell.toJSON();
1834 1834 }
1835 1835 var data = {
1836 1836 // Only handle 1 worksheet for now.
1837 1837 worksheets : [{
1838 1838 cells: cell_array,
1839 1839 metadata: this.worksheet_metadata
1840 1840 }],
1841 1841 metadata : this.metadata
1842 1842 };
1843 1843 if (trusted != this.trusted) {
1844 1844 this.trusted = trusted;
1845 1845 this.events.trigger("trust_changed.Notebook", trusted);
1846 1846 }
1847 1847 return data;
1848 1848 };
1849 1849
1850 1850 /**
1851 1851 * Start an autosave timer, for periodically saving the notebook.
1852 1852 *
1853 1853 * @method set_autosave_interval
1854 1854 * @param {Integer} interval the autosave interval in milliseconds
1855 1855 */
1856 1856 Notebook.prototype.set_autosave_interval = function (interval) {
1857 1857 var that = this;
1858 1858 // clear previous interval, so we don't get simultaneous timers
1859 1859 if (this.autosave_timer) {
1860 1860 clearInterval(this.autosave_timer);
1861 1861 }
1862 1862
1863 1863 this.autosave_interval = this.minimum_autosave_interval = interval;
1864 1864 if (interval) {
1865 1865 this.autosave_timer = setInterval(function() {
1866 1866 if (that.dirty) {
1867 1867 that.save_notebook();
1868 1868 }
1869 1869 }, interval);
1870 1870 this.events.trigger("autosave_enabled.Notebook", interval);
1871 1871 } else {
1872 1872 this.autosave_timer = null;
1873 1873 this.events.trigger("autosave_disabled.Notebook");
1874 1874 }
1875 1875 };
1876 1876
1877 1877 /**
1878 1878 * Save this notebook on the server. This becomes a notebook instance's
1879 1879 * .save_notebook method *after* the entire notebook has been loaded.
1880 1880 *
1881 1881 * @method save_notebook
1882 1882 */
1883 1883 Notebook.prototype.save_notebook = function (extra_settings) {
1884 1884 // Create a JSON model to be sent to the server.
1885 1885 var model = {};
1886 1886 model.name = this.notebook_name;
1887 1887 model.path = this.notebook_path;
1888 1888 model.content = this.toJSON();
1889 1889 model.content.nbformat = this.nbformat;
1890 1890 model.content.nbformat_minor = this.nbformat_minor;
1891 1891 // time the ajax call for autosave tuning purposes.
1892 1892 var start = new Date().getTime();
1893 1893 // We do the call with settings so we can set cache to false.
1894 1894 var settings = {
1895 1895 processData : false,
1896 1896 cache : false,
1897 1897 type : "PUT",
1898 1898 data : JSON.stringify(model),
1899 1899 headers : {'Content-Type': 'application/json'},
1900 1900 success : $.proxy(this.save_notebook_success, this, start),
1901 1901 error : $.proxy(this.save_notebook_error, this)
1902 1902 };
1903 1903 if (extra_settings) {
1904 1904 for (var key in extra_settings) {
1905 1905 settings[key] = extra_settings[key];
1906 1906 }
1907 1907 }
1908 1908 this.events.trigger('notebook_saving.Notebook');
1909 1909 var url = utils.url_join_encode(
1910 1910 this.base_url,
1911 'api/notebooks',
1911 'api/contents',
1912 1912 this.notebook_path,
1913 1913 this.notebook_name
1914 1914 );
1915 1915 $.ajax(url, settings);
1916 1916 };
1917 1917
1918 1918 /**
1919 1919 * Success callback for saving a notebook.
1920 1920 *
1921 1921 * @method save_notebook_success
1922 1922 * @param {Integer} start the time when the save request started
1923 1923 * @param {Object} data JSON representation of a notebook
1924 1924 * @param {String} status Description of response status
1925 1925 * @param {jqXHR} xhr jQuery Ajax object
1926 1926 */
1927 1927 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1928 1928 this.set_dirty(false);
1929 1929 this.events.trigger('notebook_saved.Notebook');
1930 1930 this._update_autosave_interval(start);
1931 1931 if (this._checkpoint_after_save) {
1932 1932 this.create_checkpoint();
1933 1933 this._checkpoint_after_save = false;
1934 1934 }
1935 1935 };
1936 1936
1937 1937 /**
1938 1938 * update the autosave interval based on how long the last save took
1939 1939 *
1940 1940 * @method _update_autosave_interval
1941 1941 * @param {Integer} timestamp when the save request started
1942 1942 */
1943 1943 Notebook.prototype._update_autosave_interval = function (start) {
1944 1944 var duration = (new Date().getTime() - start);
1945 1945 if (this.autosave_interval) {
1946 1946 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1947 1947 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1948 1948 // round to 10 seconds, otherwise we will be setting a new interval too often
1949 1949 interval = 10000 * Math.round(interval / 10000);
1950 1950 // set new interval, if it's changed
1951 1951 if (interval != this.autosave_interval) {
1952 1952 this.set_autosave_interval(interval);
1953 1953 }
1954 1954 }
1955 1955 };
1956 1956
1957 1957 /**
1958 1958 * Failure callback for saving a notebook.
1959 1959 *
1960 1960 * @method save_notebook_error
1961 1961 * @param {jqXHR} xhr jQuery Ajax object
1962 1962 * @param {String} status Description of response status
1963 1963 * @param {String} error HTTP error message
1964 1964 */
1965 1965 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
1966 1966 this.events.trigger('notebook_save_failed.Notebook', [xhr, status, error]);
1967 1967 };
1968 1968
1969 1969 /**
1970 1970 * Explicitly trust the output of this notebook.
1971 1971 *
1972 1972 * @method trust_notebook
1973 1973 */
1974 1974 Notebook.prototype.trust_notebook = function (extra_settings) {
1975 1975 var body = $("<div>").append($("<p>")
1976 1976 .text("A trusted IPython notebook may execute hidden malicious code ")
1977 1977 .append($("<strong>")
1978 1978 .append(
1979 1979 $("<em>").text("when you open it")
1980 1980 )
1981 1981 ).append(".").append(
1982 1982 " Selecting trust will immediately reload this notebook in a trusted state."
1983 1983 ).append(
1984 1984 " For more information, see the "
1985 1985 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
1986 1986 .text("IPython security documentation")
1987 1987 ).append(".")
1988 1988 );
1989 1989
1990 1990 var nb = this;
1991 1991 dialog.modal({
1992 1992 notebook: this,
1993 1993 keyboard_manager: this.keyboard_manager,
1994 1994 title: "Trust this notebook?",
1995 1995 body: body,
1996 1996
1997 1997 buttons: {
1998 1998 Cancel : {},
1999 1999 Trust : {
2000 2000 class : "btn-danger",
2001 2001 click : function () {
2002 2002 var cells = nb.get_cells();
2003 2003 for (var i = 0; i < cells.length; i++) {
2004 2004 var cell = cells[i];
2005 2005 if (cell.cell_type == 'code') {
2006 2006 cell.output_area.trusted = true;
2007 2007 }
2008 2008 }
2009 2009 this.events.on('notebook_saved.Notebook', function () {
2010 2010 window.location.reload();
2011 2011 });
2012 2012 nb.save_notebook();
2013 2013 }
2014 2014 }
2015 2015 }
2016 2016 });
2017 2017 };
2018 2018
2019 2019 Notebook.prototype.new_notebook = function(){
2020 2020 var path = this.notebook_path;
2021 2021 var base_url = this.base_url;
2022 2022 var settings = {
2023 2023 processData : false,
2024 2024 cache : false,
2025 2025 type : "POST",
2026 2026 dataType : "json",
2027 2027 async : false,
2028 2028 success : function (data, status, xhr){
2029 2029 var notebook_name = data.name;
2030 2030 window.open(
2031 2031 utils.url_join_encode(
2032 2032 base_url,
2033 2033 'notebooks',
2034 2034 path,
2035 2035 notebook_name
2036 2036 ),
2037 2037 '_blank'
2038 2038 );
2039 2039 },
2040 2040 error : utils.log_ajax_error,
2041 2041 };
2042 2042 var url = utils.url_join_encode(
2043 2043 base_url,
2044 'api/notebooks',
2044 'api/contents',
2045 2045 path
2046 2046 );
2047 2047 $.ajax(url,settings);
2048 2048 };
2049 2049
2050 2050
2051 2051 Notebook.prototype.copy_notebook = function(){
2052 2052 var path = this.notebook_path;
2053 2053 var base_url = this.base_url;
2054 2054 var settings = {
2055 2055 processData : false,
2056 2056 cache : false,
2057 2057 type : "POST",
2058 2058 dataType : "json",
2059 2059 data : JSON.stringify({copy_from : this.notebook_name}),
2060 2060 async : false,
2061 2061 success : function (data, status, xhr) {
2062 2062 window.open(utils.url_join_encode(
2063 2063 base_url,
2064 2064 'notebooks',
2065 2065 data.path,
2066 2066 data.name
2067 2067 ), '_blank');
2068 2068 },
2069 2069 error : utils.log_ajax_error,
2070 2070 };
2071 2071 var url = utils.url_join_encode(
2072 2072 base_url,
2073 'api/notebooks',
2073 'api/contents',
2074 2074 path
2075 2075 );
2076 2076 $.ajax(url,settings);
2077 2077 };
2078 2078
2079 2079 Notebook.prototype.rename = function (nbname) {
2080 2080 var that = this;
2081 2081 if (!nbname.match(/\.ipynb$/)) {
2082 2082 nbname = nbname + ".ipynb";
2083 2083 }
2084 2084 var data = {name: nbname};
2085 2085 var settings = {
2086 2086 processData : false,
2087 2087 cache : false,
2088 2088 type : "PATCH",
2089 2089 data : JSON.stringify(data),
2090 2090 dataType: "json",
2091 2091 headers : {'Content-Type': 'application/json'},
2092 2092 success : $.proxy(that.rename_success, this),
2093 2093 error : $.proxy(that.rename_error, this)
2094 2094 };
2095 2095 this.events.trigger('rename_notebook.Notebook', data);
2096 2096 var url = utils.url_join_encode(
2097 2097 this.base_url,
2098 'api/notebooks',
2098 'api/contents',
2099 2099 this.notebook_path,
2100 2100 this.notebook_name
2101 2101 );
2102 2102 $.ajax(url, settings);
2103 2103 };
2104 2104
2105 2105 Notebook.prototype.delete = function () {
2106 2106 var that = this;
2107 2107 var settings = {
2108 2108 processData : false,
2109 2109 cache : false,
2110 2110 type : "DELETE",
2111 2111 dataType: "json",
2112 2112 error : utils.log_ajax_error,
2113 2113 };
2114 2114 var url = utils.url_join_encode(
2115 2115 this.base_url,
2116 'api/notebooks',
2116 'api/contents',
2117 2117 this.notebook_path,
2118 2118 this.notebook_name
2119 2119 );
2120 2120 $.ajax(url, settings);
2121 2121 };
2122 2122
2123 2123
2124 2124 Notebook.prototype.rename_success = function (json, status, xhr) {
2125 2125 var name = this.notebook_name = json.name;
2126 2126 var path = json.path;
2127 2127 this.session.rename_notebook(name, path);
2128 2128 this.events.trigger('notebook_renamed.Notebook', json);
2129 2129 };
2130 2130
2131 2131 Notebook.prototype.rename_error = function (xhr, status, error) {
2132 2132 var that = this;
2133 2133 var dialog_body = $('<div/>').append(
2134 2134 $("<p/>").text('This notebook name already exists.')
2135 2135 );
2136 2136 this.events.trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
2137 2137 dialog.modal({
2138 2138 notebook: this,
2139 2139 keyboard_manager: this.keyboard_manager,
2140 2140 title: "Notebook Rename Error!",
2141 2141 body: dialog_body,
2142 2142 buttons : {
2143 2143 "Cancel": {},
2144 2144 "OK": {
2145 2145 class: "btn-primary",
2146 2146 click: function () {
2147 2147 this.save_widget.rename_notebook({notebook:that});
2148 2148 }}
2149 2149 },
2150 2150 open : function (event, ui) {
2151 2151 var that = $(this);
2152 2152 // Upon ENTER, click the OK button.
2153 2153 that.find('input[type="text"]').keydown(function (event, ui) {
2154 2154 if (event.which === this.keyboard.keycodes.enter) {
2155 2155 that.find('.btn-primary').first().click();
2156 2156 }
2157 2157 });
2158 2158 that.find('input[type="text"]').focus();
2159 2159 }
2160 2160 });
2161 2161 };
2162 2162
2163 2163 /**
2164 2164 * Request a notebook's data from the server.
2165 2165 *
2166 2166 * @method load_notebook
2167 2167 * @param {String} notebook_name and path A notebook to load
2168 2168 */
2169 2169 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
2170 2170 var that = this;
2171 2171 this.notebook_name = notebook_name;
2172 2172 this.notebook_path = notebook_path;
2173 2173 // We do the call with settings so we can set cache to false.
2174 2174 var settings = {
2175 2175 processData : false,
2176 2176 cache : false,
2177 2177 type : "GET",
2178 2178 dataType : "json",
2179 2179 success : $.proxy(this.load_notebook_success,this),
2180 2180 error : $.proxy(this.load_notebook_error,this),
2181 2181 };
2182 2182 this.events.trigger('notebook_loading.Notebook');
2183 2183 var url = utils.url_join_encode(
2184 2184 this.base_url,
2185 'api/notebooks',
2185 'api/contents',
2186 2186 this.notebook_path,
2187 2187 this.notebook_name
2188 2188 );
2189 2189 $.ajax(url, settings);
2190 2190 };
2191 2191
2192 2192 /**
2193 2193 * Success callback for loading a notebook from the server.
2194 2194 *
2195 2195 * Load notebook data from the JSON response.
2196 2196 *
2197 2197 * @method load_notebook_success
2198 2198 * @param {Object} data JSON representation of a notebook
2199 2199 * @param {String} status Description of response status
2200 2200 * @param {jqXHR} xhr jQuery Ajax object
2201 2201 */
2202 2202 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
2203 2203 this.fromJSON(data);
2204 2204 if (this.ncells() === 0) {
2205 2205 this.insert_cell_below('code');
2206 2206 this.edit_mode(0);
2207 2207 } else {
2208 2208 this.select(0);
2209 2209 this.handle_command_mode(this.get_cell(0));
2210 2210 }
2211 2211 this.set_dirty(false);
2212 2212 this.scroll_to_top();
2213 2213 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
2214 2214 var msg = "This notebook has been converted from an older " +
2215 2215 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
2216 2216 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
2217 2217 "newer notebook format will be used and older versions of IPython " +
2218 2218 "may not be able to read it. To keep the older version, close the " +
2219 2219 "notebook without saving it.";
2220 2220 dialog.modal({
2221 2221 notebook: this,
2222 2222 keyboard_manager: this.keyboard_manager,
2223 2223 title : "Notebook converted",
2224 2224 body : msg,
2225 2225 buttons : {
2226 2226 OK : {
2227 2227 class : "btn-primary"
2228 2228 }
2229 2229 }
2230 2230 });
2231 2231 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
2232 2232 var that = this;
2233 2233 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
2234 2234 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
2235 2235 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2236 2236 this_vs + ". You can still work with this notebook, but some features " +
2237 2237 "introduced in later notebook versions may not be available.";
2238 2238
2239 2239 dialog.modal({
2240 2240 notebook: this,
2241 2241 keyboard_manager: this.keyboard_manager,
2242 2242 title : "Newer Notebook",
2243 2243 body : msg,
2244 2244 buttons : {
2245 2245 OK : {
2246 2246 class : "btn-danger"
2247 2247 }
2248 2248 }
2249 2249 });
2250 2250
2251 2251 }
2252 2252
2253 2253 // Create the session after the notebook is completely loaded to prevent
2254 2254 // code execution upon loading, which is a security risk.
2255 2255 if (this.session === null) {
2256 2256 var kernelspec = this.metadata.kernelspec || {};
2257 2257 var kernel_name = kernelspec.name || this.default_kernel_name;
2258 2258
2259 2259 this.start_session(kernel_name);
2260 2260 }
2261 2261 // load our checkpoint list
2262 2262 this.list_checkpoints();
2263 2263
2264 2264 // load toolbar state
2265 2265 if (this.metadata.celltoolbar) {
2266 2266 celltoolbar.CellToolbar.global_show();
2267 2267 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2268 2268 } else {
2269 2269 celltoolbar.CellToolbar.global_hide();
2270 2270 }
2271 2271
2272 2272 // now that we're fully loaded, it is safe to restore save functionality
2273 2273 delete(this.save_notebook);
2274 2274 this.events.trigger('notebook_loaded.Notebook');
2275 2275 };
2276 2276
2277 2277 /**
2278 2278 * Failure callback for loading a notebook from the server.
2279 2279 *
2280 2280 * @method load_notebook_error
2281 2281 * @param {jqXHR} xhr jQuery Ajax object
2282 2282 * @param {String} status Description of response status
2283 2283 * @param {String} error HTTP error message
2284 2284 */
2285 2285 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
2286 2286 this.events.trigger('notebook_load_failed.Notebook', [xhr, status, error]);
2287 2287 var msg;
2288 2288 if (xhr.status === 400) {
2289 2289 msg = error;
2290 2290 } else if (xhr.status === 500) {
2291 2291 msg = "An unknown error occurred while loading this notebook. " +
2292 2292 "This version can load notebook formats " +
2293 2293 "v" + this.nbformat + " or earlier.";
2294 2294 }
2295 2295 dialog.modal({
2296 2296 notebook: this,
2297 2297 keyboard_manager: this.keyboard_manager,
2298 2298 title: "Error loading notebook",
2299 2299 body : msg,
2300 2300 buttons : {
2301 2301 "OK": {}
2302 2302 }
2303 2303 });
2304 2304 };
2305 2305
2306 2306 /********************* checkpoint-related *********************/
2307 2307
2308 2308 /**
2309 2309 * Save the notebook then immediately create a checkpoint.
2310 2310 *
2311 2311 * @method save_checkpoint
2312 2312 */
2313 2313 Notebook.prototype.save_checkpoint = function () {
2314 2314 this._checkpoint_after_save = true;
2315 2315 this.save_notebook();
2316 2316 };
2317 2317
2318 2318 /**
2319 2319 * Add a checkpoint for this notebook.
2320 2320 * for use as a callback from checkpoint creation.
2321 2321 *
2322 2322 * @method add_checkpoint
2323 2323 */
2324 2324 Notebook.prototype.add_checkpoint = function (checkpoint) {
2325 2325 var found = false;
2326 2326 for (var i = 0; i < this.checkpoints.length; i++) {
2327 2327 var existing = this.checkpoints[i];
2328 2328 if (existing.id == checkpoint.id) {
2329 2329 found = true;
2330 2330 this.checkpoints[i] = checkpoint;
2331 2331 break;
2332 2332 }
2333 2333 }
2334 2334 if (!found) {
2335 2335 this.checkpoints.push(checkpoint);
2336 2336 }
2337 2337 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2338 2338 };
2339 2339
2340 2340 /**
2341 2341 * List checkpoints for this notebook.
2342 2342 *
2343 2343 * @method list_checkpoints
2344 2344 */
2345 2345 Notebook.prototype.list_checkpoints = function () {
2346 2346 var url = utils.url_join_encode(
2347 2347 this.base_url,
2348 'api/notebooks',
2348 'api/contents',
2349 2349 this.notebook_path,
2350 2350 this.notebook_name,
2351 2351 'checkpoints'
2352 2352 );
2353 2353 $.get(url).done(
2354 2354 $.proxy(this.list_checkpoints_success, this)
2355 2355 ).fail(
2356 2356 $.proxy(this.list_checkpoints_error, this)
2357 2357 );
2358 2358 };
2359 2359
2360 2360 /**
2361 2361 * Success callback for listing checkpoints.
2362 2362 *
2363 2363 * @method list_checkpoint_success
2364 2364 * @param {Object} data JSON representation of a checkpoint
2365 2365 * @param {String} status Description of response status
2366 2366 * @param {jqXHR} xhr jQuery Ajax object
2367 2367 */
2368 2368 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2369 2369 data = $.parseJSON(data);
2370 2370 this.checkpoints = data;
2371 2371 if (data.length) {
2372 2372 this.last_checkpoint = data[data.length - 1];
2373 2373 } else {
2374 2374 this.last_checkpoint = null;
2375 2375 }
2376 2376 this.events.trigger('checkpoints_listed.Notebook', [data]);
2377 2377 };
2378 2378
2379 2379 /**
2380 2380 * Failure callback for listing a checkpoint.
2381 2381 *
2382 2382 * @method list_checkpoint_error
2383 2383 * @param {jqXHR} xhr jQuery Ajax object
2384 2384 * @param {String} status Description of response status
2385 2385 * @param {String} error_msg HTTP error message
2386 2386 */
2387 2387 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2388 2388 this.events.trigger('list_checkpoints_failed.Notebook');
2389 2389 };
2390 2390
2391 2391 /**
2392 2392 * Create a checkpoint of this notebook on the server from the most recent save.
2393 2393 *
2394 2394 * @method create_checkpoint
2395 2395 */
2396 2396 Notebook.prototype.create_checkpoint = function () {
2397 2397 var url = utils.url_join_encode(
2398 2398 this.base_url,
2399 'api/notebooks',
2399 'api/contents',
2400 2400 this.notebook_path,
2401 2401 this.notebook_name,
2402 2402 'checkpoints'
2403 2403 );
2404 2404 $.post(url).done(
2405 2405 $.proxy(this.create_checkpoint_success, this)
2406 2406 ).fail(
2407 2407 $.proxy(this.create_checkpoint_error, this)
2408 2408 );
2409 2409 };
2410 2410
2411 2411 /**
2412 2412 * Success callback for creating a checkpoint.
2413 2413 *
2414 2414 * @method create_checkpoint_success
2415 2415 * @param {Object} data JSON representation of a checkpoint
2416 2416 * @param {String} status Description of response status
2417 2417 * @param {jqXHR} xhr jQuery Ajax object
2418 2418 */
2419 2419 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2420 2420 data = $.parseJSON(data);
2421 2421 this.add_checkpoint(data);
2422 2422 this.events.trigger('checkpoint_created.Notebook', data);
2423 2423 };
2424 2424
2425 2425 /**
2426 2426 * Failure callback for creating a checkpoint.
2427 2427 *
2428 2428 * @method create_checkpoint_error
2429 2429 * @param {jqXHR} xhr jQuery Ajax object
2430 2430 * @param {String} status Description of response status
2431 2431 * @param {String} error_msg HTTP error message
2432 2432 */
2433 2433 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2434 2434 this.events.trigger('checkpoint_failed.Notebook');
2435 2435 };
2436 2436
2437 2437 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2438 2438 var that = this;
2439 2439 checkpoint = checkpoint || this.last_checkpoint;
2440 2440 if ( ! checkpoint ) {
2441 2441 console.log("restore dialog, but no checkpoint to restore to!");
2442 2442 return;
2443 2443 }
2444 2444 var body = $('<div/>').append(
2445 2445 $('<p/>').addClass("p-space").text(
2446 2446 "Are you sure you want to revert the notebook to " +
2447 2447 "the latest checkpoint?"
2448 2448 ).append(
2449 2449 $("<strong/>").text(
2450 2450 " This cannot be undone."
2451 2451 )
2452 2452 )
2453 2453 ).append(
2454 2454 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2455 2455 ).append(
2456 2456 $('<p/>').addClass("p-space").text(
2457 2457 Date(checkpoint.last_modified)
2458 2458 ).css("text-align", "center")
2459 2459 );
2460 2460
2461 2461 dialog.modal({
2462 2462 notebook: this,
2463 2463 keyboard_manager: this.keyboard_manager,
2464 2464 title : "Revert notebook to checkpoint",
2465 2465 body : body,
2466 2466 buttons : {
2467 2467 Revert : {
2468 2468 class : "btn-danger",
2469 2469 click : function () {
2470 2470 that.restore_checkpoint(checkpoint.id);
2471 2471 }
2472 2472 },
2473 2473 Cancel : {}
2474 2474 }
2475 2475 });
2476 2476 };
2477 2477
2478 2478 /**
2479 2479 * Restore the notebook to a checkpoint state.
2480 2480 *
2481 2481 * @method restore_checkpoint
2482 2482 * @param {String} checkpoint ID
2483 2483 */
2484 2484 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2485 2485 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2486 2486 var url = utils.url_join_encode(
2487 2487 this.base_url,
2488 'api/notebooks',
2488 'api/contents',
2489 2489 this.notebook_path,
2490 2490 this.notebook_name,
2491 2491 'checkpoints',
2492 2492 checkpoint
2493 2493 );
2494 2494 $.post(url).done(
2495 2495 $.proxy(this.restore_checkpoint_success, this)
2496 2496 ).fail(
2497 2497 $.proxy(this.restore_checkpoint_error, this)
2498 2498 );
2499 2499 };
2500 2500
2501 2501 /**
2502 2502 * Success callback for restoring a notebook to a checkpoint.
2503 2503 *
2504 2504 * @method restore_checkpoint_success
2505 2505 * @param {Object} data (ignored, should be empty)
2506 2506 * @param {String} status Description of response status
2507 2507 * @param {jqXHR} xhr jQuery Ajax object
2508 2508 */
2509 2509 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2510 2510 this.events.trigger('checkpoint_restored.Notebook');
2511 2511 this.load_notebook(this.notebook_name, this.notebook_path);
2512 2512 };
2513 2513
2514 2514 /**
2515 2515 * Failure callback for restoring a notebook to a checkpoint.
2516 2516 *
2517 2517 * @method restore_checkpoint_error
2518 2518 * @param {jqXHR} xhr jQuery Ajax object
2519 2519 * @param {String} status Description of response status
2520 2520 * @param {String} error_msg HTTP error message
2521 2521 */
2522 2522 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2523 2523 this.events.trigger('checkpoint_restore_failed.Notebook');
2524 2524 };
2525 2525
2526 2526 /**
2527 2527 * Delete a notebook checkpoint.
2528 2528 *
2529 2529 * @method delete_checkpoint
2530 2530 * @param {String} checkpoint ID
2531 2531 */
2532 2532 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2533 2533 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2534 2534 var url = utils.url_join_encode(
2535 2535 this.base_url,
2536 'api/notebooks',
2536 'api/contents',
2537 2537 this.notebook_path,
2538 2538 this.notebook_name,
2539 2539 'checkpoints',
2540 2540 checkpoint
2541 2541 );
2542 2542 $.ajax(url, {
2543 2543 type: 'DELETE',
2544 2544 success: $.proxy(this.delete_checkpoint_success, this),
2545 2545 error: $.proxy(this.delete_checkpoint_error, this)
2546 2546 });
2547 2547 };
2548 2548
2549 2549 /**
2550 2550 * Success callback for deleting a notebook checkpoint
2551 2551 *
2552 2552 * @method delete_checkpoint_success
2553 2553 * @param {Object} data (ignored, should be empty)
2554 2554 * @param {String} status Description of response status
2555 2555 * @param {jqXHR} xhr jQuery Ajax object
2556 2556 */
2557 2557 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2558 2558 this.events.trigger('checkpoint_deleted.Notebook', data);
2559 2559 this.load_notebook(this.notebook_name, this.notebook_path);
2560 2560 };
2561 2561
2562 2562 /**
2563 2563 * Failure callback for deleting a notebook checkpoint.
2564 2564 *
2565 2565 * @method delete_checkpoint_error
2566 2566 * @param {jqXHR} xhr jQuery Ajax object
2567 2567 * @param {String} status Description of response status
2568 2568 * @param {String} error_msg HTTP error message
2569 2569 */
2570 2570 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2571 2571 this.events.trigger('checkpoint_delete_failed.Notebook');
2572 2572 };
2573 2573
2574 2574
2575 2575 // For backwards compatability.
2576 2576 IPython.Notebook = Notebook;
2577 2577
2578 2578 return {'Notebook': Notebook};
2579 2579 });
@@ -1,448 +1,448 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'base/js/dialog',
9 9 ], function(IPython, $, utils, dialog) {
10 10 "use strict";
11 11
12 12 var NotebookList = function (selector, options) {
13 13 // Constructor
14 14 //
15 15 // Parameters:
16 16 // selector: string
17 17 // options: dictionary
18 18 // Dictionary of keyword arguments.
19 19 // session_list: SessionList instance
20 20 // element_name: string
21 21 // base_url: string
22 22 // notebook_path: string
23 23 var that = this;
24 24 this.session_list = options.session_list;
25 25 // allow code re-use by just changing element_name in kernellist.js
26 26 this.element_name = options.element_name || 'notebook';
27 27 this.selector = selector;
28 28 if (this.selector !== undefined) {
29 29 this.element = $(selector);
30 30 this.style();
31 31 this.bind_events();
32 32 }
33 33 this.notebooks_list = [];
34 34 this.sessions = {};
35 35 this.base_url = options.base_url || utils.get_body_data("baseUrl");
36 36 this.notebook_path = options.notebook_path || utils.get_body_data("notebookPath");
37 37 if (this.session_list && this.session_list.events) {
38 38 this.session_list.events.on('sessions_loaded.Dashboard',
39 39 function(e, d) { that.sessions_loaded(d); });
40 40 }
41 41 };
42 42
43 43 NotebookList.prototype.style = function () {
44 44 var prefix = '#' + this.element_name;
45 45 $(prefix + '_toolbar').addClass('list_toolbar');
46 46 $(prefix + '_list_info').addClass('toolbar_info');
47 47 $(prefix + '_buttons').addClass('toolbar_buttons');
48 48 $(prefix + '_list_header').addClass('list_header');
49 49 this.element.addClass("list_container");
50 50 };
51 51
52 52
53 53 NotebookList.prototype.bind_events = function () {
54 54 var that = this;
55 55 $('#refresh_' + this.element_name + '_list').click(function () {
56 56 that.load_sessions();
57 57 });
58 58 this.element.bind('dragover', function () {
59 59 return false;
60 60 });
61 61 this.element.bind('drop', function(event){
62 62 that.handleFilesUpload(event,'drop');
63 63 return false;
64 64 });
65 65 };
66 66
67 67 NotebookList.prototype.handleFilesUpload = function(event, dropOrForm) {
68 68 var that = this;
69 69 var files;
70 70 if(dropOrForm =='drop'){
71 71 files = event.originalEvent.dataTransfer.files;
72 72 } else
73 73 {
74 74 files = event.originalEvent.target.files;
75 75 }
76 76 for (var i = 0; i < files.length; i++) {
77 77 var f = files[i];
78 78 var reader = new FileReader();
79 79 reader.readAsText(f);
80 80 var name_and_ext = utils.splitext(f.name);
81 81 var file_ext = name_and_ext[1];
82 82 if (file_ext === '.ipynb') {
83 83 var item = that.new_notebook_item(0);
84 84 item.addClass('new-file');
85 85 that.add_name_input(f.name, item);
86 86 // Store the notebook item in the reader so we can use it later
87 87 // to know which item it belongs to.
88 88 $(reader).data('item', item);
89 89 reader.onload = function (event) {
90 90 var nbitem = $(event.target).data('item');
91 91 that.add_notebook_data(event.target.result, nbitem);
92 92 that.add_upload_button(nbitem);
93 93 };
94 94 } else {
95 95 var dialog_body = 'Uploaded notebooks must be .ipynb files';
96 96 dialog.modal({
97 97 title : 'Invalid file type',
98 98 body : dialog_body,
99 99 buttons : {'OK' : {'class' : 'btn-primary'}}
100 100 });
101 101 }
102 102 }
103 103 // Replace the file input form wth a clone of itself. This is required to
104 104 // reset the form. Otherwise, if you upload a file, delete it and try to
105 105 // upload it again, the changed event won't fire.
106 106 var form = $('input.fileinput');
107 107 form.replaceWith(form.clone(true));
108 108 return false;
109 109 };
110 110
111 111 NotebookList.prototype.clear_list = function (remove_uploads) {
112 112 // Clears the navigation tree.
113 113 //
114 114 // Parameters
115 115 // remove_uploads: bool=False
116 116 // Should upload prompts also be removed from the tree.
117 117 if (remove_uploads) {
118 118 this.element.children('.list_item').remove();
119 119 } else {
120 120 this.element.children('.list_item:not(.new-file)').remove();
121 121 }
122 122 };
123 123
124 124 NotebookList.prototype.load_sessions = function(){
125 125 this.session_list.load_sessions();
126 126 };
127 127
128 128
129 129 NotebookList.prototype.sessions_loaded = function(data){
130 130 this.sessions = data;
131 131 this.load_list();
132 132 };
133 133
134 134 NotebookList.prototype.load_list = function () {
135 135 var that = this;
136 136 var settings = {
137 137 processData : false,
138 138 cache : false,
139 139 type : "GET",
140 140 dataType : "json",
141 141 success : $.proxy(this.list_loaded, this),
142 142 error : $.proxy( function(xhr, status, error){
143 143 utils.log_ajax_error(xhr, status, error);
144 144 that.list_loaded([], null, null, {msg:"Error connecting to server."});
145 145 },this)
146 146 };
147 147
148 148 var url = utils.url_join_encode(
149 149 this.base_url,
150 150 'api',
151 'notebooks',
151 'contents',
152 152 this.notebook_path
153 153 );
154 154 $.ajax(url, settings);
155 155 };
156 156
157 157
158 158 NotebookList.prototype.list_loaded = function (data, status, xhr, param) {
159 159 var message = 'Notebook list empty.';
160 160 if (param !== undefined && param.msg) {
161 161 message = param.msg;
162 162 }
163 163 var item = null;
164 164 var len = data.length;
165 165 this.clear_list();
166 166 if (len === 0) {
167 167 item = this.new_notebook_item(0);
168 168 var span12 = item.children().first();
169 169 span12.empty();
170 170 span12.append($('<div style="margin:auto;text-align:center;color:grey"/>').text(message));
171 171 }
172 172 var path = this.notebook_path;
173 173 var offset = 0;
174 174 if (path !== '') {
175 175 item = this.new_notebook_item(0);
176 176 this.add_dir(path, '..', item);
177 177 offset = 1;
178 178 }
179 179 for (var i=0; i<len; i++) {
180 180 if (data[i].type === 'directory') {
181 181 var name = data[i].name;
182 182 item = this.new_notebook_item(i+offset);
183 183 this.add_dir(path, name, item);
184 184 } else {
185 185 var name = data[i].name;
186 186 item = this.new_notebook_item(i+offset);
187 187 this.add_link(path, name, item);
188 188 name = utils.url_path_join(path, name);
189 189 if(this.sessions[name] === undefined){
190 190 this.add_delete_button(item);
191 191 } else {
192 192 this.add_shutdown_button(item,this.sessions[name]);
193 193 }
194 194 }
195 195 }
196 196 };
197 197
198 198
199 199 NotebookList.prototype.new_notebook_item = function (index) {
200 200 var item = $('<div/>').addClass("list_item").addClass("row");
201 201 // item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
202 202 // item.css('border-top-style','none');
203 203 item.append($("<div/>").addClass("col-md-12").append(
204 204 $('<i/>').addClass('item_icon')
205 205 ).append(
206 206 $("<a/>").addClass("item_link").append(
207 207 $("<span/>").addClass("item_name")
208 208 )
209 209 ).append(
210 210 $('<div/>').addClass("item_buttons btn-group pull-right")
211 211 ));
212 212
213 213 if (index === -1) {
214 214 this.element.append(item);
215 215 } else {
216 216 this.element.children().eq(index).after(item);
217 217 }
218 218 return item;
219 219 };
220 220
221 221
222 222 NotebookList.prototype.add_dir = function (path, name, item) {
223 223 item.data('name', name);
224 224 item.data('path', path);
225 225 item.find(".item_name").text(name);
226 226 item.find(".item_icon").addClass('folder_icon').addClass('icon-fixed-width');
227 227 item.find("a.item_link")
228 228 .attr('href',
229 229 utils.url_join_encode(
230 230 this.base_url,
231 231 "tree",
232 232 path,
233 233 name
234 234 )
235 235 );
236 236 };
237 237
238 238
239 239 NotebookList.prototype.add_link = function (path, nbname, item) {
240 240 item.data('nbname', nbname);
241 241 item.data('path', path);
242 242 item.find(".item_name").text(nbname);
243 243 item.find(".item_icon").addClass('notebook_icon').addClass('icon-fixed-width');
244 244 item.find("a.item_link")
245 245 .attr('href',
246 246 utils.url_join_encode(
247 247 this.base_url,
248 248 "notebooks",
249 249 path,
250 250 nbname
251 251 )
252 252 ).attr('target','_blank');
253 253 };
254 254
255 255
256 256 NotebookList.prototype.add_name_input = function (nbname, item) {
257 257 item.data('nbname', nbname);
258 258 item.find(".item_icon").addClass('notebook_icon').addClass('icon-fixed-width');
259 259 item.find(".item_name").empty().append(
260 260 $('<input/>')
261 261 .addClass("nbname_input")
262 262 .attr('value', utils.splitext(nbname)[0])
263 263 .attr('size', '30')
264 264 .attr('type', 'text')
265 265 );
266 266 };
267 267
268 268
269 269 NotebookList.prototype.add_notebook_data = function (data, item) {
270 270 item.data('nbdata', data);
271 271 };
272 272
273 273
274 274 NotebookList.prototype.add_shutdown_button = function (item, session) {
275 275 var that = this;
276 276 var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-xs btn-danger").
277 277 click(function (e) {
278 278 var settings = {
279 279 processData : false,
280 280 cache : false,
281 281 type : "DELETE",
282 282 dataType : "json",
283 283 success : function () {
284 284 that.load_sessions();
285 285 },
286 286 error : utils.log_ajax_error,
287 287 };
288 288 var url = utils.url_join_encode(
289 289 that.base_url,
290 290 'api/sessions',
291 291 session
292 292 );
293 293 $.ajax(url, settings);
294 294 return false;
295 295 });
296 296 // var new_buttons = item.find('a'); // shutdown_button;
297 297 item.find(".item_buttons").text("").append(shutdown_button);
298 298 };
299 299
300 300 NotebookList.prototype.add_delete_button = function (item) {
301 301 var new_buttons = $('<span/>').addClass("btn-group pull-right");
302 302 var notebooklist = this;
303 303 var delete_button = $("<button/>").text("Delete").addClass("btn btn-default btn-xs").
304 304 click(function (e) {
305 305 // $(this) is the button that was clicked.
306 306 var that = $(this);
307 307 // We use the nbname and notebook_id from the parent notebook_item element's
308 308 // data because the outer scopes values change as we iterate through the loop.
309 309 var parent_item = that.parents('div.list_item');
310 310 var nbname = parent_item.data('nbname');
311 311 var message = 'Are you sure you want to permanently delete the notebook: ' + nbname + '?';
312 312 dialog.modal({
313 313 title : "Delete notebook",
314 314 body : message,
315 315 buttons : {
316 316 Delete : {
317 317 class: "btn-danger",
318 318 click: function() {
319 319 var settings = {
320 320 processData : false,
321 321 cache : false,
322 322 type : "DELETE",
323 323 dataType : "json",
324 324 success : function (data, status, xhr) {
325 325 parent_item.remove();
326 326 },
327 327 error : utils.log_ajax_error,
328 328 };
329 329 var url = utils.url_join_encode(
330 330 notebooklist.base_url,
331 'api/notebooks',
331 'api/contents',
332 332 notebooklist.notebook_path,
333 333 nbname
334 334 );
335 335 $.ajax(url, settings);
336 336 }
337 337 },
338 338 Cancel : {}
339 339 }
340 340 });
341 341 return false;
342 342 });
343 343 item.find(".item_buttons").text("").append(delete_button);
344 344 };
345 345
346 346
347 347 NotebookList.prototype.add_upload_button = function (item) {
348 348 var that = this;
349 349 var upload_button = $('<button/>').text("Upload")
350 350 .addClass('btn btn-primary btn-xs upload_button')
351 351 .click(function (e) {
352 352 var nbname = item.find('.item_name > input').val();
353 353 if (nbname.slice(nbname.length-6, nbname.length) != ".ipynb") {
354 354 nbname = nbname + ".ipynb";
355 355 }
356 356 var path = that.notebook_path;
357 357 var nbdata = item.data('nbdata');
358 358 var content_type = 'application/json';
359 359 var model = {
360 360 content : JSON.parse(nbdata),
361 361 };
362 362 var settings = {
363 363 processData : false,
364 364 cache : false,
365 365 type : 'PUT',
366 366 dataType : 'json',
367 367 data : JSON.stringify(model),
368 368 headers : {'Content-Type': content_type},
369 369 success : function (data, status, xhr) {
370 370 that.add_link(path, nbname, item);
371 371 that.add_delete_button(item);
372 372 },
373 373 error : utils.log_ajax_error,
374 374 };
375 375
376 376 var url = utils.url_join_encode(
377 377 that.base_url,
378 'api/notebooks',
378 'api/contents',
379 379 that.notebook_path,
380 380 nbname
381 381 );
382 382 $.ajax(url, settings);
383 383 return false;
384 384 });
385 385 var cancel_button = $('<button/>').text("Cancel")
386 386 .addClass("btn btn-default btn-xs")
387 387 .click(function (e) {
388 388 console.log('cancel click');
389 389 item.remove();
390 390 return false;
391 391 });
392 392 item.find(".item_buttons").empty()
393 393 .append(upload_button)
394 394 .append(cancel_button);
395 395 };
396 396
397 397
398 398 NotebookList.prototype.new_notebook = function(){
399 399 var path = this.notebook_path;
400 400 var base_url = this.base_url;
401 401 var settings = {
402 402 processData : false,
403 403 cache : false,
404 404 type : "POST",
405 405 dataType : "json",
406 406 async : false,
407 407 success : function (data, status, xhr) {
408 408 var notebook_name = data.name;
409 409 window.open(
410 410 utils.url_join_encode(
411 411 base_url,
412 412 'notebooks',
413 413 path,
414 414 notebook_name),
415 415 '_blank'
416 416 );
417 417 },
418 418 error : $.proxy(this.new_notebook_failed, this),
419 419 };
420 420 var url = utils.url_join_encode(
421 421 base_url,
422 'api/notebooks',
422 'api/contents',
423 423 path
424 424 );
425 425 $.ajax(url, settings);
426 426 };
427 427
428 428
429 429 NotebookList.prototype.new_notebook_failed = function (xhr, status, error) {
430 430 utils.log_ajax_error(xhr, status, error);
431 431 var msg;
432 432 if (xhr.responseJSON && xhr.responseJSON.message) {
433 433 msg = xhr.responseJSON.message;
434 434 } else {
435 435 msg = xhr.statusText;
436 436 }
437 437 dialog.modal({
438 438 title : 'Creating Notebook Failed',
439 439 body : "The error was: " + msg,
440 440 buttons : {'OK' : {'class' : 'btn-primary'}}
441 441 });
442 442 };
443 443
444 444 // Backwards compatability.
445 445 IPython.NotebookList = NotebookList;
446 446
447 447 return {'NotebookList': NotebookList};
448 448 });
@@ -1,102 +1,102 b''
1 1 """Base class for notebook tests."""
2 2
3 3 from __future__ import print_function
4 4
5 5 import sys
6 6 import time
7 7 import requests
8 8 from contextlib import contextmanager
9 9 from subprocess import Popen, STDOUT
10 10 from unittest import TestCase
11 11
12 12 import nose
13 13
14 14 from IPython.utils.tempdir import TemporaryDirectory
15 15
16 16 MAX_WAITTIME = 30 # seconds to wait for notebook server to start
17 17 POLL_INTERVAL = 0.1 # time between attempts
18 18
19 19 # TimeoutError is a builtin on Python 3. This can be removed when we stop
20 20 # supporting Python 2.
21 21 class TimeoutError(Exception):
22 22 pass
23 23
24 24 class NotebookTestBase(TestCase):
25 25 """A base class for tests that need a running notebook.
26 26
27 27 This creates an empty profile in a temp ipython_dir
28 28 and then starts the notebook server with a separate temp notebook_dir.
29 29 """
30 30
31 31 port = 12341
32 32
33 33 @classmethod
34 34 def wait_until_alive(cls):
35 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 37 for _ in range(int(MAX_WAITTIME/POLL_INTERVAL)):
38 38 try:
39 39 requests.get(url)
40 40 except requests.exceptions.ConnectionError:
41 41 if cls.notebook.poll() is not None:
42 42 raise RuntimeError("The notebook server exited with status %s" \
43 43 % cls.notebook.poll())
44 44 time.sleep(POLL_INTERVAL)
45 45 else:
46 46 return
47 47
48 48 raise TimeoutError("The notebook server didn't start up correctly.")
49 49
50 50 @classmethod
51 51 def wait_until_dead(cls):
52 52 """Wait for the server process to terminate after shutdown"""
53 53 for _ in range(int(MAX_WAITTIME/POLL_INTERVAL)):
54 54 if cls.notebook.poll() is not None:
55 55 return
56 56 time.sleep(POLL_INTERVAL)
57 57
58 58 raise TimeoutError("Undead notebook server")
59 59
60 60 @classmethod
61 61 def setup_class(cls):
62 62 cls.ipython_dir = TemporaryDirectory()
63 63 cls.notebook_dir = TemporaryDirectory()
64 64 notebook_args = [
65 65 sys.executable, '-c',
66 66 'from IPython.html.notebookapp import launch_new_instance; launch_new_instance()',
67 67 '--port=%d' % cls.port,
68 68 '--port-retries=0', # Don't try any other ports
69 69 '--no-browser',
70 70 '--ipython-dir=%s' % cls.ipython_dir.name,
71 71 '--notebook-dir=%s' % cls.notebook_dir.name,
72 72 ]
73 73 cls.notebook = Popen(notebook_args,
74 74 stdout=nose.iptest_stdstreams_fileno(),
75 75 stderr=STDOUT,
76 76 )
77 77 cls.wait_until_alive()
78 78
79 79 @classmethod
80 80 def teardown_class(cls):
81 81 cls.notebook.terminate()
82 82 cls.wait_until_dead()
83 83 cls.ipython_dir.cleanup()
84 84 cls.notebook_dir.cleanup()
85 85
86 86 @classmethod
87 87 def base_url(cls):
88 88 return 'http://localhost:%i/' % cls.port
89 89
90 90
91 91 @contextmanager
92 92 def assert_http_error(status, msg=None):
93 93 try:
94 94 yield
95 95 except requests.HTTPError as e:
96 96 real_status = e.response.status_code
97 97 assert real_status == status, \
98 98 "Expected status %d, got %d" % (real_status, status)
99 99 if msg:
100 100 assert msg in str(e), e
101 101 else:
102 102 assert False, "Expected HTTP error status" No newline at end of file
@@ -1,101 +1,101 b''
1 1 """Tornado handlers for the tree view.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18 from tornado import web
19 19 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
20 20 from ..utils import url_path_join, url_escape
21 21
22 22 #-----------------------------------------------------------------------------
23 23 # Handlers
24 24 #-----------------------------------------------------------------------------
25 25
26 26
27 27 class TreeHandler(IPythonHandler):
28 28 """Render the tree view, listing notebooks, clusters, etc."""
29 29
30 30 def generate_breadcrumbs(self, path):
31 31 breadcrumbs = [(url_escape(url_path_join(self.base_url, 'tree')), '')]
32 32 comps = path.split('/')
33 33 ncomps = len(comps)
34 34 for i in range(ncomps):
35 35 if comps[i]:
36 36 link = url_escape(url_path_join(self.base_url, 'tree', *comps[0:i+1]))
37 37 breadcrumbs.append((link, comps[i]))
38 38 return breadcrumbs
39 39
40 40 def generate_page_title(self, path):
41 41 comps = path.split('/')
42 42 if len(comps) > 3:
43 43 for i in range(len(comps)-2):
44 44 comps.pop(0)
45 45 page_title = url_path_join(*comps)
46 46 if page_title:
47 47 return page_title+'/'
48 48 else:
49 49 return 'Home'
50 50
51 51 @web.authenticated
52 52 def get(self, path='', name=None):
53 53 path = path.strip('/')
54 nbm = self.notebook_manager
54 cm = self.contents_manager
55 55 if name is not None:
56 56 # is a notebook, redirect to notebook handler
57 57 url = url_escape(url_path_join(
58 58 self.base_url, 'notebooks', path, name
59 59 ))
60 60 self.log.debug("Redirecting %s to %s", self.request.path, url)
61 61 self.redirect(url)
62 62 else:
63 if not nbm.path_exists(path=path):
63 if not cm.path_exists(path=path):
64 64 # Directory is hidden or does not exist.
65 65 raise web.HTTPError(404)
66 elif nbm.is_hidden(path):
66 elif cm.is_hidden(path):
67 67 self.log.info("Refusing to serve hidden directory, via 404 Error")
68 68 raise web.HTTPError(404)
69 69 breadcrumbs = self.generate_breadcrumbs(path)
70 70 page_title = self.generate_page_title(path)
71 71 self.write(self.render_template('tree.html',
72 72 project=self.project_dir,
73 73 page_title=page_title,
74 74 notebook_path=path,
75 75 breadcrumbs=breadcrumbs
76 76 ))
77 77
78 78
79 79 class TreeRedirectHandler(IPythonHandler):
80 80 """Redirect a request to the corresponding tree URL"""
81 81
82 82 @web.authenticated
83 83 def get(self, path=''):
84 84 url = url_escape(url_path_join(
85 85 self.base_url, 'tree', path.strip('/')
86 86 ))
87 87 self.log.debug("Redirecting %s to %s", self.request.path, url)
88 88 self.redirect(url)
89 89
90 90
91 91 #-----------------------------------------------------------------------------
92 92 # URL to handler mappings
93 93 #-----------------------------------------------------------------------------
94 94
95 95
96 96 default_handlers = [
97 97 (r"/tree%s" % notebook_path_regex, TreeHandler),
98 98 (r"/tree%s" % path_regex, TreeHandler),
99 99 (r"/tree", TreeHandler),
100 100 (r"", TreeRedirectHandler),
101 101 ]
General Comments 0
You need to be logged in to leave comments. Login now