##// END OF EJS Templates
update html/js to nbformat 4
MinRK -
Show More
@@ -1,147 +1,148 b''
1 """Tornado handlers for nbconvert."""
1 """Tornado handlers for nbconvert."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import io
6 import io
7 import os
7 import os
8 import zipfile
8 import zipfile
9
9
10 from tornado import web
10 from tornado import web
11
11
12 from ..base.handlers import (
12 from ..base.handlers import (
13 IPythonHandler, FilesRedirectHandler,
13 IPythonHandler, FilesRedirectHandler,
14 notebook_path_regex, path_regex,
14 notebook_path_regex, path_regex,
15 )
15 )
16 from IPython.nbformat.current import to_notebook_json
16 from IPython.nbformat.current import to_notebook_json
17
17
18 from IPython.utils.py3compat import cast_bytes
18 from IPython.utils.py3compat import cast_bytes
19
19
20 def find_resource_files(output_files_dir):
20 def find_resource_files(output_files_dir):
21 files = []
21 files = []
22 for dirpath, dirnames, filenames in os.walk(output_files_dir):
22 for dirpath, dirnames, filenames in os.walk(output_files_dir):
23 files.extend([os.path.join(dirpath, f) for f in filenames])
23 files.extend([os.path.join(dirpath, f) for f in filenames])
24 return files
24 return files
25
25
26 def respond_zip(handler, name, output, resources):
26 def respond_zip(handler, name, output, resources):
27 """Zip up the output and resource files and respond with the zip file.
27 """Zip up the output and resource files and respond with the zip file.
28
28
29 Returns True if it has served a zip file, False if there are no resource
29 Returns True if it has served a zip file, False if there are no resource
30 files, in which case we serve the plain output file.
30 files, in which case we serve the plain output file.
31 """
31 """
32 # Check if we have resource files we need to zip
32 # Check if we have resource files we need to zip
33 output_files = resources.get('outputs', None)
33 output_files = resources.get('outputs', None)
34 if not output_files:
34 if not output_files:
35 return False
35 return False
36
36
37 # Headers
37 # Headers
38 zip_filename = os.path.splitext(name)[0] + '.zip'
38 zip_filename = os.path.splitext(name)[0] + '.zip'
39 handler.set_header('Content-Disposition',
39 handler.set_header('Content-Disposition',
40 'attachment; filename="%s"' % zip_filename)
40 'attachment; filename="%s"' % zip_filename)
41 handler.set_header('Content-Type', 'application/zip')
41 handler.set_header('Content-Type', 'application/zip')
42
42
43 # Prepare the zip file
43 # Prepare the zip file
44 buffer = io.BytesIO()
44 buffer = io.BytesIO()
45 zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
45 zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
46 output_filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
46 output_filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
47 zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
47 zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
48 for filename, data in output_files.items():
48 for filename, data in output_files.items():
49 zipf.writestr(os.path.basename(filename), data)
49 zipf.writestr(os.path.basename(filename), data)
50 zipf.close()
50 zipf.close()
51
51
52 handler.finish(buffer.getvalue())
52 handler.finish(buffer.getvalue())
53 return True
53 return True
54
54
55 def get_exporter(format, **kwargs):
55 def get_exporter(format, **kwargs):
56 """get an exporter, raising appropriate errors"""
56 """get an exporter, raising appropriate errors"""
57 # if this fails, will raise 500
57 # if this fails, will raise 500
58 try:
58 try:
59 from IPython.nbconvert.exporters.export import exporter_map
59 from IPython.nbconvert.exporters.export import exporter_map
60 except ImportError as e:
60 except ImportError as e:
61 raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
61 raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
62
62
63 try:
63 try:
64 Exporter = exporter_map[format]
64 Exporter = exporter_map[format]
65 except KeyError:
65 except KeyError:
66 # should this be 400?
66 # should this be 400?
67 raise web.HTTPError(404, u"No exporter for format: %s" % format)
67 raise web.HTTPError(404, u"No exporter for format: %s" % format)
68
68
69 try:
69 try:
70 return Exporter(**kwargs)
70 return Exporter(**kwargs)
71 except Exception as e:
71 except Exception as e:
72 raise web.HTTPError(500, "Could not construct Exporter: %s" % e)
72 raise web.HTTPError(500, "Could not construct Exporter: %s" % e)
73
73
74 class NbconvertFileHandler(IPythonHandler):
74 class NbconvertFileHandler(IPythonHandler):
75
75
76 SUPPORTED_METHODS = ('GET',)
76 SUPPORTED_METHODS = ('GET',)
77
77
78 @web.authenticated
78 @web.authenticated
79 def get(self, format, path='', name=None):
79 def get(self, format, path='', name=None):
80
80
81 exporter = get_exporter(format, config=self.config, log=self.log)
81 exporter = get_exporter(format, config=self.config, log=self.log)
82
82
83 path = path.strip('/')
83 path = path.strip('/')
84 model = self.contents_manager.get_model(name=name, path=path)
84 model = self.contents_manager.get_model(name=name, path=path)
85
85
86 self.set_header('Last-Modified', model['last_modified'])
86 self.set_header('Last-Modified', model['last_modified'])
87
87
88 try:
88 try:
89 output, resources = exporter.from_notebook_node(model['content'])
89 output, resources = exporter.from_notebook_node(model['content'])
90 except Exception as e:
90 except Exception as e:
91 raise web.HTTPError(500, "nbconvert failed: %s" % e)
91 raise web.HTTPError(500, "nbconvert failed: %s" % e)
92
92
93 if respond_zip(self, name, output, resources):
93 if respond_zip(self, name, output, resources):
94 return
94 return
95
95
96 # Force download if requested
96 # Force download if requested
97 if self.get_argument('download', 'false').lower() == 'true':
97 if self.get_argument('download', 'false').lower() == 'true':
98 filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
98 filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
99 self.set_header('Content-Disposition',
99 self.set_header('Content-Disposition',
100 'attachment; filename="%s"' % filename)
100 'attachment; filename="%s"' % filename)
101
101
102 # MIME type
102 # MIME type
103 if exporter.output_mimetype:
103 if exporter.output_mimetype:
104 self.set_header('Content-Type',
104 self.set_header('Content-Type',
105 '%s; charset=utf-8' % exporter.output_mimetype)
105 '%s; charset=utf-8' % exporter.output_mimetype)
106
106
107 self.finish(output)
107 self.finish(output)
108
108
109 class NbconvertPostHandler(IPythonHandler):
109 class NbconvertPostHandler(IPythonHandler):
110 SUPPORTED_METHODS = ('POST',)
110 SUPPORTED_METHODS = ('POST',)
111
111
112 @web.authenticated
112 @web.authenticated
113 def post(self, format):
113 def post(self, format):
114 exporter = get_exporter(format, config=self.config)
114 exporter = get_exporter(format, config=self.config)
115
115
116 model = self.get_json_body()
116 model = self.get_json_body()
117 name = model.get('name', 'notebook.ipynb')
117 nbnode = to_notebook_json(model['content'])
118 nbnode = to_notebook_json(model['content'])
118
119
119 try:
120 try:
120 output, resources = exporter.from_notebook_node(nbnode)
121 output, resources = exporter.from_notebook_node(nbnode)
121 except Exception as e:
122 except Exception as e:
122 raise web.HTTPError(500, "nbconvert failed: %s" % e)
123 raise web.HTTPError(500, "nbconvert failed: %s" % e)
123
124
124 if respond_zip(self, nbnode.metadata.name, output, resources):
125 if respond_zip(self, name, output, resources):
125 return
126 return
126
127
127 # MIME type
128 # MIME type
128 if exporter.output_mimetype:
129 if exporter.output_mimetype:
129 self.set_header('Content-Type',
130 self.set_header('Content-Type',
130 '%s; charset=utf-8' % exporter.output_mimetype)
131 '%s; charset=utf-8' % exporter.output_mimetype)
131
132
132 self.finish(output)
133 self.finish(output)
133
134
134
135
135 #-----------------------------------------------------------------------------
136 #-----------------------------------------------------------------------------
136 # URL to handler mappings
137 # URL to handler mappings
137 #-----------------------------------------------------------------------------
138 #-----------------------------------------------------------------------------
138
139
139 _format_regex = r"(?P<format>\w+)"
140 _format_regex = r"(?P<format>\w+)"
140
141
141
142
142 default_handlers = [
143 default_handlers = [
143 (r"/nbconvert/%s%s" % (_format_regex, notebook_path_regex),
144 (r"/nbconvert/%s%s" % (_format_regex, notebook_path_regex),
144 NbconvertFileHandler),
145 NbconvertFileHandler),
145 (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
146 (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
146 (r"/nbconvert/html%s" % path_regex, FilesRedirectHandler),
147 (r"/nbconvert/html%s" % path_regex, FilesRedirectHandler),
147 ]
148 ]
@@ -1,129 +1,131 b''
1 # coding: utf-8
1 # coding: utf-8
2 import base64
2 import base64
3 import io
3 import io
4 import json
4 import json
5 import os
5 import os
6 from os.path import join as pjoin
6 from os.path import join as pjoin
7 import shutil
7 import shutil
8
8
9 import requests
9 import requests
10
10
11 from IPython.html.utils import url_path_join
11 from IPython.html.utils import url_path_join
12 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
12 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
13 from IPython.nbformat.current import (new_notebook, write, new_worksheet,
13 from IPython.nbformat.current import (new_notebook, write,
14 new_heading_cell, new_code_cell,
14 new_heading_cell, new_code_cell,
15 new_output)
15 new_output)
16
16
17 from IPython.testing.decorators import onlyif_cmds_exist
17 from IPython.testing.decorators import onlyif_cmds_exist
18
18
19
19
20 class NbconvertAPI(object):
20 class NbconvertAPI(object):
21 """Wrapper for nbconvert API calls."""
21 """Wrapper for nbconvert API calls."""
22 def __init__(self, base_url):
22 def __init__(self, base_url):
23 self.base_url = base_url
23 self.base_url = base_url
24
24
25 def _req(self, verb, path, body=None, params=None):
25 def _req(self, verb, path, body=None, params=None):
26 response = requests.request(verb,
26 response = requests.request(verb,
27 url_path_join(self.base_url, 'nbconvert', path),
27 url_path_join(self.base_url, 'nbconvert', path),
28 data=body, params=params,
28 data=body, params=params,
29 )
29 )
30 response.raise_for_status()
30 response.raise_for_status()
31 return response
31 return response
32
32
33 def from_file(self, format, path, name, download=False):
33 def from_file(self, format, path, name, download=False):
34 return self._req('GET', url_path_join(format, path, name),
34 return self._req('GET', url_path_join(format, path, name),
35 params={'download':download})
35 params={'download':download})
36
36
37 def from_post(self, format, nbmodel):
37 def from_post(self, format, nbmodel):
38 body = json.dumps(nbmodel)
38 body = json.dumps(nbmodel)
39 return self._req('POST', format, body)
39 return self._req('POST', format, body)
40
40
41 def list_formats(self):
41 def list_formats(self):
42 return self._req('GET', '')
42 return self._req('GET', '')
43
43
44 png_green_pixel = base64.encodestring(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00'
44 png_green_pixel = base64.encodestring(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00'
45 b'\x00\x00\x01\x00\x00x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDAT'
45 b'\x00\x00\x01\x00\x00x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDAT'
46 b'\x08\xd7c\x90\xfb\xcf\x00\x00\x02\\\x01\x1e.~d\x87\x00\x00\x00\x00IEND\xaeB`\x82')
46 b'\x08\xd7c\x90\xfb\xcf\x00\x00\x02\\\x01\x1e.~d\x87\x00\x00\x00\x00IEND\xaeB`\x82'
47 ).decode('ascii')
47
48
48 class APITest(NotebookTestBase):
49 class APITest(NotebookTestBase):
49 def setUp(self):
50 def setUp(self):
50 nbdir = self.notebook_dir.name
51 nbdir = self.notebook_dir.name
51
52
52 if not os.path.isdir(pjoin(nbdir, 'foo')):
53 if not os.path.isdir(pjoin(nbdir, 'foo')):
53 os.mkdir(pjoin(nbdir, 'foo'))
54 os.mkdir(pjoin(nbdir, 'foo'))
54
55
55 nb = new_notebook(name='testnb')
56 nb = new_notebook()
56
57
57 ws = new_worksheet()
58 nb.cells.append(new_heading_cell(u'Created by test Β³'))
58 nb.worksheets = [ws]
59 cc1 = new_code_cell(source=u'print(2*6)')
59 ws.cells.append(new_heading_cell(u'Created by test Β³'))
60 cc1.outputs.append(new_output(output_type="stream", data=u'12'))
60 cc1 = new_code_cell(input=u'print(2*6)')
61 cc1.outputs.append(new_output(output_type="execute_result",
61 cc1.outputs.append(new_output(output_text=u'12', output_type='stream'))
62 mime_bundle={'image/png' : png_green_pixel},
62 cc1.outputs.append(new_output(output_png=png_green_pixel, output_type='pyout'))
63 prompt_number=1,
63 ws.cells.append(cc1)
64 ))
65 nb.cells.append(cc1)
64
66
65 with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w',
67 with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w',
66 encoding='utf-8') as f:
68 encoding='utf-8') as f:
67 write(nb, f, format='ipynb')
69 write(nb, f)
68
70
69 self.nbconvert_api = NbconvertAPI(self.base_url())
71 self.nbconvert_api = NbconvertAPI(self.base_url())
70
72
71 def tearDown(self):
73 def tearDown(self):
72 nbdir = self.notebook_dir.name
74 nbdir = self.notebook_dir.name
73
75
74 for dname in ['foo']:
76 for dname in ['foo']:
75 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
77 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
76
78
77 @onlyif_cmds_exist('pandoc')
79 @onlyif_cmds_exist('pandoc')
78 def test_from_file(self):
80 def test_from_file(self):
79 r = self.nbconvert_api.from_file('html', 'foo', 'testnb.ipynb')
81 r = self.nbconvert_api.from_file('html', 'foo', 'testnb.ipynb')
80 self.assertEqual(r.status_code, 200)
82 self.assertEqual(r.status_code, 200)
81 self.assertIn(u'text/html', r.headers['Content-Type'])
83 self.assertIn(u'text/html', r.headers['Content-Type'])
82 self.assertIn(u'Created by test', r.text)
84 self.assertIn(u'Created by test', r.text)
83 self.assertIn(u'print', r.text)
85 self.assertIn(u'print', r.text)
84
86
85 r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb')
87 r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb')
86 self.assertIn(u'text/x-python', r.headers['Content-Type'])
88 self.assertIn(u'text/x-python', r.headers['Content-Type'])
87 self.assertIn(u'print(2*6)', r.text)
89 self.assertIn(u'print(2*6)', r.text)
88
90
89 @onlyif_cmds_exist('pandoc')
91 @onlyif_cmds_exist('pandoc')
90 def test_from_file_404(self):
92 def test_from_file_404(self):
91 with assert_http_error(404):
93 with assert_http_error(404):
92 self.nbconvert_api.from_file('html', 'foo', 'thisdoesntexist.ipynb')
94 self.nbconvert_api.from_file('html', 'foo', 'thisdoesntexist.ipynb')
93
95
94 @onlyif_cmds_exist('pandoc')
96 @onlyif_cmds_exist('pandoc')
95 def test_from_file_download(self):
97 def test_from_file_download(self):
96 r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb', download=True)
98 r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb', download=True)
97 content_disposition = r.headers['Content-Disposition']
99 content_disposition = r.headers['Content-Disposition']
98 self.assertIn('attachment', content_disposition)
100 self.assertIn('attachment', content_disposition)
99 self.assertIn('testnb.py', content_disposition)
101 self.assertIn('testnb.py', content_disposition)
100
102
101 @onlyif_cmds_exist('pandoc')
103 @onlyif_cmds_exist('pandoc')
102 def test_from_file_zip(self):
104 def test_from_file_zip(self):
103 r = self.nbconvert_api.from_file('latex', 'foo', 'testnb.ipynb', download=True)
105 r = self.nbconvert_api.from_file('latex', 'foo', 'testnb.ipynb', download=True)
104 self.assertIn(u'application/zip', r.headers['Content-Type'])
106 self.assertIn(u'application/zip', r.headers['Content-Type'])
105 self.assertIn(u'.zip', r.headers['Content-Disposition'])
107 self.assertIn(u'.zip', r.headers['Content-Disposition'])
106
108
107 @onlyif_cmds_exist('pandoc')
109 @onlyif_cmds_exist('pandoc')
108 def test_from_post(self):
110 def test_from_post(self):
109 nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb')
111 nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb')
110 nbmodel = requests.get(nbmodel_url).json()
112 nbmodel = requests.get(nbmodel_url).json()
111
113
112 r = self.nbconvert_api.from_post(format='html', nbmodel=nbmodel)
114 r = self.nbconvert_api.from_post(format='html', nbmodel=nbmodel)
113 self.assertEqual(r.status_code, 200)
115 self.assertEqual(r.status_code, 200)
114 self.assertIn(u'text/html', r.headers['Content-Type'])
116 self.assertIn(u'text/html', r.headers['Content-Type'])
115 self.assertIn(u'Created by test', r.text)
117 self.assertIn(u'Created by test', r.text)
116 self.assertIn(u'print', r.text)
118 self.assertIn(u'print', r.text)
117
119
118 r = self.nbconvert_api.from_post(format='python', nbmodel=nbmodel)
120 r = self.nbconvert_api.from_post(format='python', nbmodel=nbmodel)
119 self.assertIn(u'text/x-python', r.headers['Content-Type'])
121 self.assertIn(u'text/x-python', r.headers['Content-Type'])
120 self.assertIn(u'print(2*6)', r.text)
122 self.assertIn(u'print(2*6)', r.text)
121
123
122 @onlyif_cmds_exist('pandoc')
124 @onlyif_cmds_exist('pandoc')
123 def test_from_post_zip(self):
125 def test_from_post_zip(self):
124 nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb')
126 nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb')
125 nbmodel = requests.get(nbmodel_url).json()
127 nbmodel = requests.get(nbmodel_url).json()
126
128
127 r = self.nbconvert_api.from_post(format='latex', nbmodel=nbmodel)
129 r = self.nbconvert_api.from_post(format='latex', nbmodel=nbmodel)
128 self.assertIn(u'application/zip', r.headers['Content-Type'])
130 self.assertIn(u'application/zip', r.headers['Content-Type'])
129 self.assertIn(u'.zip', r.headers['Content-Disposition'])
131 self.assertIn(u'.zip', r.headers['Content-Disposition'])
@@ -1,346 +1,345 b''
1 """A base class for contents managers."""
1 """A base class for contents managers."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 from fnmatch import fnmatch
6 from fnmatch import fnmatch
7 import itertools
7 import itertools
8 import json
8 import json
9 import os
9 import os
10
10
11 from tornado.web import HTTPError
11 from tornado.web import HTTPError
12
12
13 from IPython.config.configurable import LoggingConfigurable
13 from IPython.config.configurable import LoggingConfigurable
14 from IPython.nbformat import current, sign
14 from IPython.nbformat import current, sign
15 from IPython.utils.traitlets import Instance, Unicode, List
15 from IPython.utils.traitlets import Instance, Unicode, List
16
16
17
17
18 class ContentsManager(LoggingConfigurable):
18 class ContentsManager(LoggingConfigurable):
19 """Base class for serving files and directories.
19 """Base class for serving files and directories.
20
20
21 This serves any text or binary file,
21 This serves any text or binary file,
22 as well as directories,
22 as well as directories,
23 with special handling for JSON notebook documents.
23 with special handling for JSON notebook documents.
24
24
25 Most APIs take a path argument,
25 Most APIs take a path argument,
26 which is always an API-style unicode path,
26 which is always an API-style unicode path,
27 and always refers to a directory.
27 and always refers to a directory.
28
28
29 - unicode, not url-escaped
29 - unicode, not url-escaped
30 - '/'-separated
30 - '/'-separated
31 - leading and trailing '/' will be stripped
31 - leading and trailing '/' will be stripped
32 - if unspecified, path defaults to '',
32 - if unspecified, path defaults to '',
33 indicating the root path.
33 indicating the root path.
34
34
35 name is also unicode, and refers to a specfic target:
35 name is also unicode, and refers to a specfic target:
36
36
37 - unicode, not url-escaped
37 - unicode, not url-escaped
38 - must not contain '/'
38 - must not contain '/'
39 - It refers to an individual filename
39 - It refers to an individual filename
40 - It may refer to a directory name,
40 - It may refer to a directory name,
41 in the case of listing or creating directories.
41 in the case of listing or creating directories.
42
42
43 """
43 """
44
44
45 notary = Instance(sign.NotebookNotary)
45 notary = Instance(sign.NotebookNotary)
46 def _notary_default(self):
46 def _notary_default(self):
47 return sign.NotebookNotary(parent=self)
47 return sign.NotebookNotary(parent=self)
48
48
49 hide_globs = List(Unicode, [
49 hide_globs = List(Unicode, [
50 u'__pycache__', '*.pyc', '*.pyo',
50 u'__pycache__', '*.pyc', '*.pyo',
51 '.DS_Store', '*.so', '*.dylib', '*~',
51 '.DS_Store', '*.so', '*.dylib', '*~',
52 ], config=True, help="""
52 ], config=True, help="""
53 Glob patterns to hide in file and directory listings.
53 Glob patterns to hide in file and directory listings.
54 """)
54 """)
55
55
56 untitled_notebook = Unicode("Untitled", config=True,
56 untitled_notebook = Unicode("Untitled", config=True,
57 help="The base name used when creating untitled notebooks."
57 help="The base name used when creating untitled notebooks."
58 )
58 )
59
59
60 untitled_file = Unicode("untitled", config=True,
60 untitled_file = Unicode("untitled", config=True,
61 help="The base name used when creating untitled files."
61 help="The base name used when creating untitled files."
62 )
62 )
63
63
64 untitled_directory = Unicode("Untitled Folder", config=True,
64 untitled_directory = Unicode("Untitled Folder", config=True,
65 help="The base name used when creating untitled directories."
65 help="The base name used when creating untitled directories."
66 )
66 )
67
67
68 # ContentsManager API part 1: methods that must be
68 # ContentsManager API part 1: methods that must be
69 # implemented in subclasses.
69 # implemented in subclasses.
70
70
71 def path_exists(self, path):
71 def path_exists(self, path):
72 """Does the API-style path (directory) actually exist?
72 """Does the API-style path (directory) actually exist?
73
73
74 Like os.path.isdir
74 Like os.path.isdir
75
75
76 Override this method in subclasses.
76 Override this method in subclasses.
77
77
78 Parameters
78 Parameters
79 ----------
79 ----------
80 path : string
80 path : string
81 The path to check
81 The path to check
82
82
83 Returns
83 Returns
84 -------
84 -------
85 exists : bool
85 exists : bool
86 Whether the path does indeed exist.
86 Whether the path does indeed exist.
87 """
87 """
88 raise NotImplementedError
88 raise NotImplementedError
89
89
90 def is_hidden(self, path):
90 def is_hidden(self, path):
91 """Does the API style path correspond to a hidden directory or file?
91 """Does the API style path correspond to a hidden directory or file?
92
92
93 Parameters
93 Parameters
94 ----------
94 ----------
95 path : string
95 path : string
96 The path to check. This is an API path (`/` separated,
96 The path to check. This is an API path (`/` separated,
97 relative to root dir).
97 relative to root dir).
98
98
99 Returns
99 Returns
100 -------
100 -------
101 hidden : bool
101 hidden : bool
102 Whether the path is hidden.
102 Whether the path is hidden.
103
103
104 """
104 """
105 raise NotImplementedError
105 raise NotImplementedError
106
106
107 def file_exists(self, name, path=''):
107 def file_exists(self, name, path=''):
108 """Does a file exist at the given name and path?
108 """Does a file exist at the given name and path?
109
109
110 Like os.path.isfile
110 Like os.path.isfile
111
111
112 Override this method in subclasses.
112 Override this method in subclasses.
113
113
114 Parameters
114 Parameters
115 ----------
115 ----------
116 name : string
116 name : string
117 The name of the file you are checking.
117 The name of the file you are checking.
118 path : string
118 path : string
119 The relative path to the file's directory (with '/' as separator)
119 The relative path to the file's directory (with '/' as separator)
120
120
121 Returns
121 Returns
122 -------
122 -------
123 exists : bool
123 exists : bool
124 Whether the file exists.
124 Whether the file exists.
125 """
125 """
126 raise NotImplementedError('must be implemented in a subclass')
126 raise NotImplementedError('must be implemented in a subclass')
127
127
128 def exists(self, name, path=''):
128 def exists(self, name, path=''):
129 """Does a file or directory exist at the given name and path?
129 """Does a file or directory exist at the given name and path?
130
130
131 Like os.path.exists
131 Like os.path.exists
132
132
133 Parameters
133 Parameters
134 ----------
134 ----------
135 name : string
135 name : string
136 The name of the file you are checking.
136 The name of the file you are checking.
137 path : string
137 path : string
138 The relative path to the file's directory (with '/' as separator)
138 The relative path to the file's directory (with '/' as separator)
139
139
140 Returns
140 Returns
141 -------
141 -------
142 exists : bool
142 exists : bool
143 Whether the target exists.
143 Whether the target exists.
144 """
144 """
145 return self.file_exists(name, path) or self.path_exists("%s/%s" % (path, name))
145 return self.file_exists(name, path) or self.path_exists("%s/%s" % (path, name))
146
146
147 def get_model(self, name, path='', content=True):
147 def get_model(self, name, path='', content=True):
148 """Get the model of a file or directory with or without content."""
148 """Get the model of a file or directory with or without content."""
149 raise NotImplementedError('must be implemented in a subclass')
149 raise NotImplementedError('must be implemented in a subclass')
150
150
151 def save(self, model, name, path=''):
151 def save(self, model, name, path=''):
152 """Save the file or directory and return the model with no content."""
152 """Save the file or directory and return the model with no content."""
153 raise NotImplementedError('must be implemented in a subclass')
153 raise NotImplementedError('must be implemented in a subclass')
154
154
155 def update(self, model, name, path=''):
155 def update(self, model, name, path=''):
156 """Update the file or directory and return the model with no content.
156 """Update the file or directory and return the model with no content.
157
157
158 For use in PATCH requests, to enable renaming a file without
158 For use in PATCH requests, to enable renaming a file without
159 re-uploading its contents. Only used for renaming at the moment.
159 re-uploading its contents. Only used for renaming at the moment.
160 """
160 """
161 raise NotImplementedError('must be implemented in a subclass')
161 raise NotImplementedError('must be implemented in a subclass')
162
162
163 def delete(self, name, path=''):
163 def delete(self, name, path=''):
164 """Delete file or directory by name and path."""
164 """Delete file or directory by name and path."""
165 raise NotImplementedError('must be implemented in a subclass')
165 raise NotImplementedError('must be implemented in a subclass')
166
166
167 def create_checkpoint(self, name, path=''):
167 def create_checkpoint(self, name, path=''):
168 """Create a checkpoint of the current state of a file
168 """Create a checkpoint of the current state of a file
169
169
170 Returns a checkpoint_id for the new checkpoint.
170 Returns a checkpoint_id for the new checkpoint.
171 """
171 """
172 raise NotImplementedError("must be implemented in a subclass")
172 raise NotImplementedError("must be implemented in a subclass")
173
173
174 def list_checkpoints(self, name, path=''):
174 def list_checkpoints(self, name, path=''):
175 """Return a list of checkpoints for a given file"""
175 """Return a list of checkpoints for a given file"""
176 return []
176 return []
177
177
178 def restore_checkpoint(self, checkpoint_id, name, path=''):
178 def restore_checkpoint(self, checkpoint_id, name, path=''):
179 """Restore a file from one of its checkpoints"""
179 """Restore a file from one of its checkpoints"""
180 raise NotImplementedError("must be implemented in a subclass")
180 raise NotImplementedError("must be implemented in a subclass")
181
181
182 def delete_checkpoint(self, checkpoint_id, name, path=''):
182 def delete_checkpoint(self, checkpoint_id, name, path=''):
183 """delete a checkpoint for a file"""
183 """delete a checkpoint for a file"""
184 raise NotImplementedError("must be implemented in a subclass")
184 raise NotImplementedError("must be implemented in a subclass")
185
185
186 # ContentsManager API part 2: methods that have useable default
186 # ContentsManager API part 2: methods that have useable default
187 # implementations, but can be overridden in subclasses.
187 # implementations, but can be overridden in subclasses.
188
188
189 def info_string(self):
189 def info_string(self):
190 return "Serving contents"
190 return "Serving contents"
191
191
192 def get_kernel_path(self, name, path='', model=None):
192 def get_kernel_path(self, name, path='', model=None):
193 """ Return the path to start kernel in """
193 """ Return the path to start kernel in """
194 return path
194 return path
195
195
196 def increment_filename(self, filename, path=''):
196 def increment_filename(self, filename, path=''):
197 """Increment a filename until it is unique.
197 """Increment a filename until it is unique.
198
198
199 Parameters
199 Parameters
200 ----------
200 ----------
201 filename : unicode
201 filename : unicode
202 The name of a file, including extension
202 The name of a file, including extension
203 path : unicode
203 path : unicode
204 The API path of the target's directory
204 The API path of the target's directory
205
205
206 Returns
206 Returns
207 -------
207 -------
208 name : unicode
208 name : unicode
209 A filename that is unique, based on the input filename.
209 A filename that is unique, based on the input filename.
210 """
210 """
211 path = path.strip('/')
211 path = path.strip('/')
212 basename, ext = os.path.splitext(filename)
212 basename, ext = os.path.splitext(filename)
213 for i in itertools.count():
213 for i in itertools.count():
214 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
214 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
215 ext=ext)
215 ext=ext)
216 if not self.file_exists(name, path):
216 if not self.file_exists(name, path):
217 break
217 break
218 return name
218 return name
219
219
220 def validate_notebook_model(self, model):
220 def validate_notebook_model(self, model):
221 """Add failed-validation message to model"""
221 """Add failed-validation message to model"""
222 try:
222 try:
223 current.validate(model['content'])
223 current.validate(model['content'])
224 except current.ValidationError as e:
224 except current.ValidationError as e:
225 model['message'] = 'Notebook Validation failed: {}:\n{}'.format(
225 model['message'] = 'Notebook Validation failed: {}:\n{}'.format(
226 e.message, json.dumps(e.instance, indent=1, default=lambda obj: '<UNKNOWN>'),
226 e.message, json.dumps(e.instance, indent=1, default=lambda obj: '<UNKNOWN>'),
227 )
227 )
228 return model
228 return model
229
229
230 def create_file(self, model=None, path='', ext='.ipynb'):
230 def create_file(self, model=None, path='', ext='.ipynb'):
231 """Create a new file or directory and return its model with no content."""
231 """Create a new file or directory and return its model with no content."""
232 path = path.strip('/')
232 path = path.strip('/')
233 if model is None:
233 if model is None:
234 model = {}
234 model = {}
235 if 'content' not in model and model.get('type', None) != 'directory':
235 if 'content' not in model and model.get('type', None) != 'directory':
236 if ext == '.ipynb':
236 if ext == '.ipynb':
237 metadata = current.new_metadata(name=u'')
237 model['content'] = current.new_notebook()
238 model['content'] = current.new_notebook(metadata=metadata)
239 model['type'] = 'notebook'
238 model['type'] = 'notebook'
240 model['format'] = 'json'
239 model['format'] = 'json'
241 else:
240 else:
242 model['content'] = ''
241 model['content'] = ''
243 model['type'] = 'file'
242 model['type'] = 'file'
244 model['format'] = 'text'
243 model['format'] = 'text'
245 if 'name' not in model:
244 if 'name' not in model:
246 if model['type'] == 'directory':
245 if model['type'] == 'directory':
247 untitled = self.untitled_directory
246 untitled = self.untitled_directory
248 elif model['type'] == 'notebook':
247 elif model['type'] == 'notebook':
249 untitled = self.untitled_notebook
248 untitled = self.untitled_notebook
250 elif model['type'] == 'file':
249 elif model['type'] == 'file':
251 untitled = self.untitled_file
250 untitled = self.untitled_file
252 else:
251 else:
253 raise HTTPError(400, "Unexpected model type: %r" % model['type'])
252 raise HTTPError(400, "Unexpected model type: %r" % model['type'])
254 model['name'] = self.increment_filename(untitled + ext, path)
253 model['name'] = self.increment_filename(untitled + ext, path)
255
254
256 model['path'] = path
255 model['path'] = path
257 model = self.save(model, model['name'], model['path'])
256 model = self.save(model, model['name'], model['path'])
258 return model
257 return model
259
258
260 def copy(self, from_name, to_name=None, path=''):
259 def copy(self, from_name, to_name=None, path=''):
261 """Copy an existing file and return its new model.
260 """Copy an existing file and return its new model.
262
261
263 If to_name not specified, increment `from_name-Copy#.ext`.
262 If to_name not specified, increment `from_name-Copy#.ext`.
264
263
265 copy_from can be a full path to a file,
264 copy_from can be a full path to a file,
266 or just a base name. If a base name, `path` is used.
265 or just a base name. If a base name, `path` is used.
267 """
266 """
268 path = path.strip('/')
267 path = path.strip('/')
269 if '/' in from_name:
268 if '/' in from_name:
270 from_path, from_name = from_name.rsplit('/', 1)
269 from_path, from_name = from_name.rsplit('/', 1)
271 else:
270 else:
272 from_path = path
271 from_path = path
273 model = self.get_model(from_name, from_path)
272 model = self.get_model(from_name, from_path)
274 if model['type'] == 'directory':
273 if model['type'] == 'directory':
275 raise HTTPError(400, "Can't copy directories")
274 raise HTTPError(400, "Can't copy directories")
276 if not to_name:
275 if not to_name:
277 base, ext = os.path.splitext(from_name)
276 base, ext = os.path.splitext(from_name)
278 copy_name = u'{0}-Copy{1}'.format(base, ext)
277 copy_name = u'{0}-Copy{1}'.format(base, ext)
279 to_name = self.increment_filename(copy_name, path)
278 to_name = self.increment_filename(copy_name, path)
280 model['name'] = to_name
279 model['name'] = to_name
281 model['path'] = path
280 model['path'] = path
282 model = self.save(model, to_name, path)
281 model = self.save(model, to_name, path)
283 return model
282 return model
284
283
285 def log_info(self):
284 def log_info(self):
286 self.log.info(self.info_string())
285 self.log.info(self.info_string())
287
286
288 def trust_notebook(self, name, path=''):
287 def trust_notebook(self, name, path=''):
289 """Explicitly trust a notebook
288 """Explicitly trust a notebook
290
289
291 Parameters
290 Parameters
292 ----------
291 ----------
293 name : string
292 name : string
294 The filename of the notebook
293 The filename of the notebook
295 path : string
294 path : string
296 The notebook's directory
295 The notebook's directory
297 """
296 """
298 model = self.get_model(name, path)
297 model = self.get_model(name, path)
299 nb = model['content']
298 nb = model['content']
300 self.log.warn("Trusting notebook %s/%s", path, name)
299 self.log.warn("Trusting notebook %s/%s", path, name)
301 self.notary.mark_cells(nb, True)
300 self.notary.mark_cells(nb, True)
302 self.save(model, name, path)
301 self.save(model, name, path)
303
302
304 def check_and_sign(self, nb, name='', path=''):
303 def check_and_sign(self, nb, name='', path=''):
305 """Check for trusted cells, and sign the notebook.
304 """Check for trusted cells, and sign the notebook.
306
305
307 Called as a part of saving notebooks.
306 Called as a part of saving notebooks.
308
307
309 Parameters
308 Parameters
310 ----------
309 ----------
311 nb : dict
310 nb : dict
312 The notebook object (in nbformat.current format)
311 The notebook object (in nbformat.current format)
313 name : string
312 name : string
314 The filename of the notebook (for logging)
313 The filename of the notebook (for logging)
315 path : string
314 path : string
316 The notebook's directory (for logging)
315 The notebook's directory (for logging)
317 """
316 """
318 if nb['nbformat'] != current.nbformat:
317 if nb['nbformat'] != current.nbformat:
319 return
318 return
320 if self.notary.check_cells(nb):
319 if self.notary.check_cells(nb):
321 self.notary.sign(nb)
320 self.notary.sign(nb)
322 else:
321 else:
323 self.log.warn("Saving untrusted notebook %s/%s", path, name)
322 self.log.warn("Saving untrusted notebook %s/%s", path, name)
324
323
325 def mark_trusted_cells(self, nb, name='', path=''):
324 def mark_trusted_cells(self, nb, name='', path=''):
326 """Mark cells as trusted if the notebook signature matches.
325 """Mark cells as trusted if the notebook signature matches.
327
326
328 Called as a part of loading notebooks.
327 Called as a part of loading notebooks.
329
328
330 Parameters
329 Parameters
331 ----------
330 ----------
332 nb : dict
331 nb : dict
333 The notebook object (in nbformat.current format)
332 The notebook object (in nbformat.current format)
334 name : string
333 name : string
335 The filename of the notebook (for logging)
334 The filename of the notebook (for logging)
336 path : string
335 path : string
337 The notebook's directory (for logging)
336 The notebook's directory (for logging)
338 """
337 """
339 trusted = self.notary.check_signature(nb)
338 trusted = self.notary.check_signature(nb)
340 if not trusted:
339 if not trusted:
341 self.log.warn("Notebook %s/%s is not trusted", path, name)
340 self.log.warn("Notebook %s/%s is not trusted", path, name)
342 self.notary.mark_cells(nb, trusted)
341 self.notary.mark_cells(nb, trusted)
343
342
344 def should_list(self, name):
343 def should_list(self, name):
345 """Should this file/directory name be displayed in a listing?"""
344 """Should this file/directory name be displayed in a listing?"""
346 return not any(fnmatch(name, glob) for glob in self.hide_globs)
345 return not any(fnmatch(name, glob) for glob in self.hide_globs)
@@ -1,485 +1,480 b''
1 # coding: utf-8
1 # coding: utf-8
2 """Test the contents webservice API."""
2 """Test the contents webservice API."""
3
3
4 import base64
4 import base64
5 import io
5 import io
6 import json
6 import json
7 import os
7 import os
8 import shutil
8 import shutil
9 from unicodedata import normalize
9 from unicodedata import normalize
10
10
11 pjoin = os.path.join
11 pjoin = os.path.join
12
12
13 import requests
13 import requests
14
14
15 from IPython.html.utils import url_path_join, url_escape
15 from IPython.html.utils import url_path_join, url_escape
16 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
16 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
17 from IPython.nbformat import current
17 from IPython.nbformat import current
18 from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
18 from IPython.nbformat.current import (new_notebook, write, read,
19 new_heading_cell, to_notebook_json)
19 new_heading_cell, to_notebook_json)
20 from IPython.nbformat import v2
20 from IPython.nbformat import v2
21 from IPython.utils import py3compat
21 from IPython.utils import py3compat
22 from IPython.utils.data import uniq_stable
22 from IPython.utils.data import uniq_stable
23
23
24
24
25 def notebooks_only(dir_model):
25 def notebooks_only(dir_model):
26 return [nb for nb in dir_model['content'] if nb['type']=='notebook']
26 return [nb for nb in dir_model['content'] if nb['type']=='notebook']
27
27
28 def dirs_only(dir_model):
28 def dirs_only(dir_model):
29 return [x for x in dir_model['content'] if x['type']=='directory']
29 return [x for x in dir_model['content'] if x['type']=='directory']
30
30
31
31
32 class API(object):
32 class API(object):
33 """Wrapper for contents API calls."""
33 """Wrapper for contents API calls."""
34 def __init__(self, base_url):
34 def __init__(self, base_url):
35 self.base_url = base_url
35 self.base_url = base_url
36
36
37 def _req(self, verb, path, body=None):
37 def _req(self, verb, path, body=None):
38 response = requests.request(verb,
38 response = requests.request(verb,
39 url_path_join(self.base_url, 'api/contents', path),
39 url_path_join(self.base_url, 'api/contents', path),
40 data=body,
40 data=body,
41 )
41 )
42 response.raise_for_status()
42 response.raise_for_status()
43 return response
43 return response
44
44
45 def list(self, path='/'):
45 def list(self, path='/'):
46 return self._req('GET', path)
46 return self._req('GET', path)
47
47
48 def read(self, name, path='/'):
48 def read(self, name, path='/'):
49 return self._req('GET', url_path_join(path, name))
49 return self._req('GET', url_path_join(path, name))
50
50
51 def create_untitled(self, path='/', ext=None):
51 def create_untitled(self, path='/', ext=None):
52 body = None
52 body = None
53 if ext:
53 if ext:
54 body = json.dumps({'ext': ext})
54 body = json.dumps({'ext': ext})
55 return self._req('POST', path, body)
55 return self._req('POST', path, body)
56
56
57 def upload_untitled(self, body, path='/'):
57 def upload_untitled(self, body, path='/'):
58 return self._req('POST', path, body)
58 return self._req('POST', path, body)
59
59
60 def copy_untitled(self, copy_from, path='/'):
60 def copy_untitled(self, copy_from, path='/'):
61 body = json.dumps({'copy_from':copy_from})
61 body = json.dumps({'copy_from':copy_from})
62 return self._req('POST', path, body)
62 return self._req('POST', path, body)
63
63
64 def create(self, name, path='/'):
64 def create(self, name, path='/'):
65 return self._req('PUT', url_path_join(path, name))
65 return self._req('PUT', url_path_join(path, name))
66
66
67 def upload(self, name, body, path='/'):
67 def upload(self, name, body, path='/'):
68 return self._req('PUT', url_path_join(path, name), body)
68 return self._req('PUT', url_path_join(path, name), body)
69
69
70 def mkdir(self, name, path='/'):
70 def mkdir(self, name, path='/'):
71 return self._req('PUT', url_path_join(path, name), json.dumps({'type': 'directory'}))
71 return self._req('PUT', url_path_join(path, name), json.dumps({'type': 'directory'}))
72
72
73 def copy(self, copy_from, copy_to, path='/'):
73 def copy(self, copy_from, copy_to, path='/'):
74 body = json.dumps({'copy_from':copy_from})
74 body = json.dumps({'copy_from':copy_from})
75 return self._req('PUT', url_path_join(path, copy_to), body)
75 return self._req('PUT', url_path_join(path, copy_to), body)
76
76
77 def save(self, name, body, path='/'):
77 def save(self, name, body, path='/'):
78 return self._req('PUT', url_path_join(path, name), body)
78 return self._req('PUT', url_path_join(path, name), body)
79
79
80 def delete(self, name, path='/'):
80 def delete(self, name, path='/'):
81 return self._req('DELETE', url_path_join(path, name))
81 return self._req('DELETE', url_path_join(path, name))
82
82
83 def rename(self, name, path, new_name):
83 def rename(self, name, path, new_name):
84 body = json.dumps({'name': new_name})
84 body = json.dumps({'name': new_name})
85 return self._req('PATCH', url_path_join(path, name), body)
85 return self._req('PATCH', url_path_join(path, name), body)
86
86
87 def get_checkpoints(self, name, path):
87 def get_checkpoints(self, name, path):
88 return self._req('GET', url_path_join(path, name, 'checkpoints'))
88 return self._req('GET', url_path_join(path, name, 'checkpoints'))
89
89
90 def new_checkpoint(self, name, path):
90 def new_checkpoint(self, name, path):
91 return self._req('POST', url_path_join(path, name, 'checkpoints'))
91 return self._req('POST', url_path_join(path, name, 'checkpoints'))
92
92
93 def restore_checkpoint(self, name, path, checkpoint_id):
93 def restore_checkpoint(self, name, path, checkpoint_id):
94 return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
94 return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
95
95
96 def delete_checkpoint(self, name, path, checkpoint_id):
96 def delete_checkpoint(self, name, path, checkpoint_id):
97 return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
97 return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
98
98
99 class APITest(NotebookTestBase):
99 class APITest(NotebookTestBase):
100 """Test the kernels web service API"""
100 """Test the kernels web service API"""
101 dirs_nbs = [('', 'inroot'),
101 dirs_nbs = [('', 'inroot'),
102 ('Directory with spaces in', 'inspace'),
102 ('Directory with spaces in', 'inspace'),
103 (u'unicodΓ©', 'innonascii'),
103 (u'unicodΓ©', 'innonascii'),
104 ('foo', 'a'),
104 ('foo', 'a'),
105 ('foo', 'b'),
105 ('foo', 'b'),
106 ('foo', 'name with spaces'),
106 ('foo', 'name with spaces'),
107 ('foo', u'unicodΓ©'),
107 ('foo', u'unicodΓ©'),
108 ('foo/bar', 'baz'),
108 ('foo/bar', 'baz'),
109 ('ordering', 'A'),
109 ('ordering', 'A'),
110 ('ordering', 'b'),
110 ('ordering', 'b'),
111 ('ordering', 'C'),
111 ('ordering', 'C'),
112 (u'Γ₯ b', u'Γ§ d'),
112 (u'Γ₯ b', u'Γ§ d'),
113 ]
113 ]
114 hidden_dirs = ['.hidden', '__pycache__']
114 hidden_dirs = ['.hidden', '__pycache__']
115
115
116 dirs = uniq_stable([py3compat.cast_unicode(d) for (d,n) in dirs_nbs])
116 dirs = uniq_stable([py3compat.cast_unicode(d) for (d,n) in dirs_nbs])
117 del dirs[0] # remove ''
117 del dirs[0] # remove ''
118 top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs}
118 top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs}
119
119
120 @staticmethod
120 @staticmethod
121 def _blob_for_name(name):
121 def _blob_for_name(name):
122 return name.encode('utf-8') + b'\xFF'
122 return name.encode('utf-8') + b'\xFF'
123
123
124 @staticmethod
124 @staticmethod
125 def _txt_for_name(name):
125 def _txt_for_name(name):
126 return u'%s text file' % name
126 return u'%s text file' % name
127
127
128 def setUp(self):
128 def setUp(self):
129 nbdir = self.notebook_dir.name
129 nbdir = self.notebook_dir.name
130 self.blob = os.urandom(100)
130 self.blob = os.urandom(100)
131 self.b64_blob = base64.encodestring(self.blob).decode('ascii')
131 self.b64_blob = base64.encodestring(self.blob).decode('ascii')
132
132
133
133
134
134
135 for d in (self.dirs + self.hidden_dirs):
135 for d in (self.dirs + self.hidden_dirs):
136 d.replace('/', os.sep)
136 d.replace('/', os.sep)
137 if not os.path.isdir(pjoin(nbdir, d)):
137 if not os.path.isdir(pjoin(nbdir, d)):
138 os.mkdir(pjoin(nbdir, d))
138 os.mkdir(pjoin(nbdir, d))
139
139
140 for d, name in self.dirs_nbs:
140 for d, name in self.dirs_nbs:
141 d = d.replace('/', os.sep)
141 d = d.replace('/', os.sep)
142 # create a notebook
142 # create a notebook
143 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w',
143 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w',
144 encoding='utf-8') as f:
144 encoding='utf-8') as f:
145 nb = new_notebook(name=name)
145 nb = new_notebook()
146 write(nb, f, format='ipynb')
146 write(nb, f, format='ipynb')
147
147
148 # create a text file
148 # create a text file
149 with io.open(pjoin(nbdir, d, '%s.txt' % name), 'w',
149 with io.open(pjoin(nbdir, d, '%s.txt' % name), 'w',
150 encoding='utf-8') as f:
150 encoding='utf-8') as f:
151 f.write(self._txt_for_name(name))
151 f.write(self._txt_for_name(name))
152
152
153 # create a binary file
153 # create a binary file
154 with io.open(pjoin(nbdir, d, '%s.blob' % name), 'wb') as f:
154 with io.open(pjoin(nbdir, d, '%s.blob' % name), 'wb') as f:
155 f.write(self._blob_for_name(name))
155 f.write(self._blob_for_name(name))
156
156
157 self.api = API(self.base_url())
157 self.api = API(self.base_url())
158
158
159 def tearDown(self):
159 def tearDown(self):
160 nbdir = self.notebook_dir.name
160 nbdir = self.notebook_dir.name
161
161
162 for dname in (list(self.top_level_dirs) + self.hidden_dirs):
162 for dname in (list(self.top_level_dirs) + self.hidden_dirs):
163 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
163 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
164
164
165 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
165 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
166 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
166 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
167
167
168 def test_list_notebooks(self):
168 def test_list_notebooks(self):
169 nbs = notebooks_only(self.api.list().json())
169 nbs = notebooks_only(self.api.list().json())
170 self.assertEqual(len(nbs), 1)
170 self.assertEqual(len(nbs), 1)
171 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
171 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
172
172
173 nbs = notebooks_only(self.api.list('/Directory with spaces in/').json())
173 nbs = notebooks_only(self.api.list('/Directory with spaces in/').json())
174 self.assertEqual(len(nbs), 1)
174 self.assertEqual(len(nbs), 1)
175 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
175 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
176
176
177 nbs = notebooks_only(self.api.list(u'/unicodΓ©/').json())
177 nbs = notebooks_only(self.api.list(u'/unicodΓ©/').json())
178 self.assertEqual(len(nbs), 1)
178 self.assertEqual(len(nbs), 1)
179 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
179 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
180 self.assertEqual(nbs[0]['path'], u'unicodΓ©')
180 self.assertEqual(nbs[0]['path'], u'unicodΓ©')
181
181
182 nbs = notebooks_only(self.api.list('/foo/bar/').json())
182 nbs = notebooks_only(self.api.list('/foo/bar/').json())
183 self.assertEqual(len(nbs), 1)
183 self.assertEqual(len(nbs), 1)
184 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
184 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
185 self.assertEqual(nbs[0]['path'], 'foo/bar')
185 self.assertEqual(nbs[0]['path'], 'foo/bar')
186
186
187 nbs = notebooks_only(self.api.list('foo').json())
187 nbs = notebooks_only(self.api.list('foo').json())
188 self.assertEqual(len(nbs), 4)
188 self.assertEqual(len(nbs), 4)
189 nbnames = { normalize('NFC', n['name']) for n in nbs }
189 nbnames = { normalize('NFC', n['name']) for n in nbs }
190 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
190 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
191 expected = { normalize('NFC', name) for name in expected }
191 expected = { normalize('NFC', name) for name in expected }
192 self.assertEqual(nbnames, expected)
192 self.assertEqual(nbnames, expected)
193
193
194 nbs = notebooks_only(self.api.list('ordering').json())
194 nbs = notebooks_only(self.api.list('ordering').json())
195 nbnames = [n['name'] for n in nbs]
195 nbnames = [n['name'] for n in nbs]
196 expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
196 expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
197 self.assertEqual(nbnames, expected)
197 self.assertEqual(nbnames, expected)
198
198
199 def test_list_dirs(self):
199 def test_list_dirs(self):
200 dirs = dirs_only(self.api.list().json())
200 dirs = dirs_only(self.api.list().json())
201 dir_names = {normalize('NFC', d['name']) for d in dirs}
201 dir_names = {normalize('NFC', d['name']) for d in dirs}
202 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
202 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
203
203
204 def test_list_nonexistant_dir(self):
204 def test_list_nonexistant_dir(self):
205 with assert_http_error(404):
205 with assert_http_error(404):
206 self.api.list('nonexistant')
206 self.api.list('nonexistant')
207
207
208 def test_get_nb_contents(self):
208 def test_get_nb_contents(self):
209 for d, name in self.dirs_nbs:
209 for d, name in self.dirs_nbs:
210 nb = self.api.read('%s.ipynb' % name, d+'/').json()
210 nb = self.api.read('%s.ipynb' % name, d+'/').json()
211 self.assertEqual(nb['name'], u'%s.ipynb' % name)
211 self.assertEqual(nb['name'], u'%s.ipynb' % name)
212 self.assertEqual(nb['type'], 'notebook')
212 self.assertEqual(nb['type'], 'notebook')
213 self.assertIn('content', nb)
213 self.assertIn('content', nb)
214 self.assertEqual(nb['format'], 'json')
214 self.assertEqual(nb['format'], 'json')
215 self.assertIn('content', nb)
215 self.assertIn('content', nb)
216 self.assertIn('metadata', nb['content'])
216 self.assertIn('metadata', nb['content'])
217 self.assertIsInstance(nb['content']['metadata'], dict)
217 self.assertIsInstance(nb['content']['metadata'], dict)
218
218
219 def test_get_contents_no_such_file(self):
219 def test_get_contents_no_such_file(self):
220 # Name that doesn't exist - should be a 404
220 # Name that doesn't exist - should be a 404
221 with assert_http_error(404):
221 with assert_http_error(404):
222 self.api.read('q.ipynb', 'foo')
222 self.api.read('q.ipynb', 'foo')
223
223
224 def test_get_text_file_contents(self):
224 def test_get_text_file_contents(self):
225 for d, name in self.dirs_nbs:
225 for d, name in self.dirs_nbs:
226 model = self.api.read(u'%s.txt' % name, d+'/').json()
226 model = self.api.read(u'%s.txt' % name, d+'/').json()
227 self.assertEqual(model['name'], u'%s.txt' % name)
227 self.assertEqual(model['name'], u'%s.txt' % name)
228 self.assertIn('content', model)
228 self.assertIn('content', model)
229 self.assertEqual(model['format'], 'text')
229 self.assertEqual(model['format'], 'text')
230 self.assertEqual(model['type'], 'file')
230 self.assertEqual(model['type'], 'file')
231 self.assertEqual(model['content'], self._txt_for_name(name))
231 self.assertEqual(model['content'], self._txt_for_name(name))
232
232
233 # Name that doesn't exist - should be a 404
233 # Name that doesn't exist - should be a 404
234 with assert_http_error(404):
234 with assert_http_error(404):
235 self.api.read('q.txt', 'foo')
235 self.api.read('q.txt', 'foo')
236
236
237 def test_get_binary_file_contents(self):
237 def test_get_binary_file_contents(self):
238 for d, name in self.dirs_nbs:
238 for d, name in self.dirs_nbs:
239 model = self.api.read(u'%s.blob' % name, d+'/').json()
239 model = self.api.read(u'%s.blob' % name, d+'/').json()
240 self.assertEqual(model['name'], u'%s.blob' % name)
240 self.assertEqual(model['name'], u'%s.blob' % name)
241 self.assertIn('content', model)
241 self.assertIn('content', model)
242 self.assertEqual(model['format'], 'base64')
242 self.assertEqual(model['format'], 'base64')
243 self.assertEqual(model['type'], 'file')
243 self.assertEqual(model['type'], 'file')
244 b64_data = base64.encodestring(self._blob_for_name(name)).decode('ascii')
244 b64_data = base64.encodestring(self._blob_for_name(name)).decode('ascii')
245 self.assertEqual(model['content'], b64_data)
245 self.assertEqual(model['content'], b64_data)
246
246
247 # Name that doesn't exist - should be a 404
247 # Name that doesn't exist - should be a 404
248 with assert_http_error(404):
248 with assert_http_error(404):
249 self.api.read('q.txt', 'foo')
249 self.api.read('q.txt', 'foo')
250
250
251 def _check_created(self, resp, name, path, type='notebook'):
251 def _check_created(self, resp, name, path, type='notebook'):
252 self.assertEqual(resp.status_code, 201)
252 self.assertEqual(resp.status_code, 201)
253 location_header = py3compat.str_to_unicode(resp.headers['Location'])
253 location_header = py3compat.str_to_unicode(resp.headers['Location'])
254 self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path, name)))
254 self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path, name)))
255 rjson = resp.json()
255 rjson = resp.json()
256 self.assertEqual(rjson['name'], name)
256 self.assertEqual(rjson['name'], name)
257 self.assertEqual(rjson['path'], path)
257 self.assertEqual(rjson['path'], path)
258 self.assertEqual(rjson['type'], type)
258 self.assertEqual(rjson['type'], type)
259 isright = os.path.isdir if type == 'directory' else os.path.isfile
259 isright = os.path.isdir if type == 'directory' else os.path.isfile
260 assert isright(pjoin(
260 assert isright(pjoin(
261 self.notebook_dir.name,
261 self.notebook_dir.name,
262 path.replace('/', os.sep),
262 path.replace('/', os.sep),
263 name,
263 name,
264 ))
264 ))
265
265
266 def test_create_untitled(self):
266 def test_create_untitled(self):
267 resp = self.api.create_untitled(path=u'Γ₯ b')
267 resp = self.api.create_untitled(path=u'Γ₯ b')
268 self._check_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
268 self._check_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
269
269
270 # Second time
270 # Second time
271 resp = self.api.create_untitled(path=u'Γ₯ b')
271 resp = self.api.create_untitled(path=u'Γ₯ b')
272 self._check_created(resp, 'Untitled1.ipynb', u'Γ₯ b')
272 self._check_created(resp, 'Untitled1.ipynb', u'Γ₯ b')
273
273
274 # And two directories down
274 # And two directories down
275 resp = self.api.create_untitled(path='foo/bar')
275 resp = self.api.create_untitled(path='foo/bar')
276 self._check_created(resp, 'Untitled0.ipynb', 'foo/bar')
276 self._check_created(resp, 'Untitled0.ipynb', 'foo/bar')
277
277
278 def test_create_untitled_txt(self):
278 def test_create_untitled_txt(self):
279 resp = self.api.create_untitled(path='foo/bar', ext='.txt')
279 resp = self.api.create_untitled(path='foo/bar', ext='.txt')
280 self._check_created(resp, 'untitled0.txt', 'foo/bar', type='file')
280 self._check_created(resp, 'untitled0.txt', 'foo/bar', type='file')
281
281
282 resp = self.api.read(path='foo/bar', name='untitled0.txt')
282 resp = self.api.read(path='foo/bar', name='untitled0.txt')
283 model = resp.json()
283 model = resp.json()
284 self.assertEqual(model['type'], 'file')
284 self.assertEqual(model['type'], 'file')
285 self.assertEqual(model['format'], 'text')
285 self.assertEqual(model['format'], 'text')
286 self.assertEqual(model['content'], '')
286 self.assertEqual(model['content'], '')
287
287
288 def test_upload_untitled(self):
288 def test_upload_untitled(self):
289 nb = new_notebook(name='Upload test')
289 nb = new_notebook()
290 nbmodel = {'content': nb, 'type': 'notebook'}
290 nbmodel = {'content': nb, 'type': 'notebook'}
291 resp = self.api.upload_untitled(path=u'Γ₯ b',
291 resp = self.api.upload_untitled(path=u'Γ₯ b',
292 body=json.dumps(nbmodel))
292 body=json.dumps(nbmodel))
293 self._check_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
293 self._check_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
294
294
295 def test_upload(self):
295 def test_upload(self):
296 nb = new_notebook(name=u'ignored')
296 nb = new_notebook()
297 nbmodel = {'content': nb, 'type': 'notebook'}
297 nbmodel = {'content': nb, 'type': 'notebook'}
298 resp = self.api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
298 resp = self.api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
299 body=json.dumps(nbmodel))
299 body=json.dumps(nbmodel))
300 self._check_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
300 self._check_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
301
301
302 def test_mkdir(self):
302 def test_mkdir(self):
303 resp = self.api.mkdir(u'New βˆ‚ir', path=u'Γ₯ b')
303 resp = self.api.mkdir(u'New βˆ‚ir', path=u'Γ₯ b')
304 self._check_created(resp, u'New βˆ‚ir', u'Γ₯ b', type='directory')
304 self._check_created(resp, u'New βˆ‚ir', u'Γ₯ b', type='directory')
305
305
306 def test_mkdir_hidden_400(self):
306 def test_mkdir_hidden_400(self):
307 with assert_http_error(400):
307 with assert_http_error(400):
308 resp = self.api.mkdir(u'.hidden', path=u'Γ₯ b')
308 resp = self.api.mkdir(u'.hidden', path=u'Γ₯ b')
309
309
310 def test_upload_txt(self):
310 def test_upload_txt(self):
311 body = u'ΓΌnicode tΓ©xt'
311 body = u'ΓΌnicode tΓ©xt'
312 model = {
312 model = {
313 'content' : body,
313 'content' : body,
314 'format' : 'text',
314 'format' : 'text',
315 'type' : 'file',
315 'type' : 'file',
316 }
316 }
317 resp = self.api.upload(u'Upload tΓ©st.txt', path=u'Γ₯ b',
317 resp = self.api.upload(u'Upload tΓ©st.txt', path=u'Γ₯ b',
318 body=json.dumps(model))
318 body=json.dumps(model))
319
319
320 # check roundtrip
320 # check roundtrip
321 resp = self.api.read(path=u'Γ₯ b', name=u'Upload tΓ©st.txt')
321 resp = self.api.read(path=u'Γ₯ b', name=u'Upload tΓ©st.txt')
322 model = resp.json()
322 model = resp.json()
323 self.assertEqual(model['type'], 'file')
323 self.assertEqual(model['type'], 'file')
324 self.assertEqual(model['format'], 'text')
324 self.assertEqual(model['format'], 'text')
325 self.assertEqual(model['content'], body)
325 self.assertEqual(model['content'], body)
326
326
327 def test_upload_b64(self):
327 def test_upload_b64(self):
328 body = b'\xFFblob'
328 body = b'\xFFblob'
329 b64body = base64.encodestring(body).decode('ascii')
329 b64body = base64.encodestring(body).decode('ascii')
330 model = {
330 model = {
331 'content' : b64body,
331 'content' : b64body,
332 'format' : 'base64',
332 'format' : 'base64',
333 'type' : 'file',
333 'type' : 'file',
334 }
334 }
335 resp = self.api.upload(u'Upload tΓ©st.blob', path=u'Γ₯ b',
335 resp = self.api.upload(u'Upload tΓ©st.blob', path=u'Γ₯ b',
336 body=json.dumps(model))
336 body=json.dumps(model))
337
337
338 # check roundtrip
338 # check roundtrip
339 resp = self.api.read(path=u'Γ₯ b', name=u'Upload tΓ©st.blob')
339 resp = self.api.read(path=u'Γ₯ b', name=u'Upload tΓ©st.blob')
340 model = resp.json()
340 model = resp.json()
341 self.assertEqual(model['type'], 'file')
341 self.assertEqual(model['type'], 'file')
342 self.assertEqual(model['format'], 'base64')
342 self.assertEqual(model['format'], 'base64')
343 decoded = base64.decodestring(model['content'].encode('ascii'))
343 decoded = base64.decodestring(model['content'].encode('ascii'))
344 self.assertEqual(decoded, body)
344 self.assertEqual(decoded, body)
345
345
346 def test_upload_v2(self):
346 def test_upload_v2(self):
347 nb = v2.new_notebook()
347 nb = v2.new_notebook()
348 ws = v2.new_worksheet()
348 ws = v2.new_worksheet()
349 nb.worksheets.append(ws)
349 nb.worksheets.append(ws)
350 ws.cells.append(v2.new_code_cell(input='print("hi")'))
350 ws.cells.append(v2.new_code_cell(input='print("hi")'))
351 nbmodel = {'content': nb, 'type': 'notebook'}
351 nbmodel = {'content': nb, 'type': 'notebook'}
352 resp = self.api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
352 resp = self.api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
353 body=json.dumps(nbmodel))
353 body=json.dumps(nbmodel))
354 self._check_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
354 self._check_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
355 resp = self.api.read(u'Upload tΓ©st.ipynb', u'Γ₯ b')
355 resp = self.api.read(u'Upload tΓ©st.ipynb', u'Γ₯ b')
356 data = resp.json()
356 data = resp.json()
357 self.assertEqual(data['content']['nbformat'], current.nbformat)
357 self.assertEqual(data['content']['nbformat'], current.nbformat)
358 self.assertEqual(data['content']['orig_nbformat'], 2)
359
358
360 def test_copy_untitled(self):
359 def test_copy_untitled(self):
361 resp = self.api.copy_untitled(u'Γ§ d.ipynb', path=u'Γ₯ b')
360 resp = self.api.copy_untitled(u'Γ§ d.ipynb', path=u'Γ₯ b')
362 self._check_created(resp, u'Γ§ d-Copy0.ipynb', u'Γ₯ b')
361 self._check_created(resp, u'Γ§ d-Copy0.ipynb', u'Γ₯ b')
363
362
364 def test_copy(self):
363 def test_copy(self):
365 resp = self.api.copy(u'Γ§ d.ipynb', u'cΓΈpy.ipynb', path=u'Γ₯ b')
364 resp = self.api.copy(u'Γ§ d.ipynb', u'cΓΈpy.ipynb', path=u'Γ₯ b')
366 self._check_created(resp, u'cΓΈpy.ipynb', u'Γ₯ b')
365 self._check_created(resp, u'cΓΈpy.ipynb', u'Γ₯ b')
367
366
368 def test_copy_path(self):
367 def test_copy_path(self):
369 resp = self.api.copy(u'foo/a.ipynb', u'cΓΈpyfoo.ipynb', path=u'Γ₯ b')
368 resp = self.api.copy(u'foo/a.ipynb', u'cΓΈpyfoo.ipynb', path=u'Γ₯ b')
370 self._check_created(resp, u'cΓΈpyfoo.ipynb', u'Γ₯ b')
369 self._check_created(resp, u'cΓΈpyfoo.ipynb', u'Γ₯ b')
371
370
372 def test_copy_dir_400(self):
371 def test_copy_dir_400(self):
373 # can't copy directories
372 # can't copy directories
374 with assert_http_error(400):
373 with assert_http_error(400):
375 resp = self.api.copy(u'Γ₯ b', u'Γ₯ c')
374 resp = self.api.copy(u'Γ₯ b', u'Γ₯ c')
376
375
377 def test_delete(self):
376 def test_delete(self):
378 for d, name in self.dirs_nbs:
377 for d, name in self.dirs_nbs:
379 resp = self.api.delete('%s.ipynb' % name, d)
378 resp = self.api.delete('%s.ipynb' % name, d)
380 self.assertEqual(resp.status_code, 204)
379 self.assertEqual(resp.status_code, 204)
381
380
382 for d in self.dirs + ['/']:
381 for d in self.dirs + ['/']:
383 nbs = notebooks_only(self.api.list(d).json())
382 nbs = notebooks_only(self.api.list(d).json())
384 self.assertEqual(len(nbs), 0)
383 self.assertEqual(len(nbs), 0)
385
384
386 def test_delete_dirs(self):
385 def test_delete_dirs(self):
387 # depth-first delete everything, so we don't try to delete empty directories
386 # depth-first delete everything, so we don't try to delete empty directories
388 for name in sorted(self.dirs + ['/'], key=len, reverse=True):
387 for name in sorted(self.dirs + ['/'], key=len, reverse=True):
389 listing = self.api.list(name).json()['content']
388 listing = self.api.list(name).json()['content']
390 for model in listing:
389 for model in listing:
391 self.api.delete(model['name'], model['path'])
390 self.api.delete(model['name'], model['path'])
392 listing = self.api.list('/').json()['content']
391 listing = self.api.list('/').json()['content']
393 self.assertEqual(listing, [])
392 self.assertEqual(listing, [])
394
393
395 def test_delete_non_empty_dir(self):
394 def test_delete_non_empty_dir(self):
396 """delete non-empty dir raises 400"""
395 """delete non-empty dir raises 400"""
397 with assert_http_error(400):
396 with assert_http_error(400):
398 self.api.delete(u'Γ₯ b')
397 self.api.delete(u'Γ₯ b')
399
398
400 def test_rename(self):
399 def test_rename(self):
401 resp = self.api.rename('a.ipynb', 'foo', 'z.ipynb')
400 resp = self.api.rename('a.ipynb', 'foo', 'z.ipynb')
402 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
401 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
403 self.assertEqual(resp.json()['name'], 'z.ipynb')
402 self.assertEqual(resp.json()['name'], 'z.ipynb')
404 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
403 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
405
404
406 nbs = notebooks_only(self.api.list('foo').json())
405 nbs = notebooks_only(self.api.list('foo').json())
407 nbnames = set(n['name'] for n in nbs)
406 nbnames = set(n['name'] for n in nbs)
408 self.assertIn('z.ipynb', nbnames)
407 self.assertIn('z.ipynb', nbnames)
409 self.assertNotIn('a.ipynb', nbnames)
408 self.assertNotIn('a.ipynb', nbnames)
410
409
411 def test_rename_existing(self):
410 def test_rename_existing(self):
412 with assert_http_error(409):
411 with assert_http_error(409):
413 self.api.rename('a.ipynb', 'foo', 'b.ipynb')
412 self.api.rename('a.ipynb', 'foo', 'b.ipynb')
414
413
415 def test_save(self):
414 def test_save(self):
416 resp = self.api.read('a.ipynb', 'foo')
415 resp = self.api.read('a.ipynb', 'foo')
417 nbcontent = json.loads(resp.text)['content']
416 nbcontent = json.loads(resp.text)['content']
418 nb = to_notebook_json(nbcontent)
417 nb = to_notebook_json(nbcontent)
419 ws = new_worksheet()
418 nb.cells.append(new_heading_cell(u'Created by test Β³'))
420 nb.worksheets = [ws]
421 ws.cells.append(new_heading_cell(u'Created by test Β³'))
422
419
423 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'}
420 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'}
424 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
421 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
425
422
426 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
423 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
427 with io.open(nbfile, 'r', encoding='utf-8') as f:
424 with io.open(nbfile, 'r', encoding='utf-8') as f:
428 newnb = read(f, format='ipynb')
425 newnb = read(f, format='ipynb')
429 self.assertEqual(newnb.worksheets[0].cells[0].source,
426 self.assertEqual(newnb.cells[0].source,
430 u'Created by test Β³')
427 u'Created by test Β³')
431 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
428 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
432 newnb = to_notebook_json(nbcontent)
429 newnb = to_notebook_json(nbcontent)
433 self.assertEqual(newnb.worksheets[0].cells[0].source,
430 self.assertEqual(newnb.cells[0].source,
434 u'Created by test Β³')
431 u'Created by test Β³')
435
432
436 # Save and rename
433 # Save and rename
437 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb, 'type': 'notebook'}
434 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb, 'type': 'notebook'}
438 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
435 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
439 saved = resp.json()
436 saved = resp.json()
440 self.assertEqual(saved['name'], 'a2.ipynb')
437 self.assertEqual(saved['name'], 'a2.ipynb')
441 self.assertEqual(saved['path'], 'foo/bar')
438 self.assertEqual(saved['path'], 'foo/bar')
442 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
439 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
443 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
440 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
444 with assert_http_error(404):
441 with assert_http_error(404):
445 self.api.read('a.ipynb', 'foo')
442 self.api.read('a.ipynb', 'foo')
446
443
447 def test_checkpoints(self):
444 def test_checkpoints(self):
448 resp = self.api.read('a.ipynb', 'foo')
445 resp = self.api.read('a.ipynb', 'foo')
449 r = self.api.new_checkpoint('a.ipynb', 'foo')
446 r = self.api.new_checkpoint('a.ipynb', 'foo')
450 self.assertEqual(r.status_code, 201)
447 self.assertEqual(r.status_code, 201)
451 cp1 = r.json()
448 cp1 = r.json()
452 self.assertEqual(set(cp1), {'id', 'last_modified'})
449 self.assertEqual(set(cp1), {'id', 'last_modified'})
453 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
450 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
454
451
455 # Modify it
452 # Modify it
456 nbcontent = json.loads(resp.text)['content']
453 nbcontent = json.loads(resp.text)['content']
457 nb = to_notebook_json(nbcontent)
454 nb = to_notebook_json(nbcontent)
458 ws = new_worksheet()
459 nb.worksheets = [ws]
460 hcell = new_heading_cell('Created by test')
455 hcell = new_heading_cell('Created by test')
461 ws.cells.append(hcell)
456 nb.cells.append(hcell)
462 # Save
457 # Save
463 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'}
458 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'}
464 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
459 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
465
460
466 # List checkpoints
461 # List checkpoints
467 cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
462 cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
468 self.assertEqual(cps, [cp1])
463 self.assertEqual(cps, [cp1])
469
464
470 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
465 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
471 nb = to_notebook_json(nbcontent)
466 nb = to_notebook_json(nbcontent)
472 self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test')
467 self.assertEqual(nb.cells[0].source, 'Created by test')
473
468
474 # Restore cp1
469 # Restore cp1
475 r = self.api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
470 r = self.api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
476 self.assertEqual(r.status_code, 204)
471 self.assertEqual(r.status_code, 204)
477 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
472 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
478 nb = to_notebook_json(nbcontent)
473 nb = to_notebook_json(nbcontent)
479 self.assertEqual(nb.worksheets, [])
474 self.assertEqual(nb.cells, [])
480
475
481 # Delete cp1
476 # Delete cp1
482 r = self.api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
477 r = self.api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
483 self.assertEqual(r.status_code, 204)
478 self.assertEqual(r.status_code, 204)
484 cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
479 cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
485 self.assertEqual(cps, [])
480 self.assertEqual(cps, [])
@@ -1,334 +1,332 b''
1 # coding: utf-8
1 # coding: utf-8
2 """Tests for the notebook manager."""
2 """Tests for the notebook manager."""
3 from __future__ import print_function
3 from __future__ import print_function
4
4
5 import logging
5 import logging
6 import os
6 import os
7
7
8 from tornado.web import HTTPError
8 from tornado.web import HTTPError
9 from unittest import TestCase
9 from unittest import TestCase
10 from tempfile import NamedTemporaryFile
10 from tempfile import NamedTemporaryFile
11
11
12 from IPython.nbformat import current
12 from IPython.nbformat import current
13
13
14 from IPython.utils.tempdir import TemporaryDirectory
14 from IPython.utils.tempdir import TemporaryDirectory
15 from IPython.utils.traitlets import TraitError
15 from IPython.utils.traitlets import TraitError
16 from IPython.html.utils import url_path_join
16 from IPython.html.utils import url_path_join
17 from IPython.testing import decorators as dec
17 from IPython.testing import decorators as dec
18
18
19 from ..filemanager import FileContentsManager
19 from ..filemanager import FileContentsManager
20 from ..manager import ContentsManager
20 from ..manager import ContentsManager
21
21
22
22
23 class TestFileContentsManager(TestCase):
23 class TestFileContentsManager(TestCase):
24
24
25 def test_root_dir(self):
25 def test_root_dir(self):
26 with TemporaryDirectory() as td:
26 with TemporaryDirectory() as td:
27 fm = FileContentsManager(root_dir=td)
27 fm = FileContentsManager(root_dir=td)
28 self.assertEqual(fm.root_dir, td)
28 self.assertEqual(fm.root_dir, td)
29
29
30 def test_missing_root_dir(self):
30 def test_missing_root_dir(self):
31 with TemporaryDirectory() as td:
31 with TemporaryDirectory() as td:
32 root = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
32 root = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
33 self.assertRaises(TraitError, FileContentsManager, root_dir=root)
33 self.assertRaises(TraitError, FileContentsManager, root_dir=root)
34
34
35 def test_invalid_root_dir(self):
35 def test_invalid_root_dir(self):
36 with NamedTemporaryFile() as tf:
36 with NamedTemporaryFile() as tf:
37 self.assertRaises(TraitError, FileContentsManager, root_dir=tf.name)
37 self.assertRaises(TraitError, FileContentsManager, root_dir=tf.name)
38
38
39 def test_get_os_path(self):
39 def test_get_os_path(self):
40 # full filesystem path should be returned with correct operating system
40 # full filesystem path should be returned with correct operating system
41 # separators.
41 # separators.
42 with TemporaryDirectory() as td:
42 with TemporaryDirectory() as td:
43 root = td
43 root = td
44 fm = FileContentsManager(root_dir=root)
44 fm = FileContentsManager(root_dir=root)
45 path = fm._get_os_path('test.ipynb', '/path/to/notebook/')
45 path = fm._get_os_path('test.ipynb', '/path/to/notebook/')
46 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
46 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
47 fs_path = os.path.join(fm.root_dir, *rel_path_list)
47 fs_path = os.path.join(fm.root_dir, *rel_path_list)
48 self.assertEqual(path, fs_path)
48 self.assertEqual(path, fs_path)
49
49
50 fm = FileContentsManager(root_dir=root)
50 fm = FileContentsManager(root_dir=root)
51 path = fm._get_os_path('test.ipynb')
51 path = fm._get_os_path('test.ipynb')
52 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
52 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
53 self.assertEqual(path, fs_path)
53 self.assertEqual(path, fs_path)
54
54
55 fm = FileContentsManager(root_dir=root)
55 fm = FileContentsManager(root_dir=root)
56 path = fm._get_os_path('test.ipynb', '////')
56 path = fm._get_os_path('test.ipynb', '////')
57 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
57 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
58 self.assertEqual(path, fs_path)
58 self.assertEqual(path, fs_path)
59
59
60 def test_checkpoint_subdir(self):
60 def test_checkpoint_subdir(self):
61 subd = u'sub βˆ‚ir'
61 subd = u'sub βˆ‚ir'
62 cp_name = 'test-cp.ipynb'
62 cp_name = 'test-cp.ipynb'
63 with TemporaryDirectory() as td:
63 with TemporaryDirectory() as td:
64 root = td
64 root = td
65 os.mkdir(os.path.join(td, subd))
65 os.mkdir(os.path.join(td, subd))
66 fm = FileContentsManager(root_dir=root)
66 fm = FileContentsManager(root_dir=root)
67 cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb', '/')
67 cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb', '/')
68 cp_subdir = fm.get_checkpoint_path('cp', 'test.ipynb', '/%s/' % subd)
68 cp_subdir = fm.get_checkpoint_path('cp', 'test.ipynb', '/%s/' % subd)
69 self.assertNotEqual(cp_dir, cp_subdir)
69 self.assertNotEqual(cp_dir, cp_subdir)
70 self.assertEqual(cp_dir, os.path.join(root, fm.checkpoint_dir, cp_name))
70 self.assertEqual(cp_dir, os.path.join(root, fm.checkpoint_dir, cp_name))
71 self.assertEqual(cp_subdir, os.path.join(root, subd, fm.checkpoint_dir, cp_name))
71 self.assertEqual(cp_subdir, os.path.join(root, subd, fm.checkpoint_dir, cp_name))
72
72
73
73
74 class TestContentsManager(TestCase):
74 class TestContentsManager(TestCase):
75
75
76 def setUp(self):
76 def setUp(self):
77 self._temp_dir = TemporaryDirectory()
77 self._temp_dir = TemporaryDirectory()
78 self.td = self._temp_dir.name
78 self.td = self._temp_dir.name
79 self.contents_manager = FileContentsManager(
79 self.contents_manager = FileContentsManager(
80 root_dir=self.td,
80 root_dir=self.td,
81 log=logging.getLogger()
81 log=logging.getLogger()
82 )
82 )
83
83
84 def tearDown(self):
84 def tearDown(self):
85 self._temp_dir.cleanup()
85 self._temp_dir.cleanup()
86
86
87 def make_dir(self, abs_path, rel_path):
87 def make_dir(self, abs_path, rel_path):
88 """make subdirectory, rel_path is the relative path
88 """make subdirectory, rel_path is the relative path
89 to that directory from the location where the server started"""
89 to that directory from the location where the server started"""
90 os_path = os.path.join(abs_path, rel_path)
90 os_path = os.path.join(abs_path, rel_path)
91 try:
91 try:
92 os.makedirs(os_path)
92 os.makedirs(os_path)
93 except OSError:
93 except OSError:
94 print("Directory already exists: %r" % os_path)
94 print("Directory already exists: %r" % os_path)
95 return os_path
95 return os_path
96
96
97 def add_code_cell(self, nb):
97 def add_code_cell(self, nb):
98 output = current.new_output("display_data", output_javascript="alert('hi');")
98 output = current.new_output("display_data", {'application/javascript': "alert('hi');"})
99 cell = current.new_code_cell("print('hi')", outputs=[output])
99 cell = current.new_code_cell("print('hi')", outputs=[output])
100 if not nb.worksheets:
100 nb.cells.append(cell)
101 nb.worksheets.append(current.new_worksheet())
102 nb.worksheets[0].cells.append(cell)
103
101
104 def new_notebook(self):
102 def new_notebook(self):
105 cm = self.contents_manager
103 cm = self.contents_manager
106 model = cm.create_file()
104 model = cm.create_file()
107 name = model['name']
105 name = model['name']
108 path = model['path']
106 path = model['path']
109
107
110 full_model = cm.get_model(name, path)
108 full_model = cm.get_model(name, path)
111 nb = full_model['content']
109 nb = full_model['content']
112 self.add_code_cell(nb)
110 self.add_code_cell(nb)
113
111
114 cm.save(full_model, name, path)
112 cm.save(full_model, name, path)
115 return nb, name, path
113 return nb, name, path
116
114
117 def test_create_file(self):
115 def test_create_file(self):
118 cm = self.contents_manager
116 cm = self.contents_manager
119 # Test in root directory
117 # Test in root directory
120 model = cm.create_file()
118 model = cm.create_file()
121 assert isinstance(model, dict)
119 assert isinstance(model, dict)
122 self.assertIn('name', model)
120 self.assertIn('name', model)
123 self.assertIn('path', model)
121 self.assertIn('path', model)
124 self.assertEqual(model['name'], 'Untitled0.ipynb')
122 self.assertEqual(model['name'], 'Untitled0.ipynb')
125 self.assertEqual(model['path'], '')
123 self.assertEqual(model['path'], '')
126
124
127 # Test in sub-directory
125 # Test in sub-directory
128 sub_dir = '/foo/'
126 sub_dir = '/foo/'
129 self.make_dir(cm.root_dir, 'foo')
127 self.make_dir(cm.root_dir, 'foo')
130 model = cm.create_file(None, sub_dir)
128 model = cm.create_file(None, sub_dir)
131 assert isinstance(model, dict)
129 assert isinstance(model, dict)
132 self.assertIn('name', model)
130 self.assertIn('name', model)
133 self.assertIn('path', model)
131 self.assertIn('path', model)
134 self.assertEqual(model['name'], 'Untitled0.ipynb')
132 self.assertEqual(model['name'], 'Untitled0.ipynb')
135 self.assertEqual(model['path'], sub_dir.strip('/'))
133 self.assertEqual(model['path'], sub_dir.strip('/'))
136
134
137 def test_get(self):
135 def test_get(self):
138 cm = self.contents_manager
136 cm = self.contents_manager
139 # Create a notebook
137 # Create a notebook
140 model = cm.create_file()
138 model = cm.create_file()
141 name = model['name']
139 name = model['name']
142 path = model['path']
140 path = model['path']
143
141
144 # Check that we 'get' on the notebook we just created
142 # Check that we 'get' on the notebook we just created
145 model2 = cm.get_model(name, path)
143 model2 = cm.get_model(name, path)
146 assert isinstance(model2, dict)
144 assert isinstance(model2, dict)
147 self.assertIn('name', model2)
145 self.assertIn('name', model2)
148 self.assertIn('path', model2)
146 self.assertIn('path', model2)
149 self.assertEqual(model['name'], name)
147 self.assertEqual(model['name'], name)
150 self.assertEqual(model['path'], path)
148 self.assertEqual(model['path'], path)
151
149
152 # Test in sub-directory
150 # Test in sub-directory
153 sub_dir = '/foo/'
151 sub_dir = '/foo/'
154 self.make_dir(cm.root_dir, 'foo')
152 self.make_dir(cm.root_dir, 'foo')
155 model = cm.create_file(None, sub_dir)
153 model = cm.create_file(None, sub_dir)
156 model2 = cm.get_model(name, sub_dir)
154 model2 = cm.get_model(name, sub_dir)
157 assert isinstance(model2, dict)
155 assert isinstance(model2, dict)
158 self.assertIn('name', model2)
156 self.assertIn('name', model2)
159 self.assertIn('path', model2)
157 self.assertIn('path', model2)
160 self.assertIn('content', model2)
158 self.assertIn('content', model2)
161 self.assertEqual(model2['name'], 'Untitled0.ipynb')
159 self.assertEqual(model2['name'], 'Untitled0.ipynb')
162 self.assertEqual(model2['path'], sub_dir.strip('/'))
160 self.assertEqual(model2['path'], sub_dir.strip('/'))
163
161
164 @dec.skip_win32
162 @dec.skip_win32
165 def test_bad_symlink(self):
163 def test_bad_symlink(self):
166 cm = self.contents_manager
164 cm = self.contents_manager
167 path = 'test bad symlink'
165 path = 'test bad symlink'
168 os_path = self.make_dir(cm.root_dir, path)
166 os_path = self.make_dir(cm.root_dir, path)
169
167
170 file_model = cm.create_file(path=path, ext='.txt')
168 file_model = cm.create_file(path=path, ext='.txt')
171
169
172 # create a broken symlink
170 # create a broken symlink
173 os.symlink("target", os.path.join(os_path, "bad symlink"))
171 os.symlink("target", os.path.join(os_path, "bad symlink"))
174 model = cm.get_model(path)
172 model = cm.get_model(path)
175 self.assertEqual(model['content'], [file_model])
173 self.assertEqual(model['content'], [file_model])
176
174
177 @dec.skip_win32
175 @dec.skip_win32
178 def test_good_symlink(self):
176 def test_good_symlink(self):
179 cm = self.contents_manager
177 cm = self.contents_manager
180 path = 'test good symlink'
178 path = 'test good symlink'
181 os_path = self.make_dir(cm.root_dir, path)
179 os_path = self.make_dir(cm.root_dir, path)
182
180
183 file_model = cm.create_file(path=path, ext='.txt')
181 file_model = cm.create_file(path=path, ext='.txt')
184
182
185 # create a good symlink
183 # create a good symlink
186 os.symlink(file_model['name'], os.path.join(os_path, "good symlink"))
184 os.symlink(file_model['name'], os.path.join(os_path, "good symlink"))
187 symlink_model = cm.get_model(name="good symlink", path=path, content=False)
185 symlink_model = cm.get_model(name="good symlink", path=path, content=False)
188
186
189 dir_model = cm.get_model(path)
187 dir_model = cm.get_model(path)
190 self.assertEqual(
188 self.assertEqual(
191 sorted(dir_model['content'], key=lambda x: x['name']),
189 sorted(dir_model['content'], key=lambda x: x['name']),
192 [symlink_model, file_model],
190 [symlink_model, file_model],
193 )
191 )
194
192
195 def test_update(self):
193 def test_update(self):
196 cm = self.contents_manager
194 cm = self.contents_manager
197 # Create a notebook
195 # Create a notebook
198 model = cm.create_file()
196 model = cm.create_file()
199 name = model['name']
197 name = model['name']
200 path = model['path']
198 path = model['path']
201
199
202 # Change the name in the model for rename
200 # Change the name in the model for rename
203 model['name'] = 'test.ipynb'
201 model['name'] = 'test.ipynb'
204 model = cm.update(model, name, path)
202 model = cm.update(model, name, path)
205 assert isinstance(model, dict)
203 assert isinstance(model, dict)
206 self.assertIn('name', model)
204 self.assertIn('name', model)
207 self.assertIn('path', model)
205 self.assertIn('path', model)
208 self.assertEqual(model['name'], 'test.ipynb')
206 self.assertEqual(model['name'], 'test.ipynb')
209
207
210 # Make sure the old name is gone
208 # Make sure the old name is gone
211 self.assertRaises(HTTPError, cm.get_model, name, path)
209 self.assertRaises(HTTPError, cm.get_model, name, path)
212
210
213 # Test in sub-directory
211 # Test in sub-directory
214 # Create a directory and notebook in that directory
212 # Create a directory and notebook in that directory
215 sub_dir = '/foo/'
213 sub_dir = '/foo/'
216 self.make_dir(cm.root_dir, 'foo')
214 self.make_dir(cm.root_dir, 'foo')
217 model = cm.create_file(None, sub_dir)
215 model = cm.create_file(None, sub_dir)
218 name = model['name']
216 name = model['name']
219 path = model['path']
217 path = model['path']
220
218
221 # Change the name in the model for rename
219 # Change the name in the model for rename
222 model['name'] = 'test_in_sub.ipynb'
220 model['name'] = 'test_in_sub.ipynb'
223 model = cm.update(model, name, path)
221 model = cm.update(model, name, path)
224 assert isinstance(model, dict)
222 assert isinstance(model, dict)
225 self.assertIn('name', model)
223 self.assertIn('name', model)
226 self.assertIn('path', model)
224 self.assertIn('path', model)
227 self.assertEqual(model['name'], 'test_in_sub.ipynb')
225 self.assertEqual(model['name'], 'test_in_sub.ipynb')
228 self.assertEqual(model['path'], sub_dir.strip('/'))
226 self.assertEqual(model['path'], sub_dir.strip('/'))
229
227
230 # Make sure the old name is gone
228 # Make sure the old name is gone
231 self.assertRaises(HTTPError, cm.get_model, name, path)
229 self.assertRaises(HTTPError, cm.get_model, name, path)
232
230
233 def test_save(self):
231 def test_save(self):
234 cm = self.contents_manager
232 cm = self.contents_manager
235 # Create a notebook
233 # Create a notebook
236 model = cm.create_file()
234 model = cm.create_file()
237 name = model['name']
235 name = model['name']
238 path = model['path']
236 path = model['path']
239
237
240 # Get the model with 'content'
238 # Get the model with 'content'
241 full_model = cm.get_model(name, path)
239 full_model = cm.get_model(name, path)
242
240
243 # Save the notebook
241 # Save the notebook
244 model = cm.save(full_model, name, path)
242 model = cm.save(full_model, name, path)
245 assert isinstance(model, dict)
243 assert isinstance(model, dict)
246 self.assertIn('name', model)
244 self.assertIn('name', model)
247 self.assertIn('path', model)
245 self.assertIn('path', model)
248 self.assertEqual(model['name'], name)
246 self.assertEqual(model['name'], name)
249 self.assertEqual(model['path'], path)
247 self.assertEqual(model['path'], path)
250
248
251 # Test in sub-directory
249 # Test in sub-directory
252 # Create a directory and notebook in that directory
250 # Create a directory and notebook in that directory
253 sub_dir = '/foo/'
251 sub_dir = '/foo/'
254 self.make_dir(cm.root_dir, 'foo')
252 self.make_dir(cm.root_dir, 'foo')
255 model = cm.create_file(None, sub_dir)
253 model = cm.create_file(None, sub_dir)
256 name = model['name']
254 name = model['name']
257 path = model['path']
255 path = model['path']
258 model = cm.get_model(name, path)
256 model = cm.get_model(name, path)
259
257
260 # Change the name in the model for rename
258 # Change the name in the model for rename
261 model = cm.save(model, name, path)
259 model = cm.save(model, name, path)
262 assert isinstance(model, dict)
260 assert isinstance(model, dict)
263 self.assertIn('name', model)
261 self.assertIn('name', model)
264 self.assertIn('path', model)
262 self.assertIn('path', model)
265 self.assertEqual(model['name'], 'Untitled0.ipynb')
263 self.assertEqual(model['name'], 'Untitled0.ipynb')
266 self.assertEqual(model['path'], sub_dir.strip('/'))
264 self.assertEqual(model['path'], sub_dir.strip('/'))
267
265
268 def test_delete(self):
266 def test_delete(self):
269 cm = self.contents_manager
267 cm = self.contents_manager
270 # Create a notebook
268 # Create a notebook
271 nb, name, path = self.new_notebook()
269 nb, name, path = self.new_notebook()
272
270
273 # Delete the notebook
271 # Delete the notebook
274 cm.delete(name, path)
272 cm.delete(name, path)
275
273
276 # Check that a 'get' on the deleted notebook raises and error
274 # Check that a 'get' on the deleted notebook raises and error
277 self.assertRaises(HTTPError, cm.get_model, name, path)
275 self.assertRaises(HTTPError, cm.get_model, name, path)
278
276
279 def test_copy(self):
277 def test_copy(self):
280 cm = self.contents_manager
278 cm = self.contents_manager
281 path = u'Γ₯ b'
279 path = u'Γ₯ b'
282 name = u'nb √.ipynb'
280 name = u'nb √.ipynb'
283 os.mkdir(os.path.join(cm.root_dir, path))
281 os.mkdir(os.path.join(cm.root_dir, path))
284 orig = cm.create_file({'name' : name}, path=path)
282 orig = cm.create_file({'name' : name}, path=path)
285
283
286 # copy with unspecified name
284 # copy with unspecified name
287 copy = cm.copy(name, path=path)
285 copy = cm.copy(name, path=path)
288 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
286 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
289
287
290 # copy with specified name
288 # copy with specified name
291 copy2 = cm.copy(name, u'copy 2.ipynb', path=path)
289 copy2 = cm.copy(name, u'copy 2.ipynb', path=path)
292 self.assertEqual(copy2['name'], u'copy 2.ipynb')
290 self.assertEqual(copy2['name'], u'copy 2.ipynb')
293
291
294 def test_trust_notebook(self):
292 def test_trust_notebook(self):
295 cm = self.contents_manager
293 cm = self.contents_manager
296 nb, name, path = self.new_notebook()
294 nb, name, path = self.new_notebook()
297
295
298 untrusted = cm.get_model(name, path)['content']
296 untrusted = cm.get_model(name, path)['content']
299 assert not cm.notary.check_cells(untrusted)
297 assert not cm.notary.check_cells(untrusted)
300
298
301 # print(untrusted)
299 # print(untrusted)
302 cm.trust_notebook(name, path)
300 cm.trust_notebook(name, path)
303 trusted = cm.get_model(name, path)['content']
301 trusted = cm.get_model(name, path)['content']
304 # print(trusted)
302 # print(trusted)
305 assert cm.notary.check_cells(trusted)
303 assert cm.notary.check_cells(trusted)
306
304
307 def test_mark_trusted_cells(self):
305 def test_mark_trusted_cells(self):
308 cm = self.contents_manager
306 cm = self.contents_manager
309 nb, name, path = self.new_notebook()
307 nb, name, path = self.new_notebook()
310
308
311 cm.mark_trusted_cells(nb, name, path)
309 cm.mark_trusted_cells(nb, name, path)
312 for cell in nb.worksheets[0].cells:
310 for cell in nb.cells:
313 if cell.cell_type == 'code':
311 if cell.cell_type == 'code':
314 assert not cell.metadata.trusted
312 assert not cell.metadata.trusted
315
313
316 cm.trust_notebook(name, path)
314 cm.trust_notebook(name, path)
317 nb = cm.get_model(name, path)['content']
315 nb = cm.get_model(name, path)['content']
318 for cell in nb.worksheets[0].cells:
316 for cell in nb.cells:
319 if cell.cell_type == 'code':
317 if cell.cell_type == 'code':
320 assert cell.metadata.trusted
318 assert cell.metadata.trusted
321
319
322 def test_check_and_sign(self):
320 def test_check_and_sign(self):
323 cm = self.contents_manager
321 cm = self.contents_manager
324 nb, name, path = self.new_notebook()
322 nb, name, path = self.new_notebook()
325
323
326 cm.mark_trusted_cells(nb, name, path)
324 cm.mark_trusted_cells(nb, name, path)
327 cm.check_and_sign(nb, name, path)
325 cm.check_and_sign(nb, name, path)
328 assert not cm.notary.check_signature(nb)
326 assert not cm.notary.check_signature(nb)
329
327
330 cm.trust_notebook(name, path)
328 cm.trust_notebook(name, path)
331 nb = cm.get_model(name, path)['content']
329 nb = cm.get_model(name, path)['content']
332 cm.mark_trusted_cells(nb, name, path)
330 cm.mark_trusted_cells(nb, name, path)
333 cm.check_and_sign(nb, name, path)
331 cm.check_and_sign(nb, name, path)
334 assert cm.notary.check_signature(nb)
332 assert cm.notary.check_signature(nb)
@@ -1,116 +1,116 b''
1 """Test the sessions web service API."""
1 """Test the sessions web service API."""
2
2
3 import errno
3 import errno
4 import io
4 import io
5 import os
5 import os
6 import json
6 import json
7 import requests
7 import requests
8 import shutil
8 import shutil
9
9
10 pjoin = os.path.join
10 pjoin = os.path.join
11
11
12 from IPython.html.utils import url_path_join
12 from IPython.html.utils import url_path_join
13 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
13 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
14 from IPython.nbformat.current import new_notebook, write
14 from IPython.nbformat.current import new_notebook, write
15
15
16 class SessionAPI(object):
16 class SessionAPI(object):
17 """Wrapper for notebook API calls."""
17 """Wrapper for notebook API calls."""
18 def __init__(self, base_url):
18 def __init__(self, base_url):
19 self.base_url = base_url
19 self.base_url = base_url
20
20
21 def _req(self, verb, path, body=None):
21 def _req(self, verb, path, body=None):
22 response = requests.request(verb,
22 response = requests.request(verb,
23 url_path_join(self.base_url, 'api/sessions', path), data=body)
23 url_path_join(self.base_url, 'api/sessions', path), data=body)
24
24
25 if 400 <= response.status_code < 600:
25 if 400 <= response.status_code < 600:
26 try:
26 try:
27 response.reason = response.json()['message']
27 response.reason = response.json()['message']
28 except:
28 except:
29 pass
29 pass
30 response.raise_for_status()
30 response.raise_for_status()
31
31
32 return response
32 return response
33
33
34 def list(self):
34 def list(self):
35 return self._req('GET', '')
35 return self._req('GET', '')
36
36
37 def get(self, id):
37 def get(self, id):
38 return self._req('GET', id)
38 return self._req('GET', id)
39
39
40 def create(self, name, path, kernel_name='python'):
40 def create(self, name, path, kernel_name='python'):
41 body = json.dumps({'notebook': {'name':name, 'path':path},
41 body = json.dumps({'notebook': {'name':name, 'path':path},
42 'kernel': {'name': kernel_name}})
42 'kernel': {'name': kernel_name}})
43 return self._req('POST', '', body)
43 return self._req('POST', '', body)
44
44
45 def modify(self, id, name, path):
45 def modify(self, id, name, path):
46 body = json.dumps({'notebook': {'name':name, 'path':path}})
46 body = json.dumps({'notebook': {'name':name, 'path':path}})
47 return self._req('PATCH', id, body)
47 return self._req('PATCH', id, body)
48
48
49 def delete(self, id):
49 def delete(self, id):
50 return self._req('DELETE', id)
50 return self._req('DELETE', id)
51
51
52 class SessionAPITest(NotebookTestBase):
52 class SessionAPITest(NotebookTestBase):
53 """Test the sessions web service API"""
53 """Test the sessions web service API"""
54 def setUp(self):
54 def setUp(self):
55 nbdir = self.notebook_dir.name
55 nbdir = self.notebook_dir.name
56 try:
56 try:
57 os.mkdir(pjoin(nbdir, 'foo'))
57 os.mkdir(pjoin(nbdir, 'foo'))
58 except OSError as e:
58 except OSError as e:
59 # Deleting the folder in an earlier test may have failed
59 # Deleting the folder in an earlier test may have failed
60 if e.errno != errno.EEXIST:
60 if e.errno != errno.EEXIST:
61 raise
61 raise
62
62
63 with io.open(pjoin(nbdir, 'foo', 'nb1.ipynb'), 'w',
63 with io.open(pjoin(nbdir, 'foo', 'nb1.ipynb'), 'w',
64 encoding='utf-8') as f:
64 encoding='utf-8') as f:
65 nb = new_notebook(name='nb1')
65 nb = new_notebook()
66 write(nb, f, format='ipynb')
66 write(nb, f, format='ipynb')
67
67
68 self.sess_api = SessionAPI(self.base_url())
68 self.sess_api = SessionAPI(self.base_url())
69
69
70 def tearDown(self):
70 def tearDown(self):
71 for session in self.sess_api.list().json():
71 for session in self.sess_api.list().json():
72 self.sess_api.delete(session['id'])
72 self.sess_api.delete(session['id'])
73 shutil.rmtree(pjoin(self.notebook_dir.name, 'foo'),
73 shutil.rmtree(pjoin(self.notebook_dir.name, 'foo'),
74 ignore_errors=True)
74 ignore_errors=True)
75
75
76 def test_create(self):
76 def test_create(self):
77 sessions = self.sess_api.list().json()
77 sessions = self.sess_api.list().json()
78 self.assertEqual(len(sessions), 0)
78 self.assertEqual(len(sessions), 0)
79
79
80 resp = self.sess_api.create('nb1.ipynb', 'foo')
80 resp = self.sess_api.create('nb1.ipynb', 'foo')
81 self.assertEqual(resp.status_code, 201)
81 self.assertEqual(resp.status_code, 201)
82 newsession = resp.json()
82 newsession = resp.json()
83 self.assertIn('id', newsession)
83 self.assertIn('id', newsession)
84 self.assertEqual(newsession['notebook']['name'], 'nb1.ipynb')
84 self.assertEqual(newsession['notebook']['name'], 'nb1.ipynb')
85 self.assertEqual(newsession['notebook']['path'], 'foo')
85 self.assertEqual(newsession['notebook']['path'], 'foo')
86 self.assertEqual(resp.headers['Location'], '/api/sessions/{0}'.format(newsession['id']))
86 self.assertEqual(resp.headers['Location'], '/api/sessions/{0}'.format(newsession['id']))
87
87
88 sessions = self.sess_api.list().json()
88 sessions = self.sess_api.list().json()
89 self.assertEqual(sessions, [newsession])
89 self.assertEqual(sessions, [newsession])
90
90
91 # Retrieve it
91 # Retrieve it
92 sid = newsession['id']
92 sid = newsession['id']
93 got = self.sess_api.get(sid).json()
93 got = self.sess_api.get(sid).json()
94 self.assertEqual(got, newsession)
94 self.assertEqual(got, newsession)
95
95
96 def test_delete(self):
96 def test_delete(self):
97 newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
97 newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
98 sid = newsession['id']
98 sid = newsession['id']
99
99
100 resp = self.sess_api.delete(sid)
100 resp = self.sess_api.delete(sid)
101 self.assertEqual(resp.status_code, 204)
101 self.assertEqual(resp.status_code, 204)
102
102
103 sessions = self.sess_api.list().json()
103 sessions = self.sess_api.list().json()
104 self.assertEqual(sessions, [])
104 self.assertEqual(sessions, [])
105
105
106 with assert_http_error(404):
106 with assert_http_error(404):
107 self.sess_api.get(sid)
107 self.sess_api.get(sid)
108
108
109 def test_modify(self):
109 def test_modify(self):
110 newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
110 newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
111 sid = newsession['id']
111 sid = newsession['id']
112
112
113 changed = self.sess_api.modify(sid, 'nb2.ipynb', '').json()
113 changed = self.sess_api.modify(sid, 'nb2.ipynb', '').json()
114 self.assertEqual(changed['id'], sid)
114 self.assertEqual(changed['id'], sid)
115 self.assertEqual(changed['notebook']['name'], 'nb2.ipynb')
115 self.assertEqual(changed['notebook']['name'], 'nb2.ipynb')
116 self.assertEqual(changed['notebook']['path'], '')
116 self.assertEqual(changed['notebook']['path'], '')
@@ -1,532 +1,526 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3 /**
3 /**
4 *
4 *
5 *
5 *
6 * @module codecell
6 * @module codecell
7 * @namespace codecell
7 * @namespace codecell
8 * @class CodeCell
8 * @class CodeCell
9 */
9 */
10
10
11
11
12 define([
12 define([
13 'base/js/namespace',
13 'base/js/namespace',
14 'jquery',
14 'jquery',
15 'base/js/utils',
15 'base/js/utils',
16 'base/js/keyboard',
16 'base/js/keyboard',
17 'notebook/js/cell',
17 'notebook/js/cell',
18 'notebook/js/outputarea',
18 'notebook/js/outputarea',
19 'notebook/js/completer',
19 'notebook/js/completer',
20 'notebook/js/celltoolbar',
20 'notebook/js/celltoolbar',
21 'codemirror/lib/codemirror',
21 'codemirror/lib/codemirror',
22 'codemirror/mode/python/python',
22 'codemirror/mode/python/python',
23 'notebook/js/codemirror-ipython'
23 'notebook/js/codemirror-ipython'
24 ], function(IPython, $, utils, keyboard, cell, outputarea, completer, celltoolbar, CodeMirror, cmpython, cmip) {
24 ], function(IPython, $, utils, keyboard, cell, outputarea, completer, celltoolbar, CodeMirror, cmpython, cmip) {
25 "use strict";
25 "use strict";
26 var Cell = cell.Cell;
26 var Cell = cell.Cell;
27
27
28 /* local util for codemirror */
28 /* local util for codemirror */
29 var posEq = function(a, b) {return a.line == b.line && a.ch == b.ch;};
29 var posEq = function(a, b) {return a.line == b.line && a.ch == b.ch;};
30
30
31 /**
31 /**
32 *
32 *
33 * function to delete until previous non blanking space character
33 * function to delete until previous non blanking space character
34 * or first multiple of 4 tabstop.
34 * or first multiple of 4 tabstop.
35 * @private
35 * @private
36 */
36 */
37 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
37 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
38 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
38 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
39 if (!posEq(from, to)) { cm.replaceRange("", from, to); return; }
39 if (!posEq(from, to)) { cm.replaceRange("", from, to); return; }
40 var cur = cm.getCursor(), line = cm.getLine(cur.line);
40 var cur = cm.getCursor(), line = cm.getLine(cur.line);
41 var tabsize = cm.getOption('tabSize');
41 var tabsize = cm.getOption('tabSize');
42 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
42 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
43 from = {ch:cur.ch-chToPrevTabStop,line:cur.line};
43 from = {ch:cur.ch-chToPrevTabStop,line:cur.line};
44 var select = cm.getRange(from,cur);
44 var select = cm.getRange(from,cur);
45 if( select.match(/^\ +$/) !== null){
45 if( select.match(/^\ +$/) !== null){
46 cm.replaceRange("",from,cur);
46 cm.replaceRange("",from,cur);
47 } else {
47 } else {
48 cm.deleteH(-1,"char");
48 cm.deleteH(-1,"char");
49 }
49 }
50 };
50 };
51
51
52 var keycodes = keyboard.keycodes;
52 var keycodes = keyboard.keycodes;
53
53
54 var CodeCell = function (kernel, options) {
54 var CodeCell = function (kernel, options) {
55 // Constructor
55 // Constructor
56 //
56 //
57 // A Cell conceived to write code.
57 // A Cell conceived to write code.
58 //
58 //
59 // Parameters:
59 // Parameters:
60 // kernel: Kernel instance
60 // kernel: Kernel instance
61 // The kernel doesn't have to be set at creation time, in that case
61 // The kernel doesn't have to be set at creation time, in that case
62 // it will be null and set_kernel has to be called later.
62 // it will be null and set_kernel has to be called later.
63 // options: dictionary
63 // options: dictionary
64 // Dictionary of keyword arguments.
64 // Dictionary of keyword arguments.
65 // events: $(Events) instance
65 // events: $(Events) instance
66 // config: dictionary
66 // config: dictionary
67 // keyboard_manager: KeyboardManager instance
67 // keyboard_manager: KeyboardManager instance
68 // notebook: Notebook instance
68 // notebook: Notebook instance
69 // tooltip: Tooltip instance
69 // tooltip: Tooltip instance
70 this.kernel = kernel || null;
70 this.kernel = kernel || null;
71 this.notebook = options.notebook;
71 this.notebook = options.notebook;
72 this.collapsed = false;
72 this.collapsed = false;
73 this.events = options.events;
73 this.events = options.events;
74 this.tooltip = options.tooltip;
74 this.tooltip = options.tooltip;
75 this.config = options.config;
75 this.config = options.config;
76
76
77 // create all attributed in constructor function
77 // create all attributed in constructor function
78 // even if null for V8 VM optimisation
78 // even if null for V8 VM optimisation
79 this.input_prompt_number = null;
79 this.input_prompt_number = null;
80 this.celltoolbar = null;
80 this.celltoolbar = null;
81 this.output_area = null;
81 this.output_area = null;
82 this.last_msg_id = null;
82 this.last_msg_id = null;
83 this.completer = null;
83 this.completer = null;
84
84
85
85
86 var config = utils.mergeopt(CodeCell, this.config);
86 var config = utils.mergeopt(CodeCell, this.config);
87 Cell.apply(this,[{
87 Cell.apply(this,[{
88 config: config,
88 config: config,
89 keyboard_manager: options.keyboard_manager,
89 keyboard_manager: options.keyboard_manager,
90 events: this.events}]);
90 events: this.events}]);
91
91
92 // Attributes we want to override in this subclass.
92 // Attributes we want to override in this subclass.
93 this.cell_type = "code";
93 this.cell_type = "code";
94
94
95 var that = this;
95 var that = this;
96 this.element.focusout(
96 this.element.focusout(
97 function() { that.auto_highlight(); }
97 function() { that.auto_highlight(); }
98 );
98 );
99 };
99 };
100
100
101 CodeCell.options_default = {
101 CodeCell.options_default = {
102 cm_config : {
102 cm_config : {
103 extraKeys: {
103 extraKeys: {
104 "Tab" : "indentMore",
104 "Tab" : "indentMore",
105 "Shift-Tab" : "indentLess",
105 "Shift-Tab" : "indentLess",
106 "Backspace" : "delSpaceToPrevTabStop",
106 "Backspace" : "delSpaceToPrevTabStop",
107 "Cmd-/" : "toggleComment",
107 "Cmd-/" : "toggleComment",
108 "Ctrl-/" : "toggleComment"
108 "Ctrl-/" : "toggleComment"
109 },
109 },
110 mode: 'ipython',
110 mode: 'ipython',
111 theme: 'ipython',
111 theme: 'ipython',
112 matchBrackets: true
112 matchBrackets: true
113 }
113 }
114 };
114 };
115
115
116 CodeCell.msg_cells = {};
116 CodeCell.msg_cells = {};
117
117
118 CodeCell.prototype = Object.create(Cell.prototype);
118 CodeCell.prototype = Object.create(Cell.prototype);
119
119
120 /**
120 /**
121 * @method auto_highlight
121 * @method auto_highlight
122 */
122 */
123 CodeCell.prototype.auto_highlight = function () {
123 CodeCell.prototype.auto_highlight = function () {
124 this._auto_highlight(this.config.cell_magic_highlight);
124 this._auto_highlight(this.config.cell_magic_highlight);
125 };
125 };
126
126
127 /** @method create_element */
127 /** @method create_element */
128 CodeCell.prototype.create_element = function () {
128 CodeCell.prototype.create_element = function () {
129 Cell.prototype.create_element.apply(this, arguments);
129 Cell.prototype.create_element.apply(this, arguments);
130
130
131 var cell = $('<div></div>').addClass('cell code_cell');
131 var cell = $('<div></div>').addClass('cell code_cell');
132 cell.attr('tabindex','2');
132 cell.attr('tabindex','2');
133
133
134 var input = $('<div></div>').addClass('input');
134 var input = $('<div></div>').addClass('input');
135 var prompt = $('<div/>').addClass('prompt input_prompt');
135 var prompt = $('<div/>').addClass('prompt input_prompt');
136 var inner_cell = $('<div/>').addClass('inner_cell');
136 var inner_cell = $('<div/>').addClass('inner_cell');
137 this.celltoolbar = new celltoolbar.CellToolbar({
137 this.celltoolbar = new celltoolbar.CellToolbar({
138 cell: this,
138 cell: this,
139 notebook: this.notebook});
139 notebook: this.notebook});
140 inner_cell.append(this.celltoolbar.element);
140 inner_cell.append(this.celltoolbar.element);
141 var input_area = $('<div/>').addClass('input_area');
141 var input_area = $('<div/>').addClass('input_area');
142 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
142 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
143 this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this))
143 this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this))
144 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
144 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
145 inner_cell.append(input_area);
145 inner_cell.append(input_area);
146 input.append(prompt).append(inner_cell);
146 input.append(prompt).append(inner_cell);
147
147
148 var widget_area = $('<div/>')
148 var widget_area = $('<div/>')
149 .addClass('widget-area')
149 .addClass('widget-area')
150 .hide();
150 .hide();
151 this.widget_area = widget_area;
151 this.widget_area = widget_area;
152 var widget_prompt = $('<div/>')
152 var widget_prompt = $('<div/>')
153 .addClass('prompt')
153 .addClass('prompt')
154 .appendTo(widget_area);
154 .appendTo(widget_area);
155 var widget_subarea = $('<div/>')
155 var widget_subarea = $('<div/>')
156 .addClass('widget-subarea')
156 .addClass('widget-subarea')
157 .appendTo(widget_area);
157 .appendTo(widget_area);
158 this.widget_subarea = widget_subarea;
158 this.widget_subarea = widget_subarea;
159 var widget_clear_buton = $('<button />')
159 var widget_clear_buton = $('<button />')
160 .addClass('close')
160 .addClass('close')
161 .html('&times;')
161 .html('&times;')
162 .click(function() {
162 .click(function() {
163 widget_area.slideUp('', function(){ widget_subarea.html(''); });
163 widget_area.slideUp('', function(){ widget_subarea.html(''); });
164 })
164 })
165 .appendTo(widget_prompt);
165 .appendTo(widget_prompt);
166
166
167 var output = $('<div></div>');
167 var output = $('<div></div>');
168 cell.append(input).append(widget_area).append(output);
168 cell.append(input).append(widget_area).append(output);
169 this.element = cell;
169 this.element = cell;
170 this.output_area = new outputarea.OutputArea({
170 this.output_area = new outputarea.OutputArea({
171 selector: output,
171 selector: output,
172 prompt_area: true,
172 prompt_area: true,
173 events: this.events,
173 events: this.events,
174 keyboard_manager: this.keyboard_manager});
174 keyboard_manager: this.keyboard_manager});
175 this.completer = new completer.Completer(this, this.events);
175 this.completer = new completer.Completer(this, this.events);
176 };
176 };
177
177
178 /** @method bind_events */
178 /** @method bind_events */
179 CodeCell.prototype.bind_events = function () {
179 CodeCell.prototype.bind_events = function () {
180 Cell.prototype.bind_events.apply(this);
180 Cell.prototype.bind_events.apply(this);
181 var that = this;
181 var that = this;
182
182
183 this.element.focusout(
183 this.element.focusout(
184 function() { that.auto_highlight(); }
184 function() { that.auto_highlight(); }
185 );
185 );
186 };
186 };
187
187
188
188
189 /**
189 /**
190 * This method gets called in CodeMirror's onKeyDown/onKeyPress
190 * This method gets called in CodeMirror's onKeyDown/onKeyPress
191 * handlers and is used to provide custom key handling. Its return
191 * handlers and is used to provide custom key handling. Its return
192 * value is used to determine if CodeMirror should ignore the event:
192 * value is used to determine if CodeMirror should ignore the event:
193 * true = ignore, false = don't ignore.
193 * true = ignore, false = don't ignore.
194 * @method handle_codemirror_keyevent
194 * @method handle_codemirror_keyevent
195 */
195 */
196 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
196 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
197
197
198 var that = this;
198 var that = this;
199 // whatever key is pressed, first, cancel the tooltip request before
199 // whatever key is pressed, first, cancel the tooltip request before
200 // they are sent, and remove tooltip if any, except for tab again
200 // they are sent, and remove tooltip if any, except for tab again
201 var tooltip_closed = null;
201 var tooltip_closed = null;
202 if (event.type === 'keydown' && event.which != keycodes.tab ) {
202 if (event.type === 'keydown' && event.which != keycodes.tab ) {
203 tooltip_closed = this.tooltip.remove_and_cancel_tooltip();
203 tooltip_closed = this.tooltip.remove_and_cancel_tooltip();
204 }
204 }
205
205
206 var cur = editor.getCursor();
206 var cur = editor.getCursor();
207 if (event.keyCode === keycodes.enter){
207 if (event.keyCode === keycodes.enter){
208 this.auto_highlight();
208 this.auto_highlight();
209 }
209 }
210
210
211 if (event.which === keycodes.down && event.type === 'keypress' && this.tooltip.time_before_tooltip >= 0) {
211 if (event.which === keycodes.down && event.type === 'keypress' && this.tooltip.time_before_tooltip >= 0) {
212 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
212 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
213 // browser and keyboard layout !
213 // browser and keyboard layout !
214 // Pressing '(' , request tooltip, don't forget to reappend it
214 // Pressing '(' , request tooltip, don't forget to reappend it
215 // The second argument says to hide the tooltip if the docstring
215 // The second argument says to hide the tooltip if the docstring
216 // is actually empty
216 // is actually empty
217 this.tooltip.pending(that, true);
217 this.tooltip.pending(that, true);
218 } else if ( tooltip_closed && event.which === keycodes.esc && event.type === 'keydown') {
218 } else if ( tooltip_closed && event.which === keycodes.esc && event.type === 'keydown') {
219 // If tooltip is active, cancel it. The call to
219 // If tooltip is active, cancel it. The call to
220 // remove_and_cancel_tooltip above doesn't pass, force=true.
220 // remove_and_cancel_tooltip above doesn't pass, force=true.
221 // Because of this it won't actually close the tooltip
221 // Because of this it won't actually close the tooltip
222 // if it is in sticky mode. Thus, we have to check again if it is open
222 // if it is in sticky mode. Thus, we have to check again if it is open
223 // and close it with force=true.
223 // and close it with force=true.
224 if (!this.tooltip._hidden) {
224 if (!this.tooltip._hidden) {
225 this.tooltip.remove_and_cancel_tooltip(true);
225 this.tooltip.remove_and_cancel_tooltip(true);
226 }
226 }
227 // If we closed the tooltip, don't let CM or the global handlers
227 // If we closed the tooltip, don't let CM or the global handlers
228 // handle this event.
228 // handle this event.
229 event.codemirrorIgnore = true;
229 event.codemirrorIgnore = true;
230 event.preventDefault();
230 event.preventDefault();
231 return true;
231 return true;
232 } else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) {
232 } else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) {
233 if (editor.somethingSelected() || editor.getSelections().length !== 1){
233 if (editor.somethingSelected() || editor.getSelections().length !== 1){
234 var anchor = editor.getCursor("anchor");
234 var anchor = editor.getCursor("anchor");
235 var head = editor.getCursor("head");
235 var head = editor.getCursor("head");
236 if( anchor.line != head.line){
236 if( anchor.line != head.line){
237 return false;
237 return false;
238 }
238 }
239 }
239 }
240 this.tooltip.request(that);
240 this.tooltip.request(that);
241 event.codemirrorIgnore = true;
241 event.codemirrorIgnore = true;
242 event.preventDefault();
242 event.preventDefault();
243 return true;
243 return true;
244 } else if (event.keyCode === keycodes.tab && event.type == 'keydown') {
244 } else if (event.keyCode === keycodes.tab && event.type == 'keydown') {
245 // Tab completion.
245 // Tab completion.
246 this.tooltip.remove_and_cancel_tooltip();
246 this.tooltip.remove_and_cancel_tooltip();
247
247
248 // completion does not work on multicursor, it might be possible though in some cases
248 // completion does not work on multicursor, it might be possible though in some cases
249 if (editor.somethingSelected() || editor.getSelections().length > 1) {
249 if (editor.somethingSelected() || editor.getSelections().length > 1) {
250 return false;
250 return false;
251 }
251 }
252 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
252 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
253 if (pre_cursor.trim() === "") {
253 if (pre_cursor.trim() === "") {
254 // Don't autocomplete if the part of the line before the cursor
254 // Don't autocomplete if the part of the line before the cursor
255 // is empty. In this case, let CodeMirror handle indentation.
255 // is empty. In this case, let CodeMirror handle indentation.
256 return false;
256 return false;
257 } else {
257 } else {
258 event.codemirrorIgnore = true;
258 event.codemirrorIgnore = true;
259 event.preventDefault();
259 event.preventDefault();
260 this.completer.startCompletion();
260 this.completer.startCompletion();
261 return true;
261 return true;
262 }
262 }
263 }
263 }
264
264
265 // keyboard event wasn't one of those unique to code cells, let's see
265 // keyboard event wasn't one of those unique to code cells, let's see
266 // if it's one of the generic ones (i.e. check edit mode shortcuts)
266 // if it's one of the generic ones (i.e. check edit mode shortcuts)
267 return Cell.prototype.handle_codemirror_keyevent.apply(this, [editor, event]);
267 return Cell.prototype.handle_codemirror_keyevent.apply(this, [editor, event]);
268 };
268 };
269
269
270 // Kernel related calls.
270 // Kernel related calls.
271
271
272 CodeCell.prototype.set_kernel = function (kernel) {
272 CodeCell.prototype.set_kernel = function (kernel) {
273 this.kernel = kernel;
273 this.kernel = kernel;
274 };
274 };
275
275
276 /**
276 /**
277 * Execute current code cell to the kernel
277 * Execute current code cell to the kernel
278 * @method execute
278 * @method execute
279 */
279 */
280 CodeCell.prototype.execute = function () {
280 CodeCell.prototype.execute = function () {
281 this.output_area.clear_output();
281 this.output_area.clear_output();
282
282
283 // Clear widget area
283 // Clear widget area
284 this.widget_subarea.html('');
284 this.widget_subarea.html('');
285 this.widget_subarea.height('');
285 this.widget_subarea.height('');
286 this.widget_area.height('');
286 this.widget_area.height('');
287 this.widget_area.hide();
287 this.widget_area.hide();
288
288
289 this.set_input_prompt('*');
289 this.set_input_prompt('*');
290 this.element.addClass("running");
290 this.element.addClass("running");
291 if (this.last_msg_id) {
291 if (this.last_msg_id) {
292 this.kernel.clear_callbacks_for_msg(this.last_msg_id);
292 this.kernel.clear_callbacks_for_msg(this.last_msg_id);
293 }
293 }
294 var callbacks = this.get_callbacks();
294 var callbacks = this.get_callbacks();
295
295
296 var old_msg_id = this.last_msg_id;
296 var old_msg_id = this.last_msg_id;
297 this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true});
297 this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true});
298 if (old_msg_id) {
298 if (old_msg_id) {
299 delete CodeCell.msg_cells[old_msg_id];
299 delete CodeCell.msg_cells[old_msg_id];
300 }
300 }
301 CodeCell.msg_cells[this.last_msg_id] = this;
301 CodeCell.msg_cells[this.last_msg_id] = this;
302 this.render();
302 this.render();
303 };
303 };
304
304
305 /**
305 /**
306 * Construct the default callbacks for
306 * Construct the default callbacks for
307 * @method get_callbacks
307 * @method get_callbacks
308 */
308 */
309 CodeCell.prototype.get_callbacks = function () {
309 CodeCell.prototype.get_callbacks = function () {
310 return {
310 return {
311 shell : {
311 shell : {
312 reply : $.proxy(this._handle_execute_reply, this),
312 reply : $.proxy(this._handle_execute_reply, this),
313 payload : {
313 payload : {
314 set_next_input : $.proxy(this._handle_set_next_input, this),
314 set_next_input : $.proxy(this._handle_set_next_input, this),
315 page : $.proxy(this._open_with_pager, this)
315 page : $.proxy(this._open_with_pager, this)
316 }
316 }
317 },
317 },
318 iopub : {
318 iopub : {
319 output : $.proxy(this.output_area.handle_output, this.output_area),
319 output : $.proxy(this.output_area.handle_output, this.output_area),
320 clear_output : $.proxy(this.output_area.handle_clear_output, this.output_area),
320 clear_output : $.proxy(this.output_area.handle_clear_output, this.output_area),
321 },
321 },
322 input : $.proxy(this._handle_input_request, this)
322 input : $.proxy(this._handle_input_request, this)
323 };
323 };
324 };
324 };
325
325
326 CodeCell.prototype._open_with_pager = function (payload) {
326 CodeCell.prototype._open_with_pager = function (payload) {
327 this.events.trigger('open_with_text.Pager', payload);
327 this.events.trigger('open_with_text.Pager', payload);
328 };
328 };
329
329
330 /**
330 /**
331 * @method _handle_execute_reply
331 * @method _handle_execute_reply
332 * @private
332 * @private
333 */
333 */
334 CodeCell.prototype._handle_execute_reply = function (msg) {
334 CodeCell.prototype._handle_execute_reply = function (msg) {
335 this.set_input_prompt(msg.content.execution_count);
335 this.set_input_prompt(msg.content.execution_count);
336 this.element.removeClass("running");
336 this.element.removeClass("running");
337 this.events.trigger('set_dirty.Notebook', {value: true});
337 this.events.trigger('set_dirty.Notebook', {value: true});
338 };
338 };
339
339
340 /**
340 /**
341 * @method _handle_set_next_input
341 * @method _handle_set_next_input
342 * @private
342 * @private
343 */
343 */
344 CodeCell.prototype._handle_set_next_input = function (payload) {
344 CodeCell.prototype._handle_set_next_input = function (payload) {
345 var data = {'cell': this, 'text': payload.text};
345 var data = {'cell': this, 'text': payload.text};
346 this.events.trigger('set_next_input.Notebook', data);
346 this.events.trigger('set_next_input.Notebook', data);
347 };
347 };
348
348
349 /**
349 /**
350 * @method _handle_input_request
350 * @method _handle_input_request
351 * @private
351 * @private
352 */
352 */
353 CodeCell.prototype._handle_input_request = function (msg) {
353 CodeCell.prototype._handle_input_request = function (msg) {
354 this.output_area.append_raw_input(msg);
354 this.output_area.append_raw_input(msg);
355 };
355 };
356
356
357
357
358 // Basic cell manipulation.
358 // Basic cell manipulation.
359
359
360 CodeCell.prototype.select = function () {
360 CodeCell.prototype.select = function () {
361 var cont = Cell.prototype.select.apply(this);
361 var cont = Cell.prototype.select.apply(this);
362 if (cont) {
362 if (cont) {
363 this.code_mirror.refresh();
363 this.code_mirror.refresh();
364 this.auto_highlight();
364 this.auto_highlight();
365 }
365 }
366 return cont;
366 return cont;
367 };
367 };
368
368
369 CodeCell.prototype.render = function () {
369 CodeCell.prototype.render = function () {
370 var cont = Cell.prototype.render.apply(this);
370 var cont = Cell.prototype.render.apply(this);
371 // Always execute, even if we are already in the rendered state
371 // Always execute, even if we are already in the rendered state
372 return cont;
372 return cont;
373 };
373 };
374
374
375 CodeCell.prototype.select_all = function () {
375 CodeCell.prototype.select_all = function () {
376 var start = {line: 0, ch: 0};
376 var start = {line: 0, ch: 0};
377 var nlines = this.code_mirror.lineCount();
377 var nlines = this.code_mirror.lineCount();
378 var last_line = this.code_mirror.getLine(nlines-1);
378 var last_line = this.code_mirror.getLine(nlines-1);
379 var end = {line: nlines-1, ch: last_line.length};
379 var end = {line: nlines-1, ch: last_line.length};
380 this.code_mirror.setSelection(start, end);
380 this.code_mirror.setSelection(start, end);
381 };
381 };
382
382
383
383
384 CodeCell.prototype.collapse_output = function () {
384 CodeCell.prototype.collapse_output = function () {
385 this.collapsed = true;
386 this.output_area.collapse();
385 this.output_area.collapse();
387 };
386 };
388
387
389
388
390 CodeCell.prototype.expand_output = function () {
389 CodeCell.prototype.expand_output = function () {
391 this.collapsed = false;
392 this.output_area.expand();
390 this.output_area.expand();
393 this.output_area.unscroll_area();
391 this.output_area.unscroll_area();
394 };
392 };
395
393
396 CodeCell.prototype.scroll_output = function () {
394 CodeCell.prototype.scroll_output = function () {
397 this.output_area.expand();
395 this.output_area.expand();
398 this.output_area.scroll_if_long();
396 this.output_area.scroll_if_long();
399 };
397 };
400
398
401 CodeCell.prototype.toggle_output = function () {
399 CodeCell.prototype.toggle_output = function () {
402 this.collapsed = Boolean(1 - this.collapsed);
403 this.output_area.toggle_output();
400 this.output_area.toggle_output();
404 };
401 };
405
402
406 CodeCell.prototype.toggle_output_scroll = function () {
403 CodeCell.prototype.toggle_output_scroll = function () {
407 this.output_area.toggle_scroll();
404 this.output_area.toggle_scroll();
408 };
405 };
409
406
410
407
411 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
408 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
412 var ns;
409 var ns;
413 if (prompt_value === undefined || prompt_value === null) {
410 if (prompt_value === undefined || prompt_value === null) {
414 ns = "&nbsp;";
411 ns = "&nbsp;";
415 } else {
412 } else {
416 ns = encodeURIComponent(prompt_value);
413 ns = encodeURIComponent(prompt_value);
417 }
414 }
418 return 'In&nbsp;[' + ns + ']:';
415 return 'In&nbsp;[' + ns + ']:';
419 };
416 };
420
417
421 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
418 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
422 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
419 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
423 for(var i=1; i < lines_number; i++) {
420 for(var i=1; i < lines_number; i++) {
424 html.push(['...:']);
421 html.push(['...:']);
425 }
422 }
426 return html.join('<br/>');
423 return html.join('<br/>');
427 };
424 };
428
425
429 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
426 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
430
427
431
428
432 CodeCell.prototype.set_input_prompt = function (number) {
429 CodeCell.prototype.set_input_prompt = function (number) {
433 var nline = 1;
430 var nline = 1;
434 if (this.code_mirror !== undefined) {
431 if (this.code_mirror !== undefined) {
435 nline = this.code_mirror.lineCount();
432 nline = this.code_mirror.lineCount();
436 }
433 }
437 this.input_prompt_number = number;
434 this.input_prompt_number = number;
438 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
435 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
439 // This HTML call is okay because the user contents are escaped.
436 // This HTML call is okay because the user contents are escaped.
440 this.element.find('div.input_prompt').html(prompt_html);
437 this.element.find('div.input_prompt').html(prompt_html);
441 };
438 };
442
439
443
440
444 CodeCell.prototype.clear_input = function () {
441 CodeCell.prototype.clear_input = function () {
445 this.code_mirror.setValue('');
442 this.code_mirror.setValue('');
446 };
443 };
447
444
448
445
449 CodeCell.prototype.get_text = function () {
446 CodeCell.prototype.get_text = function () {
450 return this.code_mirror.getValue();
447 return this.code_mirror.getValue();
451 };
448 };
452
449
453
450
454 CodeCell.prototype.set_text = function (code) {
451 CodeCell.prototype.set_text = function (code) {
455 return this.code_mirror.setValue(code);
452 return this.code_mirror.setValue(code);
456 };
453 };
457
454
458
455
459 CodeCell.prototype.clear_output = function (wait) {
456 CodeCell.prototype.clear_output = function (wait) {
460 this.output_area.clear_output(wait);
457 this.output_area.clear_output(wait);
461 this.set_input_prompt();
458 this.set_input_prompt();
462 };
459 };
463
460
464
461
465 // JSON serialization
462 // JSON serialization
466
463
467 CodeCell.prototype.fromJSON = function (data) {
464 CodeCell.prototype.fromJSON = function (data) {
468 Cell.prototype.fromJSON.apply(this, arguments);
465 Cell.prototype.fromJSON.apply(this, arguments);
469 if (data.cell_type === 'code') {
466 if (data.cell_type === 'code') {
470 if (data.input !== undefined) {
467 if (data.source !== undefined) {
471 this.set_text(data.input);
468 this.set_text(data.source);
472 // make this value the starting point, so that we can only undo
469 // make this value the starting point, so that we can only undo
473 // to this state, instead of a blank cell
470 // to this state, instead of a blank cell
474 this.code_mirror.clearHistory();
471 this.code_mirror.clearHistory();
475 this.auto_highlight();
472 this.auto_highlight();
476 }
473 }
477 if (data.prompt_number !== undefined) {
474 this.set_input_prompt(data.prompt_number);
478 this.set_input_prompt(data.prompt_number);
479 } else {
480 this.set_input_prompt();
481 }
482 this.output_area.trusted = data.metadata.trusted || false;
475 this.output_area.trusted = data.metadata.trusted || false;
483 this.output_area.fromJSON(data.outputs);
476 this.output_area.fromJSON(data.outputs);
484 if (data.collapsed !== undefined) {
477 if (data.metadata.collapsed !== undefined) {
485 if (data.collapsed) {
478 if (data.metadata.collapsed) {
486 this.collapse_output();
479 this.collapse_output();
487 } else {
480 } else {
488 this.expand_output();
481 this.expand_output();
489 }
482 }
490 }
483 }
491 }
484 }
492 };
485 };
493
486
494
487
495 CodeCell.prototype.toJSON = function () {
488 CodeCell.prototype.toJSON = function () {
496 var data = Cell.prototype.toJSON.apply(this);
489 var data = Cell.prototype.toJSON.apply(this);
497 data.input = this.get_text();
490 data.source = this.get_text();
498 // is finite protect against undefined and '*' value
491 // is finite protect against undefined and '*' value
499 if (isFinite(this.input_prompt_number)) {
492 if (isFinite(this.input_prompt_number)) {
500 data.prompt_number = this.input_prompt_number;
493 data.prompt_number = this.input_prompt_number;
494 } else {
495 data.prompt_number = null;
501 }
496 }
502 var outputs = this.output_area.toJSON();
497 var outputs = this.output_area.toJSON();
503 data.outputs = outputs;
498 data.outputs = outputs;
504 data.language = 'python';
505 data.metadata.trusted = this.output_area.trusted;
499 data.metadata.trusted = this.output_area.trusted;
506 data.collapsed = this.output_area.collapsed;
500 data.metadata.collapsed = this.output_area.collapsed;
507 return data;
501 return data;
508 };
502 };
509
503
510 /**
504 /**
511 * handle cell level logic when a cell is unselected
505 * handle cell level logic when a cell is unselected
512 * @method unselect
506 * @method unselect
513 * @return is the action being taken
507 * @return is the action being taken
514 */
508 */
515 CodeCell.prototype.unselect = function () {
509 CodeCell.prototype.unselect = function () {
516 var cont = Cell.prototype.unselect.apply(this);
510 var cont = Cell.prototype.unselect.apply(this);
517 if (cont) {
511 if (cont) {
518 // When a code cell is usnelected, make sure that the corresponding
512 // When a code cell is usnelected, make sure that the corresponding
519 // tooltip and completer to that cell is closed.
513 // tooltip and completer to that cell is closed.
520 this.tooltip.remove_and_cancel_tooltip(true);
514 this.tooltip.remove_and_cancel_tooltip(true);
521 if (this.completer !== null) {
515 if (this.completer !== null) {
522 this.completer.close();
516 this.completer.close();
523 }
517 }
524 }
518 }
525 return cont;
519 return cont;
526 };
520 };
527
521
528 // Backwards compatability.
522 // Backwards compatability.
529 IPython.CodeCell = CodeCell;
523 IPython.CodeCell = CodeCell;
530
524
531 return {'CodeCell': CodeCell};
525 return {'CodeCell': CodeCell};
532 });
526 });
@@ -1,2707 +1,2678 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 'base/js/namespace',
5 'base/js/namespace',
6 'jquery',
6 'jquery',
7 'base/js/utils',
7 'base/js/utils',
8 'base/js/dialog',
8 'base/js/dialog',
9 'notebook/js/textcell',
9 'notebook/js/textcell',
10 'notebook/js/codecell',
10 'notebook/js/codecell',
11 'services/sessions/session',
11 'services/sessions/session',
12 'notebook/js/celltoolbar',
12 'notebook/js/celltoolbar',
13 'components/marked/lib/marked',
13 'components/marked/lib/marked',
14 'highlight',
14 'highlight',
15 'notebook/js/mathjaxutils',
15 'notebook/js/mathjaxutils',
16 'base/js/keyboard',
16 'base/js/keyboard',
17 'notebook/js/tooltip',
17 'notebook/js/tooltip',
18 'notebook/js/celltoolbarpresets/default',
18 'notebook/js/celltoolbarpresets/default',
19 'notebook/js/celltoolbarpresets/rawcell',
19 'notebook/js/celltoolbarpresets/rawcell',
20 'notebook/js/celltoolbarpresets/slideshow',
20 'notebook/js/celltoolbarpresets/slideshow',
21 'notebook/js/scrollmanager'
21 'notebook/js/scrollmanager'
22 ], function (
22 ], function (
23 IPython,
23 IPython,
24 $,
24 $,
25 utils,
25 utils,
26 dialog,
26 dialog,
27 textcell,
27 textcell,
28 codecell,
28 codecell,
29 session,
29 session,
30 celltoolbar,
30 celltoolbar,
31 marked,
31 marked,
32 hljs,
32 hljs,
33 mathjaxutils,
33 mathjaxutils,
34 keyboard,
34 keyboard,
35 tooltip,
35 tooltip,
36 default_celltoolbar,
36 default_celltoolbar,
37 rawcell_celltoolbar,
37 rawcell_celltoolbar,
38 slideshow_celltoolbar,
38 slideshow_celltoolbar,
39 scrollmanager
39 scrollmanager
40 ) {
40 ) {
41
41
42 var Notebook = function (selector, options) {
42 var Notebook = function (selector, options) {
43 // Constructor
43 // Constructor
44 //
44 //
45 // A notebook contains and manages cells.
45 // A notebook contains and manages cells.
46 //
46 //
47 // Parameters:
47 // Parameters:
48 // selector: string
48 // selector: string
49 // options: dictionary
49 // options: dictionary
50 // Dictionary of keyword arguments.
50 // Dictionary of keyword arguments.
51 // events: $(Events) instance
51 // events: $(Events) instance
52 // keyboard_manager: KeyboardManager instance
52 // keyboard_manager: KeyboardManager instance
53 // save_widget: SaveWidget instance
53 // save_widget: SaveWidget instance
54 // config: dictionary
54 // config: dictionary
55 // base_url : string
55 // base_url : string
56 // notebook_path : string
56 // notebook_path : string
57 // notebook_name : string
57 // notebook_name : string
58 this.config = utils.mergeopt(Notebook, options.config);
58 this.config = utils.mergeopt(Notebook, options.config);
59 this.base_url = options.base_url;
59 this.base_url = options.base_url;
60 this.notebook_path = options.notebook_path;
60 this.notebook_path = options.notebook_path;
61 this.notebook_name = options.notebook_name;
61 this.notebook_name = options.notebook_name;
62 this.events = options.events;
62 this.events = options.events;
63 this.keyboard_manager = options.keyboard_manager;
63 this.keyboard_manager = options.keyboard_manager;
64 this.save_widget = options.save_widget;
64 this.save_widget = options.save_widget;
65 this.tooltip = new tooltip.Tooltip(this.events);
65 this.tooltip = new tooltip.Tooltip(this.events);
66 this.ws_url = options.ws_url;
66 this.ws_url = options.ws_url;
67 this._session_starting = false;
67 this._session_starting = false;
68 this.default_cell_type = this.config.default_cell_type || 'code';
68 this.default_cell_type = this.config.default_cell_type || 'code';
69
69
70 // Create default scroll manager.
70 // Create default scroll manager.
71 this.scroll_manager = new scrollmanager.ScrollManager(this);
71 this.scroll_manager = new scrollmanager.ScrollManager(this);
72
72
73 // TODO: This code smells (and the other `= this` line a couple lines down)
73 // TODO: This code smells (and the other `= this` line a couple lines down)
74 // We need a better way to deal with circular instance references.
74 // We need a better way to deal with circular instance references.
75 this.keyboard_manager.notebook = this;
75 this.keyboard_manager.notebook = this;
76 this.save_widget.notebook = this;
76 this.save_widget.notebook = this;
77
77
78 mathjaxutils.init();
78 mathjaxutils.init();
79
79
80 if (marked) {
80 if (marked) {
81 marked.setOptions({
81 marked.setOptions({
82 gfm : true,
82 gfm : true,
83 tables: true,
83 tables: true,
84 langPrefix: "language-",
84 langPrefix: "language-",
85 highlight: function(code, lang) {
85 highlight: function(code, lang) {
86 if (!lang) {
86 if (!lang) {
87 // no language, no highlight
87 // no language, no highlight
88 return code;
88 return code;
89 }
89 }
90 var highlighted;
90 var highlighted;
91 try {
91 try {
92 highlighted = hljs.highlight(lang, code, false);
92 highlighted = hljs.highlight(lang, code, false);
93 } catch(err) {
93 } catch(err) {
94 highlighted = hljs.highlightAuto(code);
94 highlighted = hljs.highlightAuto(code);
95 }
95 }
96 return highlighted.value;
96 return highlighted.value;
97 }
97 }
98 });
98 });
99 }
99 }
100
100
101 this.element = $(selector);
101 this.element = $(selector);
102 this.element.scroll();
102 this.element.scroll();
103 this.element.data("notebook", this);
103 this.element.data("notebook", this);
104 this.next_prompt_number = 1;
104 this.next_prompt_number = 1;
105 this.session = null;
105 this.session = null;
106 this.kernel = null;
106 this.kernel = null;
107 this.clipboard = null;
107 this.clipboard = null;
108 this.undelete_backup = null;
108 this.undelete_backup = null;
109 this.undelete_index = null;
109 this.undelete_index = null;
110 this.undelete_below = false;
110 this.undelete_below = false;
111 this.paste_enabled = false;
111 this.paste_enabled = false;
112 // It is important to start out in command mode to match the intial mode
112 // It is important to start out in command mode to match the intial mode
113 // of the KeyboardManager.
113 // of the KeyboardManager.
114 this.mode = 'command';
114 this.mode = 'command';
115 this.set_dirty(false);
115 this.set_dirty(false);
116 this.metadata = {};
116 this.metadata = {};
117 this._checkpoint_after_save = false;
117 this._checkpoint_after_save = false;
118 this.last_checkpoint = null;
118 this.last_checkpoint = null;
119 this.checkpoints = [];
119 this.checkpoints = [];
120 this.autosave_interval = 0;
120 this.autosave_interval = 0;
121 this.autosave_timer = null;
121 this.autosave_timer = null;
122 // autosave *at most* every two minutes
122 // autosave *at most* every two minutes
123 this.minimum_autosave_interval = 120000;
123 this.minimum_autosave_interval = 120000;
124 // single worksheet for now
125 this.worksheet_metadata = {};
126 this.notebook_name_blacklist_re = /[\/\\:]/;
124 this.notebook_name_blacklist_re = /[\/\\:]/;
127 this.nbformat = 3; // Increment this when changing the nbformat
125 this.nbformat = 4; // Increment this when changing the nbformat
128 this.nbformat_minor = 0; // Increment this when changing the nbformat
126 this.nbformat_minor = 0; // Increment this when changing the nbformat
129 this.codemirror_mode = 'ipython';
127 this.codemirror_mode = 'ipython';
130 this.create_elements();
128 this.create_elements();
131 this.bind_events();
129 this.bind_events();
132 this.save_notebook = function() { // don't allow save until notebook_loaded
130 this.save_notebook = function() { // don't allow save until notebook_loaded
133 this.save_notebook_error(null, null, "Load failed, save is disabled");
131 this.save_notebook_error(null, null, "Load failed, save is disabled");
134 };
132 };
135
133
136 // Trigger cell toolbar registration.
134 // Trigger cell toolbar registration.
137 default_celltoolbar.register(this);
135 default_celltoolbar.register(this);
138 rawcell_celltoolbar.register(this);
136 rawcell_celltoolbar.register(this);
139 slideshow_celltoolbar.register(this);
137 slideshow_celltoolbar.register(this);
140 };
138 };
141
139
142 Notebook.options_default = {
140 Notebook.options_default = {
143 // can be any cell type, or the special values of
141 // can be any cell type, or the special values of
144 // 'above', 'below', or 'selected' to get the value from another cell.
142 // 'above', 'below', or 'selected' to get the value from another cell.
145 Notebook: {
143 Notebook: {
146 default_cell_type: 'code',
144 default_cell_type: 'code',
147 }
145 }
148 };
146 };
149
147
150
148
151 /**
149 /**
152 * Create an HTML and CSS representation of the notebook.
150 * Create an HTML and CSS representation of the notebook.
153 *
151 *
154 * @method create_elements
152 * @method create_elements
155 */
153 */
156 Notebook.prototype.create_elements = function () {
154 Notebook.prototype.create_elements = function () {
157 var that = this;
155 var that = this;
158 this.element.attr('tabindex','-1');
156 this.element.attr('tabindex','-1');
159 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
157 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
160 // We add this end_space div to the end of the notebook div to:
158 // We add this end_space div to the end of the notebook div to:
161 // i) provide a margin between the last cell and the end of the notebook
159 // i) provide a margin between the last cell and the end of the notebook
162 // ii) to prevent the div from scrolling up when the last cell is being
160 // ii) to prevent the div from scrolling up when the last cell is being
163 // edited, but is too low on the page, which browsers will do automatically.
161 // edited, but is too low on the page, which browsers will do automatically.
164 var end_space = $('<div/>').addClass('end_space');
162 var end_space = $('<div/>').addClass('end_space');
165 end_space.dblclick(function (e) {
163 end_space.dblclick(function (e) {
166 var ncells = that.ncells();
164 var ncells = that.ncells();
167 that.insert_cell_below('code',ncells-1);
165 that.insert_cell_below('code',ncells-1);
168 });
166 });
169 this.element.append(this.container);
167 this.element.append(this.container);
170 this.container.append(end_space);
168 this.container.append(end_space);
171 };
169 };
172
170
173 /**
171 /**
174 * Bind JavaScript events: key presses and custom IPython events.
172 * Bind JavaScript events: key presses and custom IPython events.
175 *
173 *
176 * @method bind_events
174 * @method bind_events
177 */
175 */
178 Notebook.prototype.bind_events = function () {
176 Notebook.prototype.bind_events = function () {
179 var that = this;
177 var that = this;
180
178
181 this.events.on('set_next_input.Notebook', function (event, data) {
179 this.events.on('set_next_input.Notebook', function (event, data) {
182 var index = that.find_cell_index(data.cell);
180 var index = that.find_cell_index(data.cell);
183 var new_cell = that.insert_cell_below('code',index);
181 var new_cell = that.insert_cell_below('code',index);
184 new_cell.set_text(data.text);
182 new_cell.set_text(data.text);
185 that.dirty = true;
183 that.dirty = true;
186 });
184 });
187
185
188 this.events.on('set_dirty.Notebook', function (event, data) {
186 this.events.on('set_dirty.Notebook', function (event, data) {
189 that.dirty = data.value;
187 that.dirty = data.value;
190 });
188 });
191
189
192 this.events.on('trust_changed.Notebook', function (event, trusted) {
190 this.events.on('trust_changed.Notebook', function (event, trusted) {
193 that.trusted = trusted;
191 that.trusted = trusted;
194 });
192 });
195
193
196 this.events.on('select.Cell', function (event, data) {
194 this.events.on('select.Cell', function (event, data) {
197 var index = that.find_cell_index(data.cell);
195 var index = that.find_cell_index(data.cell);
198 that.select(index);
196 that.select(index);
199 });
197 });
200
198
201 this.events.on('edit_mode.Cell', function (event, data) {
199 this.events.on('edit_mode.Cell', function (event, data) {
202 that.handle_edit_mode(data.cell);
200 that.handle_edit_mode(data.cell);
203 });
201 });
204
202
205 this.events.on('command_mode.Cell', function (event, data) {
203 this.events.on('command_mode.Cell', function (event, data) {
206 that.handle_command_mode(data.cell);
204 that.handle_command_mode(data.cell);
207 });
205 });
208
206
209 this.events.on('spec_changed.Kernel', function(event, data) {
207 this.events.on('spec_changed.Kernel', function(event, data) {
210 that.metadata.kernelspec =
208 that.metadata.kernelspec =
211 {name: data.name, display_name: data.display_name};
209 {name: data.name, display_name: data.display_name};
212 });
210 });
213
211
214 this.events.on('kernel_ready.Kernel', function(event, data) {
212 this.events.on('kernel_ready.Kernel', function(event, data) {
215 var kinfo = data.kernel.info_reply
213 var kinfo = data.kernel.info_reply
216 var langinfo = kinfo.language_info || {};
214 var langinfo = kinfo.language_info || {};
217 if (!langinfo.name) langinfo.name = kinfo.language;
215 if (!langinfo.name) langinfo.name = kinfo.language;
218
216
219 that.metadata.language_info = langinfo;
217 that.metadata.language_info = langinfo;
220 // Mode 'null' should be plain, unhighlighted text.
218 // Mode 'null' should be plain, unhighlighted text.
221 var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null'
219 var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null'
222 that.set_codemirror_mode(cm_mode);
220 that.set_codemirror_mode(cm_mode);
223 });
221 });
224
222
225 var collapse_time = function (time) {
223 var collapse_time = function (time) {
226 var app_height = $('#ipython-main-app').height(); // content height
224 var app_height = $('#ipython-main-app').height(); // content height
227 var splitter_height = $('div#pager_splitter').outerHeight(true);
225 var splitter_height = $('div#pager_splitter').outerHeight(true);
228 var new_height = app_height - splitter_height;
226 var new_height = app_height - splitter_height;
229 that.element.animate({height : new_height + 'px'}, time);
227 that.element.animate({height : new_height + 'px'}, time);
230 };
228 };
231
229
232 this.element.bind('collapse_pager', function (event, extrap) {
230 this.element.bind('collapse_pager', function (event, extrap) {
233 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
231 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
234 collapse_time(time);
232 collapse_time(time);
235 });
233 });
236
234
237 var expand_time = function (time) {
235 var expand_time = function (time) {
238 var app_height = $('#ipython-main-app').height(); // content height
236 var app_height = $('#ipython-main-app').height(); // content height
239 var splitter_height = $('div#pager_splitter').outerHeight(true);
237 var splitter_height = $('div#pager_splitter').outerHeight(true);
240 var pager_height = $('div#pager').outerHeight(true);
238 var pager_height = $('div#pager').outerHeight(true);
241 var new_height = app_height - pager_height - splitter_height;
239 var new_height = app_height - pager_height - splitter_height;
242 that.element.animate({height : new_height + 'px'}, time);
240 that.element.animate({height : new_height + 'px'}, time);
243 };
241 };
244
242
245 this.element.bind('expand_pager', function (event, extrap) {
243 this.element.bind('expand_pager', function (event, extrap) {
246 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
244 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
247 expand_time(time);
245 expand_time(time);
248 });
246 });
249
247
250 // Firefox 22 broke $(window).on("beforeunload")
248 // Firefox 22 broke $(window).on("beforeunload")
251 // I'm not sure why or how.
249 // I'm not sure why or how.
252 window.onbeforeunload = function (e) {
250 window.onbeforeunload = function (e) {
253 // TODO: Make killing the kernel configurable.
251 // TODO: Make killing the kernel configurable.
254 var kill_kernel = false;
252 var kill_kernel = false;
255 if (kill_kernel) {
253 if (kill_kernel) {
256 that.session.delete();
254 that.session.delete();
257 }
255 }
258 // if we are autosaving, trigger an autosave on nav-away.
256 // if we are autosaving, trigger an autosave on nav-away.
259 // still warn, because if we don't the autosave may fail.
257 // still warn, because if we don't the autosave may fail.
260 if (that.dirty) {
258 if (that.dirty) {
261 if ( that.autosave_interval ) {
259 if ( that.autosave_interval ) {
262 // schedule autosave in a timeout
260 // schedule autosave in a timeout
263 // this gives you a chance to forcefully discard changes
261 // this gives you a chance to forcefully discard changes
264 // by reloading the page if you *really* want to.
262 // by reloading the page if you *really* want to.
265 // the timer doesn't start until you *dismiss* the dialog.
263 // the timer doesn't start until you *dismiss* the dialog.
266 setTimeout(function () {
264 setTimeout(function () {
267 if (that.dirty) {
265 if (that.dirty) {
268 that.save_notebook();
266 that.save_notebook();
269 }
267 }
270 }, 1000);
268 }, 1000);
271 return "Autosave in progress, latest changes may be lost.";
269 return "Autosave in progress, latest changes may be lost.";
272 } else {
270 } else {
273 return "Unsaved changes will be lost.";
271 return "Unsaved changes will be lost.";
274 }
272 }
275 }
273 }
276 // Null is the *only* return value that will make the browser not
274 // Null is the *only* return value that will make the browser not
277 // pop up the "don't leave" dialog.
275 // pop up the "don't leave" dialog.
278 return null;
276 return null;
279 };
277 };
280 };
278 };
281
279
282 /**
280 /**
283 * Set the dirty flag, and trigger the set_dirty.Notebook event
281 * Set the dirty flag, and trigger the set_dirty.Notebook event
284 *
282 *
285 * @method set_dirty
283 * @method set_dirty
286 */
284 */
287 Notebook.prototype.set_dirty = function (value) {
285 Notebook.prototype.set_dirty = function (value) {
288 if (value === undefined) {
286 if (value === undefined) {
289 value = true;
287 value = true;
290 }
288 }
291 if (this.dirty == value) {
289 if (this.dirty == value) {
292 return;
290 return;
293 }
291 }
294 this.events.trigger('set_dirty.Notebook', {value: value});
292 this.events.trigger('set_dirty.Notebook', {value: value});
295 };
293 };
296
294
297 /**
295 /**
298 * Scroll the top of the page to a given cell.
296 * Scroll the top of the page to a given cell.
299 *
297 *
300 * @method scroll_to_cell
298 * @method scroll_to_cell
301 * @param {Number} cell_number An index of the cell to view
299 * @param {Number} cell_number An index of the cell to view
302 * @param {Number} time Animation time in milliseconds
300 * @param {Number} time Animation time in milliseconds
303 * @return {Number} Pixel offset from the top of the container
301 * @return {Number} Pixel offset from the top of the container
304 */
302 */
305 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
303 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
306 var cells = this.get_cells();
304 var cells = this.get_cells();
307 time = time || 0;
305 time = time || 0;
308 cell_number = Math.min(cells.length-1,cell_number);
306 cell_number = Math.min(cells.length-1,cell_number);
309 cell_number = Math.max(0 ,cell_number);
307 cell_number = Math.max(0 ,cell_number);
310 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
308 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
311 this.element.animate({scrollTop:scroll_value}, time);
309 this.element.animate({scrollTop:scroll_value}, time);
312 return scroll_value;
310 return scroll_value;
313 };
311 };
314
312
315 /**
313 /**
316 * Scroll to the bottom of the page.
314 * Scroll to the bottom of the page.
317 *
315 *
318 * @method scroll_to_bottom
316 * @method scroll_to_bottom
319 */
317 */
320 Notebook.prototype.scroll_to_bottom = function () {
318 Notebook.prototype.scroll_to_bottom = function () {
321 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
319 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
322 };
320 };
323
321
324 /**
322 /**
325 * Scroll to the top of the page.
323 * Scroll to the top of the page.
326 *
324 *
327 * @method scroll_to_top
325 * @method scroll_to_top
328 */
326 */
329 Notebook.prototype.scroll_to_top = function () {
327 Notebook.prototype.scroll_to_top = function () {
330 this.element.animate({scrollTop:0}, 0);
328 this.element.animate({scrollTop:0}, 0);
331 };
329 };
332
330
333 // Edit Notebook metadata
331 // Edit Notebook metadata
334
332
335 Notebook.prototype.edit_metadata = function () {
333 Notebook.prototype.edit_metadata = function () {
336 var that = this;
334 var that = this;
337 dialog.edit_metadata({
335 dialog.edit_metadata({
338 md: this.metadata,
336 md: this.metadata,
339 callback: function (md) {
337 callback: function (md) {
340 that.metadata = md;
338 that.metadata = md;
341 },
339 },
342 name: 'Notebook',
340 name: 'Notebook',
343 notebook: this,
341 notebook: this,
344 keyboard_manager: this.keyboard_manager});
342 keyboard_manager: this.keyboard_manager});
345 };
343 };
346
344
347 // Cell indexing, retrieval, etc.
345 // Cell indexing, retrieval, etc.
348
346
349 /**
347 /**
350 * Get all cell elements in the notebook.
348 * Get all cell elements in the notebook.
351 *
349 *
352 * @method get_cell_elements
350 * @method get_cell_elements
353 * @return {jQuery} A selector of all cell elements
351 * @return {jQuery} A selector of all cell elements
354 */
352 */
355 Notebook.prototype.get_cell_elements = function () {
353 Notebook.prototype.get_cell_elements = function () {
356 return this.container.children("div.cell");
354 return this.container.children("div.cell");
357 };
355 };
358
356
359 /**
357 /**
360 * Get a particular cell element.
358 * Get a particular cell element.
361 *
359 *
362 * @method get_cell_element
360 * @method get_cell_element
363 * @param {Number} index An index of a cell to select
361 * @param {Number} index An index of a cell to select
364 * @return {jQuery} A selector of the given cell.
362 * @return {jQuery} A selector of the given cell.
365 */
363 */
366 Notebook.prototype.get_cell_element = function (index) {
364 Notebook.prototype.get_cell_element = function (index) {
367 var result = null;
365 var result = null;
368 var e = this.get_cell_elements().eq(index);
366 var e = this.get_cell_elements().eq(index);
369 if (e.length !== 0) {
367 if (e.length !== 0) {
370 result = e;
368 result = e;
371 }
369 }
372 return result;
370 return result;
373 };
371 };
374
372
375 /**
373 /**
376 * Try to get a particular cell by msg_id.
374 * Try to get a particular cell by msg_id.
377 *
375 *
378 * @method get_msg_cell
376 * @method get_msg_cell
379 * @param {String} msg_id A message UUID
377 * @param {String} msg_id A message UUID
380 * @return {Cell} Cell or null if no cell was found.
378 * @return {Cell} Cell or null if no cell was found.
381 */
379 */
382 Notebook.prototype.get_msg_cell = function (msg_id) {
380 Notebook.prototype.get_msg_cell = function (msg_id) {
383 return codecell.CodeCell.msg_cells[msg_id] || null;
381 return codecell.CodeCell.msg_cells[msg_id] || null;
384 };
382 };
385
383
386 /**
384 /**
387 * Count the cells in this notebook.
385 * Count the cells in this notebook.
388 *
386 *
389 * @method ncells
387 * @method ncells
390 * @return {Number} The number of cells in this notebook
388 * @return {Number} The number of cells in this notebook
391 */
389 */
392 Notebook.prototype.ncells = function () {
390 Notebook.prototype.ncells = function () {
393 return this.get_cell_elements().length;
391 return this.get_cell_elements().length;
394 };
392 };
395
393
396 /**
394 /**
397 * Get all Cell objects in this notebook.
395 * Get all Cell objects in this notebook.
398 *
396 *
399 * @method get_cells
397 * @method get_cells
400 * @return {Array} This notebook's Cell objects
398 * @return {Array} This notebook's Cell objects
401 */
399 */
402 // TODO: we are often calling cells as cells()[i], which we should optimize
400 // TODO: we are often calling cells as cells()[i], which we should optimize
403 // to cells(i) or a new method.
401 // to cells(i) or a new method.
404 Notebook.prototype.get_cells = function () {
402 Notebook.prototype.get_cells = function () {
405 return this.get_cell_elements().toArray().map(function (e) {
403 return this.get_cell_elements().toArray().map(function (e) {
406 return $(e).data("cell");
404 return $(e).data("cell");
407 });
405 });
408 };
406 };
409
407
410 /**
408 /**
411 * Get a Cell object from this notebook.
409 * Get a Cell object from this notebook.
412 *
410 *
413 * @method get_cell
411 * @method get_cell
414 * @param {Number} index An index of a cell to retrieve
412 * @param {Number} index An index of a cell to retrieve
415 * @return {Cell} Cell or null if no cell was found.
413 * @return {Cell} Cell or null if no cell was found.
416 */
414 */
417 Notebook.prototype.get_cell = function (index) {
415 Notebook.prototype.get_cell = function (index) {
418 var result = null;
416 var result = null;
419 var ce = this.get_cell_element(index);
417 var ce = this.get_cell_element(index);
420 if (ce !== null) {
418 if (ce !== null) {
421 result = ce.data('cell');
419 result = ce.data('cell');
422 }
420 }
423 return result;
421 return result;
424 };
422 };
425
423
426 /**
424 /**
427 * Get the cell below a given cell.
425 * Get the cell below a given cell.
428 *
426 *
429 * @method get_next_cell
427 * @method get_next_cell
430 * @param {Cell} cell The provided cell
428 * @param {Cell} cell The provided cell
431 * @return {Cell} the next cell or null if no cell was found.
429 * @return {Cell} the next cell or null if no cell was found.
432 */
430 */
433 Notebook.prototype.get_next_cell = function (cell) {
431 Notebook.prototype.get_next_cell = function (cell) {
434 var result = null;
432 var result = null;
435 var index = this.find_cell_index(cell);
433 var index = this.find_cell_index(cell);
436 if (this.is_valid_cell_index(index+1)) {
434 if (this.is_valid_cell_index(index+1)) {
437 result = this.get_cell(index+1);
435 result = this.get_cell(index+1);
438 }
436 }
439 return result;
437 return result;
440 };
438 };
441
439
442 /**
440 /**
443 * Get the cell above a given cell.
441 * Get the cell above a given cell.
444 *
442 *
445 * @method get_prev_cell
443 * @method get_prev_cell
446 * @param {Cell} cell The provided cell
444 * @param {Cell} cell The provided cell
447 * @return {Cell} The previous cell or null if no cell was found.
445 * @return {Cell} The previous cell or null if no cell was found.
448 */
446 */
449 Notebook.prototype.get_prev_cell = function (cell) {
447 Notebook.prototype.get_prev_cell = function (cell) {
450 var result = null;
448 var result = null;
451 var index = this.find_cell_index(cell);
449 var index = this.find_cell_index(cell);
452 if (index !== null && index > 0) {
450 if (index !== null && index > 0) {
453 result = this.get_cell(index-1);
451 result = this.get_cell(index-1);
454 }
452 }
455 return result;
453 return result;
456 };
454 };
457
455
458 /**
456 /**
459 * Get the numeric index of a given cell.
457 * Get the numeric index of a given cell.
460 *
458 *
461 * @method find_cell_index
459 * @method find_cell_index
462 * @param {Cell} cell The provided cell
460 * @param {Cell} cell The provided cell
463 * @return {Number} The cell's numeric index or null if no cell was found.
461 * @return {Number} The cell's numeric index or null if no cell was found.
464 */
462 */
465 Notebook.prototype.find_cell_index = function (cell) {
463 Notebook.prototype.find_cell_index = function (cell) {
466 var result = null;
464 var result = null;
467 this.get_cell_elements().filter(function (index) {
465 this.get_cell_elements().filter(function (index) {
468 if ($(this).data("cell") === cell) {
466 if ($(this).data("cell") === cell) {
469 result = index;
467 result = index;
470 }
468 }
471 });
469 });
472 return result;
470 return result;
473 };
471 };
474
472
475 /**
473 /**
476 * Get a given index , or the selected index if none is provided.
474 * Get a given index , or the selected index if none is provided.
477 *
475 *
478 * @method index_or_selected
476 * @method index_or_selected
479 * @param {Number} index A cell's index
477 * @param {Number} index A cell's index
480 * @return {Number} The given index, or selected index if none is provided.
478 * @return {Number} The given index, or selected index if none is provided.
481 */
479 */
482 Notebook.prototype.index_or_selected = function (index) {
480 Notebook.prototype.index_or_selected = function (index) {
483 var i;
481 var i;
484 if (index === undefined || index === null) {
482 if (index === undefined || index === null) {
485 i = this.get_selected_index();
483 i = this.get_selected_index();
486 if (i === null) {
484 if (i === null) {
487 i = 0;
485 i = 0;
488 }
486 }
489 } else {
487 } else {
490 i = index;
488 i = index;
491 }
489 }
492 return i;
490 return i;
493 };
491 };
494
492
495 /**
493 /**
496 * Get the currently selected cell.
494 * Get the currently selected cell.
497 * @method get_selected_cell
495 * @method get_selected_cell
498 * @return {Cell} The selected cell
496 * @return {Cell} The selected cell
499 */
497 */
500 Notebook.prototype.get_selected_cell = function () {
498 Notebook.prototype.get_selected_cell = function () {
501 var index = this.get_selected_index();
499 var index = this.get_selected_index();
502 return this.get_cell(index);
500 return this.get_cell(index);
503 };
501 };
504
502
505 /**
503 /**
506 * Check whether a cell index is valid.
504 * Check whether a cell index is valid.
507 *
505 *
508 * @method is_valid_cell_index
506 * @method is_valid_cell_index
509 * @param {Number} index A cell index
507 * @param {Number} index A cell index
510 * @return True if the index is valid, false otherwise
508 * @return True if the index is valid, false otherwise
511 */
509 */
512 Notebook.prototype.is_valid_cell_index = function (index) {
510 Notebook.prototype.is_valid_cell_index = function (index) {
513 if (index !== null && index >= 0 && index < this.ncells()) {
511 if (index !== null && index >= 0 && index < this.ncells()) {
514 return true;
512 return true;
515 } else {
513 } else {
516 return false;
514 return false;
517 }
515 }
518 };
516 };
519
517
520 /**
518 /**
521 * Get the index of the currently selected cell.
519 * Get the index of the currently selected cell.
522
520
523 * @method get_selected_index
521 * @method get_selected_index
524 * @return {Number} The selected cell's numeric index
522 * @return {Number} The selected cell's numeric index
525 */
523 */
526 Notebook.prototype.get_selected_index = function () {
524 Notebook.prototype.get_selected_index = function () {
527 var result = null;
525 var result = null;
528 this.get_cell_elements().filter(function (index) {
526 this.get_cell_elements().filter(function (index) {
529 if ($(this).data("cell").selected === true) {
527 if ($(this).data("cell").selected === true) {
530 result = index;
528 result = index;
531 }
529 }
532 });
530 });
533 return result;
531 return result;
534 };
532 };
535
533
536
534
537 // Cell selection.
535 // Cell selection.
538
536
539 /**
537 /**
540 * Programmatically select a cell.
538 * Programmatically select a cell.
541 *
539 *
542 * @method select
540 * @method select
543 * @param {Number} index A cell's index
541 * @param {Number} index A cell's index
544 * @return {Notebook} This notebook
542 * @return {Notebook} This notebook
545 */
543 */
546 Notebook.prototype.select = function (index) {
544 Notebook.prototype.select = function (index) {
547 if (this.is_valid_cell_index(index)) {
545 if (this.is_valid_cell_index(index)) {
548 var sindex = this.get_selected_index();
546 var sindex = this.get_selected_index();
549 if (sindex !== null && index !== sindex) {
547 if (sindex !== null && index !== sindex) {
550 // If we are about to select a different cell, make sure we are
548 // If we are about to select a different cell, make sure we are
551 // first in command mode.
549 // first in command mode.
552 if (this.mode !== 'command') {
550 if (this.mode !== 'command') {
553 this.command_mode();
551 this.command_mode();
554 }
552 }
555 this.get_cell(sindex).unselect();
553 this.get_cell(sindex).unselect();
556 }
554 }
557 var cell = this.get_cell(index);
555 var cell = this.get_cell(index);
558 cell.select();
556 cell.select();
559 if (cell.cell_type === 'heading') {
557 if (cell.cell_type === 'heading') {
560 this.events.trigger('selected_cell_type_changed.Notebook',
558 this.events.trigger('selected_cell_type_changed.Notebook',
561 {'cell_type':cell.cell_type,level:cell.level}
559 {'cell_type':cell.cell_type,level:cell.level}
562 );
560 );
563 } else {
561 } else {
564 this.events.trigger('selected_cell_type_changed.Notebook',
562 this.events.trigger('selected_cell_type_changed.Notebook',
565 {'cell_type':cell.cell_type}
563 {'cell_type':cell.cell_type}
566 );
564 );
567 }
565 }
568 }
566 }
569 return this;
567 return this;
570 };
568 };
571
569
572 /**
570 /**
573 * Programmatically select the next cell.
571 * Programmatically select the next cell.
574 *
572 *
575 * @method select_next
573 * @method select_next
576 * @return {Notebook} This notebook
574 * @return {Notebook} This notebook
577 */
575 */
578 Notebook.prototype.select_next = function () {
576 Notebook.prototype.select_next = function () {
579 var index = this.get_selected_index();
577 var index = this.get_selected_index();
580 this.select(index+1);
578 this.select(index+1);
581 return this;
579 return this;
582 };
580 };
583
581
584 /**
582 /**
585 * Programmatically select the previous cell.
583 * Programmatically select the previous cell.
586 *
584 *
587 * @method select_prev
585 * @method select_prev
588 * @return {Notebook} This notebook
586 * @return {Notebook} This notebook
589 */
587 */
590 Notebook.prototype.select_prev = function () {
588 Notebook.prototype.select_prev = function () {
591 var index = this.get_selected_index();
589 var index = this.get_selected_index();
592 this.select(index-1);
590 this.select(index-1);
593 return this;
591 return this;
594 };
592 };
595
593
596
594
597 // Edit/Command mode
595 // Edit/Command mode
598
596
599 /**
597 /**
600 * Gets the index of the cell that is in edit mode.
598 * Gets the index of the cell that is in edit mode.
601 *
599 *
602 * @method get_edit_index
600 * @method get_edit_index
603 *
601 *
604 * @return index {int}
602 * @return index {int}
605 **/
603 **/
606 Notebook.prototype.get_edit_index = function () {
604 Notebook.prototype.get_edit_index = function () {
607 var result = null;
605 var result = null;
608 this.get_cell_elements().filter(function (index) {
606 this.get_cell_elements().filter(function (index) {
609 if ($(this).data("cell").mode === 'edit') {
607 if ($(this).data("cell").mode === 'edit') {
610 result = index;
608 result = index;
611 }
609 }
612 });
610 });
613 return result;
611 return result;
614 };
612 };
615
613
616 /**
614 /**
617 * Handle when a a cell blurs and the notebook should enter command mode.
615 * Handle when a a cell blurs and the notebook should enter command mode.
618 *
616 *
619 * @method handle_command_mode
617 * @method handle_command_mode
620 * @param [cell] {Cell} Cell to enter command mode on.
618 * @param [cell] {Cell} Cell to enter command mode on.
621 **/
619 **/
622 Notebook.prototype.handle_command_mode = function (cell) {
620 Notebook.prototype.handle_command_mode = function (cell) {
623 if (this.mode !== 'command') {
621 if (this.mode !== 'command') {
624 cell.command_mode();
622 cell.command_mode();
625 this.mode = 'command';
623 this.mode = 'command';
626 this.events.trigger('command_mode.Notebook');
624 this.events.trigger('command_mode.Notebook');
627 this.keyboard_manager.command_mode();
625 this.keyboard_manager.command_mode();
628 }
626 }
629 };
627 };
630
628
631 /**
629 /**
632 * Make the notebook enter command mode.
630 * Make the notebook enter command mode.
633 *
631 *
634 * @method command_mode
632 * @method command_mode
635 **/
633 **/
636 Notebook.prototype.command_mode = function () {
634 Notebook.prototype.command_mode = function () {
637 var cell = this.get_cell(this.get_edit_index());
635 var cell = this.get_cell(this.get_edit_index());
638 if (cell && this.mode !== 'command') {
636 if (cell && this.mode !== 'command') {
639 // We don't call cell.command_mode, but rather call cell.focus_cell()
637 // We don't call cell.command_mode, but rather call cell.focus_cell()
640 // which will blur and CM editor and trigger the call to
638 // which will blur and CM editor and trigger the call to
641 // handle_command_mode.
639 // handle_command_mode.
642 cell.focus_cell();
640 cell.focus_cell();
643 }
641 }
644 };
642 };
645
643
646 /**
644 /**
647 * Handle when a cell fires it's edit_mode event.
645 * Handle when a cell fires it's edit_mode event.
648 *
646 *
649 * @method handle_edit_mode
647 * @method handle_edit_mode
650 * @param [cell] {Cell} Cell to enter edit mode on.
648 * @param [cell] {Cell} Cell to enter edit mode on.
651 **/
649 **/
652 Notebook.prototype.handle_edit_mode = function (cell) {
650 Notebook.prototype.handle_edit_mode = function (cell) {
653 if (cell && this.mode !== 'edit') {
651 if (cell && this.mode !== 'edit') {
654 cell.edit_mode();
652 cell.edit_mode();
655 this.mode = 'edit';
653 this.mode = 'edit';
656 this.events.trigger('edit_mode.Notebook');
654 this.events.trigger('edit_mode.Notebook');
657 this.keyboard_manager.edit_mode();
655 this.keyboard_manager.edit_mode();
658 }
656 }
659 };
657 };
660
658
661 /**
659 /**
662 * Make a cell enter edit mode.
660 * Make a cell enter edit mode.
663 *
661 *
664 * @method edit_mode
662 * @method edit_mode
665 **/
663 **/
666 Notebook.prototype.edit_mode = function () {
664 Notebook.prototype.edit_mode = function () {
667 var cell = this.get_selected_cell();
665 var cell = this.get_selected_cell();
668 if (cell && this.mode !== 'edit') {
666 if (cell && this.mode !== 'edit') {
669 cell.unrender();
667 cell.unrender();
670 cell.focus_editor();
668 cell.focus_editor();
671 }
669 }
672 };
670 };
673
671
674 /**
672 /**
675 * Focus the currently selected cell.
673 * Focus the currently selected cell.
676 *
674 *
677 * @method focus_cell
675 * @method focus_cell
678 **/
676 **/
679 Notebook.prototype.focus_cell = function () {
677 Notebook.prototype.focus_cell = function () {
680 var cell = this.get_selected_cell();
678 var cell = this.get_selected_cell();
681 if (cell === null) {return;} // No cell is selected
679 if (cell === null) {return;} // No cell is selected
682 cell.focus_cell();
680 cell.focus_cell();
683 };
681 };
684
682
685 // Cell movement
683 // Cell movement
686
684
687 /**
685 /**
688 * Move given (or selected) cell up and select it.
686 * Move given (or selected) cell up and select it.
689 *
687 *
690 * @method move_cell_up
688 * @method move_cell_up
691 * @param [index] {integer} cell index
689 * @param [index] {integer} cell index
692 * @return {Notebook} This notebook
690 * @return {Notebook} This notebook
693 **/
691 **/
694 Notebook.prototype.move_cell_up = function (index) {
692 Notebook.prototype.move_cell_up = function (index) {
695 var i = this.index_or_selected(index);
693 var i = this.index_or_selected(index);
696 if (this.is_valid_cell_index(i) && i > 0) {
694 if (this.is_valid_cell_index(i) && i > 0) {
697 var pivot = this.get_cell_element(i-1);
695 var pivot = this.get_cell_element(i-1);
698 var tomove = this.get_cell_element(i);
696 var tomove = this.get_cell_element(i);
699 if (pivot !== null && tomove !== null) {
697 if (pivot !== null && tomove !== null) {
700 tomove.detach();
698 tomove.detach();
701 pivot.before(tomove);
699 pivot.before(tomove);
702 this.select(i-1);
700 this.select(i-1);
703 var cell = this.get_selected_cell();
701 var cell = this.get_selected_cell();
704 cell.focus_cell();
702 cell.focus_cell();
705 }
703 }
706 this.set_dirty(true);
704 this.set_dirty(true);
707 }
705 }
708 return this;
706 return this;
709 };
707 };
710
708
711
709
712 /**
710 /**
713 * Move given (or selected) cell down and select it
711 * Move given (or selected) cell down and select it
714 *
712 *
715 * @method move_cell_down
713 * @method move_cell_down
716 * @param [index] {integer} cell index
714 * @param [index] {integer} cell index
717 * @return {Notebook} This notebook
715 * @return {Notebook} This notebook
718 **/
716 **/
719 Notebook.prototype.move_cell_down = function (index) {
717 Notebook.prototype.move_cell_down = function (index) {
720 var i = this.index_or_selected(index);
718 var i = this.index_or_selected(index);
721 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
719 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
722 var pivot = this.get_cell_element(i+1);
720 var pivot = this.get_cell_element(i+1);
723 var tomove = this.get_cell_element(i);
721 var tomove = this.get_cell_element(i);
724 if (pivot !== null && tomove !== null) {
722 if (pivot !== null && tomove !== null) {
725 tomove.detach();
723 tomove.detach();
726 pivot.after(tomove);
724 pivot.after(tomove);
727 this.select(i+1);
725 this.select(i+1);
728 var cell = this.get_selected_cell();
726 var cell = this.get_selected_cell();
729 cell.focus_cell();
727 cell.focus_cell();
730 }
728 }
731 }
729 }
732 this.set_dirty();
730 this.set_dirty();
733 return this;
731 return this;
734 };
732 };
735
733
736
734
737 // Insertion, deletion.
735 // Insertion, deletion.
738
736
739 /**
737 /**
740 * Delete a cell from the notebook.
738 * Delete a cell from the notebook.
741 *
739 *
742 * @method delete_cell
740 * @method delete_cell
743 * @param [index] A cell's numeric index
741 * @param [index] A cell's numeric index
744 * @return {Notebook} This notebook
742 * @return {Notebook} This notebook
745 */
743 */
746 Notebook.prototype.delete_cell = function (index) {
744 Notebook.prototype.delete_cell = function (index) {
747 var i = this.index_or_selected(index);
745 var i = this.index_or_selected(index);
748 var cell = this.get_cell(i);
746 var cell = this.get_cell(i);
749 if (!cell.is_deletable()) {
747 if (!cell.is_deletable()) {
750 return this;
748 return this;
751 }
749 }
752
750
753 this.undelete_backup = cell.toJSON();
751 this.undelete_backup = cell.toJSON();
754 $('#undelete_cell').removeClass('disabled');
752 $('#undelete_cell').removeClass('disabled');
755 if (this.is_valid_cell_index(i)) {
753 if (this.is_valid_cell_index(i)) {
756 var old_ncells = this.ncells();
754 var old_ncells = this.ncells();
757 var ce = this.get_cell_element(i);
755 var ce = this.get_cell_element(i);
758 ce.remove();
756 ce.remove();
759 if (i === 0) {
757 if (i === 0) {
760 // Always make sure we have at least one cell.
758 // Always make sure we have at least one cell.
761 if (old_ncells === 1) {
759 if (old_ncells === 1) {
762 this.insert_cell_below('code');
760 this.insert_cell_below('code');
763 }
761 }
764 this.select(0);
762 this.select(0);
765 this.undelete_index = 0;
763 this.undelete_index = 0;
766 this.undelete_below = false;
764 this.undelete_below = false;
767 } else if (i === old_ncells-1 && i !== 0) {
765 } else if (i === old_ncells-1 && i !== 0) {
768 this.select(i-1);
766 this.select(i-1);
769 this.undelete_index = i - 1;
767 this.undelete_index = i - 1;
770 this.undelete_below = true;
768 this.undelete_below = true;
771 } else {
769 } else {
772 this.select(i);
770 this.select(i);
773 this.undelete_index = i;
771 this.undelete_index = i;
774 this.undelete_below = false;
772 this.undelete_below = false;
775 }
773 }
776 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
774 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
777 this.set_dirty(true);
775 this.set_dirty(true);
778 }
776 }
779 return this;
777 return this;
780 };
778 };
781
779
782 /**
780 /**
783 * Restore the most recently deleted cell.
781 * Restore the most recently deleted cell.
784 *
782 *
785 * @method undelete
783 * @method undelete
786 */
784 */
787 Notebook.prototype.undelete_cell = function() {
785 Notebook.prototype.undelete_cell = function() {
788 if (this.undelete_backup !== null && this.undelete_index !== null) {
786 if (this.undelete_backup !== null && this.undelete_index !== null) {
789 var current_index = this.get_selected_index();
787 var current_index = this.get_selected_index();
790 if (this.undelete_index < current_index) {
788 if (this.undelete_index < current_index) {
791 current_index = current_index + 1;
789 current_index = current_index + 1;
792 }
790 }
793 if (this.undelete_index >= this.ncells()) {
791 if (this.undelete_index >= this.ncells()) {
794 this.select(this.ncells() - 1);
792 this.select(this.ncells() - 1);
795 }
793 }
796 else {
794 else {
797 this.select(this.undelete_index);
795 this.select(this.undelete_index);
798 }
796 }
799 var cell_data = this.undelete_backup;
797 var cell_data = this.undelete_backup;
800 var new_cell = null;
798 var new_cell = null;
801 if (this.undelete_below) {
799 if (this.undelete_below) {
802 new_cell = this.insert_cell_below(cell_data.cell_type);
800 new_cell = this.insert_cell_below(cell_data.cell_type);
803 } else {
801 } else {
804 new_cell = this.insert_cell_above(cell_data.cell_type);
802 new_cell = this.insert_cell_above(cell_data.cell_type);
805 }
803 }
806 new_cell.fromJSON(cell_data);
804 new_cell.fromJSON(cell_data);
807 if (this.undelete_below) {
805 if (this.undelete_below) {
808 this.select(current_index+1);
806 this.select(current_index+1);
809 } else {
807 } else {
810 this.select(current_index);
808 this.select(current_index);
811 }
809 }
812 this.undelete_backup = null;
810 this.undelete_backup = null;
813 this.undelete_index = null;
811 this.undelete_index = null;
814 }
812 }
815 $('#undelete_cell').addClass('disabled');
813 $('#undelete_cell').addClass('disabled');
816 };
814 };
817
815
818 /**
816 /**
819 * Insert a cell so that after insertion the cell is at given index.
817 * Insert a cell so that after insertion the cell is at given index.
820 *
818 *
821 * If cell type is not provided, it will default to the type of the
819 * If cell type is not provided, it will default to the type of the
822 * currently active cell.
820 * currently active cell.
823 *
821 *
824 * Similar to insert_above, but index parameter is mandatory
822 * Similar to insert_above, but index parameter is mandatory
825 *
823 *
826 * Index will be brought back into the accessible range [0,n]
824 * Index will be brought back into the accessible range [0,n]
827 *
825 *
828 * @method insert_cell_at_index
826 * @method insert_cell_at_index
829 * @param [type] {string} in ['code','markdown','heading'], defaults to 'code'
827 * @param [type] {string} in ['code','markdown','heading'], defaults to 'code'
830 * @param [index] {int} a valid index where to insert cell
828 * @param [index] {int} a valid index where to insert cell
831 *
829 *
832 * @return cell {cell|null} created cell or null
830 * @return cell {cell|null} created cell or null
833 **/
831 **/
834 Notebook.prototype.insert_cell_at_index = function(type, index){
832 Notebook.prototype.insert_cell_at_index = function(type, index){
835
833
836 var ncells = this.ncells();
834 var ncells = this.ncells();
837 index = Math.min(index, ncells);
835 index = Math.min(index, ncells);
838 index = Math.max(index, 0);
836 index = Math.max(index, 0);
839 var cell = null;
837 var cell = null;
840 type = type || this.default_cell_type;
838 type = type || this.default_cell_type;
841 if (type === 'above') {
839 if (type === 'above') {
842 if (index > 0) {
840 if (index > 0) {
843 type = this.get_cell(index-1).cell_type;
841 type = this.get_cell(index-1).cell_type;
844 } else {
842 } else {
845 type = 'code';
843 type = 'code';
846 }
844 }
847 } else if (type === 'below') {
845 } else if (type === 'below') {
848 if (index < ncells) {
846 if (index < ncells) {
849 type = this.get_cell(index).cell_type;
847 type = this.get_cell(index).cell_type;
850 } else {
848 } else {
851 type = 'code';
849 type = 'code';
852 }
850 }
853 } else if (type === 'selected') {
851 } else if (type === 'selected') {
854 type = this.get_selected_cell().cell_type;
852 type = this.get_selected_cell().cell_type;
855 }
853 }
856
854
857 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
855 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
858 var cell_options = {
856 var cell_options = {
859 events: this.events,
857 events: this.events,
860 config: this.config,
858 config: this.config,
861 keyboard_manager: this.keyboard_manager,
859 keyboard_manager: this.keyboard_manager,
862 notebook: this,
860 notebook: this,
863 tooltip: this.tooltip,
861 tooltip: this.tooltip,
864 };
862 };
865 if (type === 'code') {
863 if (type === 'code') {
866 cell = new codecell.CodeCell(this.kernel, cell_options);
864 cell = new codecell.CodeCell(this.kernel, cell_options);
867 cell.set_input_prompt();
865 cell.set_input_prompt();
868 } else if (type === 'markdown') {
866 } else if (type === 'markdown') {
869 cell = new textcell.MarkdownCell(cell_options);
867 cell = new textcell.MarkdownCell(cell_options);
870 } else if (type === 'raw') {
868 } else if (type === 'raw') {
871 cell = new textcell.RawCell(cell_options);
869 cell = new textcell.RawCell(cell_options);
872 } else if (type === 'heading') {
870 } else if (type === 'heading') {
873 cell = new textcell.HeadingCell(cell_options);
871 cell = new textcell.HeadingCell(cell_options);
874 }
872 }
875
873
876 if(this._insert_element_at_index(cell.element,index)) {
874 if(this._insert_element_at_index(cell.element,index)) {
877 cell.render();
875 cell.render();
878 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
876 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
879 cell.refresh();
877 cell.refresh();
880 // We used to select the cell after we refresh it, but there
878 // We used to select the cell after we refresh it, but there
881 // are now cases were this method is called where select is
879 // are now cases were this method is called where select is
882 // not appropriate. The selection logic should be handled by the
880 // not appropriate. The selection logic should be handled by the
883 // caller of the the top level insert_cell methods.
881 // caller of the the top level insert_cell methods.
884 this.set_dirty(true);
882 this.set_dirty(true);
885 }
883 }
886 }
884 }
887 return cell;
885 return cell;
888
886
889 };
887 };
890
888
891 /**
889 /**
892 * Insert an element at given cell index.
890 * Insert an element at given cell index.
893 *
891 *
894 * @method _insert_element_at_index
892 * @method _insert_element_at_index
895 * @param element {dom_element} a cell element
893 * @param element {dom_element} a cell element
896 * @param [index] {int} a valid index where to inser cell
894 * @param [index] {int} a valid index where to inser cell
897 * @private
895 * @private
898 *
896 *
899 * return true if everything whent fine.
897 * return true if everything whent fine.
900 **/
898 **/
901 Notebook.prototype._insert_element_at_index = function(element, index){
899 Notebook.prototype._insert_element_at_index = function(element, index){
902 if (element === undefined){
900 if (element === undefined){
903 return false;
901 return false;
904 }
902 }
905
903
906 var ncells = this.ncells();
904 var ncells = this.ncells();
907
905
908 if (ncells === 0) {
906 if (ncells === 0) {
909 // special case append if empty
907 // special case append if empty
910 this.element.find('div.end_space').before(element);
908 this.element.find('div.end_space').before(element);
911 } else if ( ncells === index ) {
909 } else if ( ncells === index ) {
912 // special case append it the end, but not empty
910 // special case append it the end, but not empty
913 this.get_cell_element(index-1).after(element);
911 this.get_cell_element(index-1).after(element);
914 } else if (this.is_valid_cell_index(index)) {
912 } else if (this.is_valid_cell_index(index)) {
915 // otherwise always somewhere to append to
913 // otherwise always somewhere to append to
916 this.get_cell_element(index).before(element);
914 this.get_cell_element(index).before(element);
917 } else {
915 } else {
918 return false;
916 return false;
919 }
917 }
920
918
921 if (this.undelete_index !== null && index <= this.undelete_index) {
919 if (this.undelete_index !== null && index <= this.undelete_index) {
922 this.undelete_index = this.undelete_index + 1;
920 this.undelete_index = this.undelete_index + 1;
923 this.set_dirty(true);
921 this.set_dirty(true);
924 }
922 }
925 return true;
923 return true;
926 };
924 };
927
925
928 /**
926 /**
929 * Insert a cell of given type above given index, or at top
927 * Insert a cell of given type above given index, or at top
930 * of notebook if index smaller than 0.
928 * of notebook if index smaller than 0.
931 *
929 *
932 * default index value is the one of currently selected cell
930 * default index value is the one of currently selected cell
933 *
931 *
934 * @method insert_cell_above
932 * @method insert_cell_above
935 * @param [type] {string} cell type
933 * @param [type] {string} cell type
936 * @param [index] {integer}
934 * @param [index] {integer}
937 *
935 *
938 * @return handle to created cell or null
936 * @return handle to created cell or null
939 **/
937 **/
940 Notebook.prototype.insert_cell_above = function (type, index) {
938 Notebook.prototype.insert_cell_above = function (type, index) {
941 index = this.index_or_selected(index);
939 index = this.index_or_selected(index);
942 return this.insert_cell_at_index(type, index);
940 return this.insert_cell_at_index(type, index);
943 };
941 };
944
942
945 /**
943 /**
946 * Insert a cell of given type below given index, or at bottom
944 * Insert a cell of given type below given index, or at bottom
947 * of notebook if index greater than number of cells
945 * of notebook if index greater than number of cells
948 *
946 *
949 * default index value is the one of currently selected cell
947 * default index value is the one of currently selected cell
950 *
948 *
951 * @method insert_cell_below
949 * @method insert_cell_below
952 * @param [type] {string} cell type
950 * @param [type] {string} cell type
953 * @param [index] {integer}
951 * @param [index] {integer}
954 *
952 *
955 * @return handle to created cell or null
953 * @return handle to created cell or null
956 *
954 *
957 **/
955 **/
958 Notebook.prototype.insert_cell_below = function (type, index) {
956 Notebook.prototype.insert_cell_below = function (type, index) {
959 index = this.index_or_selected(index);
957 index = this.index_or_selected(index);
960 return this.insert_cell_at_index(type, index+1);
958 return this.insert_cell_at_index(type, index+1);
961 };
959 };
962
960
963
961
964 /**
962 /**
965 * Insert cell at end of notebook
963 * Insert cell at end of notebook
966 *
964 *
967 * @method insert_cell_at_bottom
965 * @method insert_cell_at_bottom
968 * @param {String} type cell type
966 * @param {String} type cell type
969 *
967 *
970 * @return the added cell; or null
968 * @return the added cell; or null
971 **/
969 **/
972 Notebook.prototype.insert_cell_at_bottom = function (type){
970 Notebook.prototype.insert_cell_at_bottom = function (type){
973 var len = this.ncells();
971 var len = this.ncells();
974 return this.insert_cell_below(type,len-1);
972 return this.insert_cell_below(type,len-1);
975 };
973 };
976
974
977 /**
975 /**
978 * Turn a cell into a code cell.
976 * Turn a cell into a code cell.
979 *
977 *
980 * @method to_code
978 * @method to_code
981 * @param {Number} [index] A cell's index
979 * @param {Number} [index] A cell's index
982 */
980 */
983 Notebook.prototype.to_code = function (index) {
981 Notebook.prototype.to_code = function (index) {
984 var i = this.index_or_selected(index);
982 var i = this.index_or_selected(index);
985 if (this.is_valid_cell_index(i)) {
983 if (this.is_valid_cell_index(i)) {
986 var source_cell = this.get_cell(i);
984 var source_cell = this.get_cell(i);
987 if (!(source_cell instanceof codecell.CodeCell)) {
985 if (!(source_cell instanceof codecell.CodeCell)) {
988 var target_cell = this.insert_cell_below('code',i);
986 var target_cell = this.insert_cell_below('code',i);
989 var text = source_cell.get_text();
987 var text = source_cell.get_text();
990 if (text === source_cell.placeholder) {
988 if (text === source_cell.placeholder) {
991 text = '';
989 text = '';
992 }
990 }
993 //metadata
991 //metadata
994 target_cell.metadata = source_cell.metadata;
992 target_cell.metadata = source_cell.metadata;
995
993
996 target_cell.set_text(text);
994 target_cell.set_text(text);
997 // make this value the starting point, so that we can only undo
995 // make this value the starting point, so that we can only undo
998 // to this state, instead of a blank cell
996 // to this state, instead of a blank cell
999 target_cell.code_mirror.clearHistory();
997 target_cell.code_mirror.clearHistory();
1000 source_cell.element.remove();
998 source_cell.element.remove();
1001 this.select(i);
999 this.select(i);
1002 var cursor = source_cell.code_mirror.getCursor();
1000 var cursor = source_cell.code_mirror.getCursor();
1003 target_cell.code_mirror.setCursor(cursor);
1001 target_cell.code_mirror.setCursor(cursor);
1004 this.set_dirty(true);
1002 this.set_dirty(true);
1005 }
1003 }
1006 }
1004 }
1007 };
1005 };
1008
1006
1009 /**
1007 /**
1010 * Turn a cell into a Markdown cell.
1008 * Turn a cell into a Markdown cell.
1011 *
1009 *
1012 * @method to_markdown
1010 * @method to_markdown
1013 * @param {Number} [index] A cell's index
1011 * @param {Number} [index] A cell's index
1014 */
1012 */
1015 Notebook.prototype.to_markdown = function (index) {
1013 Notebook.prototype.to_markdown = function (index) {
1016 var i = this.index_or_selected(index);
1014 var i = this.index_or_selected(index);
1017 if (this.is_valid_cell_index(i)) {
1015 if (this.is_valid_cell_index(i)) {
1018 var source_cell = this.get_cell(i);
1016 var source_cell = this.get_cell(i);
1019
1017
1020 if (!(source_cell instanceof textcell.MarkdownCell)) {
1018 if (!(source_cell instanceof textcell.MarkdownCell)) {
1021 var target_cell = this.insert_cell_below('markdown',i);
1019 var target_cell = this.insert_cell_below('markdown',i);
1022 var text = source_cell.get_text();
1020 var text = source_cell.get_text();
1023
1021
1024 if (text === source_cell.placeholder) {
1022 if (text === source_cell.placeholder) {
1025 text = '';
1023 text = '';
1026 }
1024 }
1027 // metadata
1025 // metadata
1028 target_cell.metadata = source_cell.metadata
1026 target_cell.metadata = source_cell.metadata
1029 // We must show the editor before setting its contents
1027 // We must show the editor before setting its contents
1030 target_cell.unrender();
1028 target_cell.unrender();
1031 target_cell.set_text(text);
1029 target_cell.set_text(text);
1032 // make this value the starting point, so that we can only undo
1030 // make this value the starting point, so that we can only undo
1033 // to this state, instead of a blank cell
1031 // to this state, instead of a blank cell
1034 target_cell.code_mirror.clearHistory();
1032 target_cell.code_mirror.clearHistory();
1035 source_cell.element.remove();
1033 source_cell.element.remove();
1036 this.select(i);
1034 this.select(i);
1037 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1035 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1038 target_cell.render();
1036 target_cell.render();
1039 }
1037 }
1040 var cursor = source_cell.code_mirror.getCursor();
1038 var cursor = source_cell.code_mirror.getCursor();
1041 target_cell.code_mirror.setCursor(cursor);
1039 target_cell.code_mirror.setCursor(cursor);
1042 this.set_dirty(true);
1040 this.set_dirty(true);
1043 }
1041 }
1044 }
1042 }
1045 };
1043 };
1046
1044
1047 /**
1045 /**
1048 * Turn a cell into a raw text cell.
1046 * Turn a cell into a raw text cell.
1049 *
1047 *
1050 * @method to_raw
1048 * @method to_raw
1051 * @param {Number} [index] A cell's index
1049 * @param {Number} [index] A cell's index
1052 */
1050 */
1053 Notebook.prototype.to_raw = function (index) {
1051 Notebook.prototype.to_raw = function (index) {
1054 var i = this.index_or_selected(index);
1052 var i = this.index_or_selected(index);
1055 if (this.is_valid_cell_index(i)) {
1053 if (this.is_valid_cell_index(i)) {
1056 var target_cell = null;
1054 var target_cell = null;
1057 var source_cell = this.get_cell(i);
1055 var source_cell = this.get_cell(i);
1058
1056
1059 if (!(source_cell instanceof textcell.RawCell)) {
1057 if (!(source_cell instanceof textcell.RawCell)) {
1060 target_cell = this.insert_cell_below('raw',i);
1058 target_cell = this.insert_cell_below('raw',i);
1061 var text = source_cell.get_text();
1059 var text = source_cell.get_text();
1062 if (text === source_cell.placeholder) {
1060 if (text === source_cell.placeholder) {
1063 text = '';
1061 text = '';
1064 }
1062 }
1065 //metadata
1063 //metadata
1066 target_cell.metadata = source_cell.metadata;
1064 target_cell.metadata = source_cell.metadata;
1067 // We must show the editor before setting its contents
1065 // We must show the editor before setting its contents
1068 target_cell.unrender();
1066 target_cell.unrender();
1069 target_cell.set_text(text);
1067 target_cell.set_text(text);
1070 // make this value the starting point, so that we can only undo
1068 // make this value the starting point, so that we can only undo
1071 // to this state, instead of a blank cell
1069 // to this state, instead of a blank cell
1072 target_cell.code_mirror.clearHistory();
1070 target_cell.code_mirror.clearHistory();
1073 source_cell.element.remove();
1071 source_cell.element.remove();
1074 this.select(i);
1072 this.select(i);
1075 var cursor = source_cell.code_mirror.getCursor();
1073 var cursor = source_cell.code_mirror.getCursor();
1076 target_cell.code_mirror.setCursor(cursor);
1074 target_cell.code_mirror.setCursor(cursor);
1077 this.set_dirty(true);
1075 this.set_dirty(true);
1078 }
1076 }
1079 }
1077 }
1080 };
1078 };
1081
1079
1082 /**
1080 /**
1083 * Turn a cell into a heading cell.
1081 * Turn a cell into a heading cell.
1084 *
1082 *
1085 * @method to_heading
1083 * @method to_heading
1086 * @param {Number} [index] A cell's index
1084 * @param {Number} [index] A cell's index
1087 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
1085 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
1088 */
1086 */
1089 Notebook.prototype.to_heading = function (index, level) {
1087 Notebook.prototype.to_heading = function (index, level) {
1090 level = level || 1;
1088 level = level || 1;
1091 var i = this.index_or_selected(index);
1089 var i = this.index_or_selected(index);
1092 if (this.is_valid_cell_index(i)) {
1090 if (this.is_valid_cell_index(i)) {
1093 var source_cell = this.get_cell(i);
1091 var source_cell = this.get_cell(i);
1094 var target_cell = null;
1092 var target_cell = null;
1095 if (source_cell instanceof textcell.HeadingCell) {
1093 if (source_cell instanceof textcell.HeadingCell) {
1096 source_cell.set_level(level);
1094 source_cell.set_level(level);
1097 } else {
1095 } else {
1098 target_cell = this.insert_cell_below('heading',i);
1096 target_cell = this.insert_cell_below('heading',i);
1099 var text = source_cell.get_text();
1097 var text = source_cell.get_text();
1100 if (text === source_cell.placeholder) {
1098 if (text === source_cell.placeholder) {
1101 text = '';
1099 text = '';
1102 }
1100 }
1103 //metadata
1101 //metadata
1104 target_cell.metadata = source_cell.metadata;
1102 target_cell.metadata = source_cell.metadata;
1105 // We must show the editor before setting its contents
1103 // We must show the editor before setting its contents
1106 target_cell.set_level(level);
1104 target_cell.set_level(level);
1107 target_cell.unrender();
1105 target_cell.unrender();
1108 target_cell.set_text(text);
1106 target_cell.set_text(text);
1109 // make this value the starting point, so that we can only undo
1107 // make this value the starting point, so that we can only undo
1110 // to this state, instead of a blank cell
1108 // to this state, instead of a blank cell
1111 target_cell.code_mirror.clearHistory();
1109 target_cell.code_mirror.clearHistory();
1112 source_cell.element.remove();
1110 source_cell.element.remove();
1113 this.select(i);
1111 this.select(i);
1114 var cursor = source_cell.code_mirror.getCursor();
1112 var cursor = source_cell.code_mirror.getCursor();
1115 target_cell.code_mirror.setCursor(cursor);
1113 target_cell.code_mirror.setCursor(cursor);
1116 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1114 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1117 target_cell.render();
1115 target_cell.render();
1118 }
1116 }
1119 }
1117 }
1120 this.set_dirty(true);
1118 this.set_dirty(true);
1121 this.events.trigger('selected_cell_type_changed.Notebook',
1119 this.events.trigger('selected_cell_type_changed.Notebook',
1122 {'cell_type':'heading',level:level}
1120 {'cell_type':'heading',level:level}
1123 );
1121 );
1124 }
1122 }
1125 };
1123 };
1126
1124
1127
1125
1128 // Cut/Copy/Paste
1126 // Cut/Copy/Paste
1129
1127
1130 /**
1128 /**
1131 * Enable UI elements for pasting cells.
1129 * Enable UI elements for pasting cells.
1132 *
1130 *
1133 * @method enable_paste
1131 * @method enable_paste
1134 */
1132 */
1135 Notebook.prototype.enable_paste = function () {
1133 Notebook.prototype.enable_paste = function () {
1136 var that = this;
1134 var that = this;
1137 if (!this.paste_enabled) {
1135 if (!this.paste_enabled) {
1138 $('#paste_cell_replace').removeClass('disabled')
1136 $('#paste_cell_replace').removeClass('disabled')
1139 .on('click', function () {that.paste_cell_replace();});
1137 .on('click', function () {that.paste_cell_replace();});
1140 $('#paste_cell_above').removeClass('disabled')
1138 $('#paste_cell_above').removeClass('disabled')
1141 .on('click', function () {that.paste_cell_above();});
1139 .on('click', function () {that.paste_cell_above();});
1142 $('#paste_cell_below').removeClass('disabled')
1140 $('#paste_cell_below').removeClass('disabled')
1143 .on('click', function () {that.paste_cell_below();});
1141 .on('click', function () {that.paste_cell_below();});
1144 this.paste_enabled = true;
1142 this.paste_enabled = true;
1145 }
1143 }
1146 };
1144 };
1147
1145
1148 /**
1146 /**
1149 * Disable UI elements for pasting cells.
1147 * Disable UI elements for pasting cells.
1150 *
1148 *
1151 * @method disable_paste
1149 * @method disable_paste
1152 */
1150 */
1153 Notebook.prototype.disable_paste = function () {
1151 Notebook.prototype.disable_paste = function () {
1154 if (this.paste_enabled) {
1152 if (this.paste_enabled) {
1155 $('#paste_cell_replace').addClass('disabled').off('click');
1153 $('#paste_cell_replace').addClass('disabled').off('click');
1156 $('#paste_cell_above').addClass('disabled').off('click');
1154 $('#paste_cell_above').addClass('disabled').off('click');
1157 $('#paste_cell_below').addClass('disabled').off('click');
1155 $('#paste_cell_below').addClass('disabled').off('click');
1158 this.paste_enabled = false;
1156 this.paste_enabled = false;
1159 }
1157 }
1160 };
1158 };
1161
1159
1162 /**
1160 /**
1163 * Cut a cell.
1161 * Cut a cell.
1164 *
1162 *
1165 * @method cut_cell
1163 * @method cut_cell
1166 */
1164 */
1167 Notebook.prototype.cut_cell = function () {
1165 Notebook.prototype.cut_cell = function () {
1168 this.copy_cell();
1166 this.copy_cell();
1169 this.delete_cell();
1167 this.delete_cell();
1170 };
1168 };
1171
1169
1172 /**
1170 /**
1173 * Copy a cell.
1171 * Copy a cell.
1174 *
1172 *
1175 * @method copy_cell
1173 * @method copy_cell
1176 */
1174 */
1177 Notebook.prototype.copy_cell = function () {
1175 Notebook.prototype.copy_cell = function () {
1178 var cell = this.get_selected_cell();
1176 var cell = this.get_selected_cell();
1179 this.clipboard = cell.toJSON();
1177 this.clipboard = cell.toJSON();
1180 // remove undeletable status from the copied cell
1178 // remove undeletable status from the copied cell
1181 if (this.clipboard.metadata.deletable !== undefined) {
1179 if (this.clipboard.metadata.deletable !== undefined) {
1182 delete this.clipboard.metadata.deletable;
1180 delete this.clipboard.metadata.deletable;
1183 }
1181 }
1184 this.enable_paste();
1182 this.enable_paste();
1185 };
1183 };
1186
1184
1187 /**
1185 /**
1188 * Replace the selected cell with a cell in the clipboard.
1186 * Replace the selected cell with a cell in the clipboard.
1189 *
1187 *
1190 * @method paste_cell_replace
1188 * @method paste_cell_replace
1191 */
1189 */
1192 Notebook.prototype.paste_cell_replace = function () {
1190 Notebook.prototype.paste_cell_replace = function () {
1193 if (this.clipboard !== null && this.paste_enabled) {
1191 if (this.clipboard !== null && this.paste_enabled) {
1194 var cell_data = this.clipboard;
1192 var cell_data = this.clipboard;
1195 var new_cell = this.insert_cell_above(cell_data.cell_type);
1193 var new_cell = this.insert_cell_above(cell_data.cell_type);
1196 new_cell.fromJSON(cell_data);
1194 new_cell.fromJSON(cell_data);
1197 var old_cell = this.get_next_cell(new_cell);
1195 var old_cell = this.get_next_cell(new_cell);
1198 this.delete_cell(this.find_cell_index(old_cell));
1196 this.delete_cell(this.find_cell_index(old_cell));
1199 this.select(this.find_cell_index(new_cell));
1197 this.select(this.find_cell_index(new_cell));
1200 }
1198 }
1201 };
1199 };
1202
1200
1203 /**
1201 /**
1204 * Paste a cell from the clipboard above the selected cell.
1202 * Paste a cell from the clipboard above the selected cell.
1205 *
1203 *
1206 * @method paste_cell_above
1204 * @method paste_cell_above
1207 */
1205 */
1208 Notebook.prototype.paste_cell_above = function () {
1206 Notebook.prototype.paste_cell_above = function () {
1209 if (this.clipboard !== null && this.paste_enabled) {
1207 if (this.clipboard !== null && this.paste_enabled) {
1210 var cell_data = this.clipboard;
1208 var cell_data = this.clipboard;
1211 var new_cell = this.insert_cell_above(cell_data.cell_type);
1209 var new_cell = this.insert_cell_above(cell_data.cell_type);
1212 new_cell.fromJSON(cell_data);
1210 new_cell.fromJSON(cell_data);
1213 new_cell.focus_cell();
1211 new_cell.focus_cell();
1214 }
1212 }
1215 };
1213 };
1216
1214
1217 /**
1215 /**
1218 * Paste a cell from the clipboard below the selected cell.
1216 * Paste a cell from the clipboard below the selected cell.
1219 *
1217 *
1220 * @method paste_cell_below
1218 * @method paste_cell_below
1221 */
1219 */
1222 Notebook.prototype.paste_cell_below = function () {
1220 Notebook.prototype.paste_cell_below = function () {
1223 if (this.clipboard !== null && this.paste_enabled) {
1221 if (this.clipboard !== null && this.paste_enabled) {
1224 var cell_data = this.clipboard;
1222 var cell_data = this.clipboard;
1225 var new_cell = this.insert_cell_below(cell_data.cell_type);
1223 var new_cell = this.insert_cell_below(cell_data.cell_type);
1226 new_cell.fromJSON(cell_data);
1224 new_cell.fromJSON(cell_data);
1227 new_cell.focus_cell();
1225 new_cell.focus_cell();
1228 }
1226 }
1229 };
1227 };
1230
1228
1231 // Split/merge
1229 // Split/merge
1232
1230
1233 /**
1231 /**
1234 * Split the selected cell into two, at the cursor.
1232 * Split the selected cell into two, at the cursor.
1235 *
1233 *
1236 * @method split_cell
1234 * @method split_cell
1237 */
1235 */
1238 Notebook.prototype.split_cell = function () {
1236 Notebook.prototype.split_cell = function () {
1239 var mdc = textcell.MarkdownCell;
1237 var mdc = textcell.MarkdownCell;
1240 var rc = textcell.RawCell;
1238 var rc = textcell.RawCell;
1241 var cell = this.get_selected_cell();
1239 var cell = this.get_selected_cell();
1242 if (cell.is_splittable()) {
1240 if (cell.is_splittable()) {
1243 var texta = cell.get_pre_cursor();
1241 var texta = cell.get_pre_cursor();
1244 var textb = cell.get_post_cursor();
1242 var textb = cell.get_post_cursor();
1245 cell.set_text(textb);
1243 cell.set_text(textb);
1246 var new_cell = this.insert_cell_above(cell.cell_type);
1244 var new_cell = this.insert_cell_above(cell.cell_type);
1247 // Unrender the new cell so we can call set_text.
1245 // Unrender the new cell so we can call set_text.
1248 new_cell.unrender();
1246 new_cell.unrender();
1249 new_cell.set_text(texta);
1247 new_cell.set_text(texta);
1250 }
1248 }
1251 };
1249 };
1252
1250
1253 /**
1251 /**
1254 * Combine the selected cell into the cell above it.
1252 * Combine the selected cell into the cell above it.
1255 *
1253 *
1256 * @method merge_cell_above
1254 * @method merge_cell_above
1257 */
1255 */
1258 Notebook.prototype.merge_cell_above = function () {
1256 Notebook.prototype.merge_cell_above = function () {
1259 var mdc = textcell.MarkdownCell;
1257 var mdc = textcell.MarkdownCell;
1260 var rc = textcell.RawCell;
1258 var rc = textcell.RawCell;
1261 var index = this.get_selected_index();
1259 var index = this.get_selected_index();
1262 var cell = this.get_cell(index);
1260 var cell = this.get_cell(index);
1263 var render = cell.rendered;
1261 var render = cell.rendered;
1264 if (!cell.is_mergeable()) {
1262 if (!cell.is_mergeable()) {
1265 return;
1263 return;
1266 }
1264 }
1267 if (index > 0) {
1265 if (index > 0) {
1268 var upper_cell = this.get_cell(index-1);
1266 var upper_cell = this.get_cell(index-1);
1269 if (!upper_cell.is_mergeable()) {
1267 if (!upper_cell.is_mergeable()) {
1270 return;
1268 return;
1271 }
1269 }
1272 var upper_text = upper_cell.get_text();
1270 var upper_text = upper_cell.get_text();
1273 var text = cell.get_text();
1271 var text = cell.get_text();
1274 if (cell instanceof codecell.CodeCell) {
1272 if (cell instanceof codecell.CodeCell) {
1275 cell.set_text(upper_text+'\n'+text);
1273 cell.set_text(upper_text+'\n'+text);
1276 } else {
1274 } else {
1277 cell.unrender(); // Must unrender before we set_text.
1275 cell.unrender(); // Must unrender before we set_text.
1278 cell.set_text(upper_text+'\n\n'+text);
1276 cell.set_text(upper_text+'\n\n'+text);
1279 if (render) {
1277 if (render) {
1280 // The rendered state of the final cell should match
1278 // The rendered state of the final cell should match
1281 // that of the original selected cell;
1279 // that of the original selected cell;
1282 cell.render();
1280 cell.render();
1283 }
1281 }
1284 }
1282 }
1285 this.delete_cell(index-1);
1283 this.delete_cell(index-1);
1286 this.select(this.find_cell_index(cell));
1284 this.select(this.find_cell_index(cell));
1287 }
1285 }
1288 };
1286 };
1289
1287
1290 /**
1288 /**
1291 * Combine the selected cell into the cell below it.
1289 * Combine the selected cell into the cell below it.
1292 *
1290 *
1293 * @method merge_cell_below
1291 * @method merge_cell_below
1294 */
1292 */
1295 Notebook.prototype.merge_cell_below = function () {
1293 Notebook.prototype.merge_cell_below = function () {
1296 var mdc = textcell.MarkdownCell;
1294 var mdc = textcell.MarkdownCell;
1297 var rc = textcell.RawCell;
1295 var rc = textcell.RawCell;
1298 var index = this.get_selected_index();
1296 var index = this.get_selected_index();
1299 var cell = this.get_cell(index);
1297 var cell = this.get_cell(index);
1300 var render = cell.rendered;
1298 var render = cell.rendered;
1301 if (!cell.is_mergeable()) {
1299 if (!cell.is_mergeable()) {
1302 return;
1300 return;
1303 }
1301 }
1304 if (index < this.ncells()-1) {
1302 if (index < this.ncells()-1) {
1305 var lower_cell = this.get_cell(index+1);
1303 var lower_cell = this.get_cell(index+1);
1306 if (!lower_cell.is_mergeable()) {
1304 if (!lower_cell.is_mergeable()) {
1307 return;
1305 return;
1308 }
1306 }
1309 var lower_text = lower_cell.get_text();
1307 var lower_text = lower_cell.get_text();
1310 var text = cell.get_text();
1308 var text = cell.get_text();
1311 if (cell instanceof codecell.CodeCell) {
1309 if (cell instanceof codecell.CodeCell) {
1312 cell.set_text(text+'\n'+lower_text);
1310 cell.set_text(text+'\n'+lower_text);
1313 } else {
1311 } else {
1314 cell.unrender(); // Must unrender before we set_text.
1312 cell.unrender(); // Must unrender before we set_text.
1315 cell.set_text(text+'\n\n'+lower_text);
1313 cell.set_text(text+'\n\n'+lower_text);
1316 if (render) {
1314 if (render) {
1317 // The rendered state of the final cell should match
1315 // The rendered state of the final cell should match
1318 // that of the original selected cell;
1316 // that of the original selected cell;
1319 cell.render();
1317 cell.render();
1320 }
1318 }
1321 }
1319 }
1322 this.delete_cell(index+1);
1320 this.delete_cell(index+1);
1323 this.select(this.find_cell_index(cell));
1321 this.select(this.find_cell_index(cell));
1324 }
1322 }
1325 };
1323 };
1326
1324
1327
1325
1328 // Cell collapsing and output clearing
1326 // Cell collapsing and output clearing
1329
1327
1330 /**
1328 /**
1331 * Hide a cell's output.
1329 * Hide a cell's output.
1332 *
1330 *
1333 * @method collapse_output
1331 * @method collapse_output
1334 * @param {Number} index A cell's numeric index
1332 * @param {Number} index A cell's numeric index
1335 */
1333 */
1336 Notebook.prototype.collapse_output = function (index) {
1334 Notebook.prototype.collapse_output = function (index) {
1337 var i = this.index_or_selected(index);
1335 var i = this.index_or_selected(index);
1338 var cell = this.get_cell(i);
1336 var cell = this.get_cell(i);
1339 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1337 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1340 cell.collapse_output();
1338 cell.collapse_output();
1341 this.set_dirty(true);
1339 this.set_dirty(true);
1342 }
1340 }
1343 };
1341 };
1344
1342
1345 /**
1343 /**
1346 * Hide each code cell's output area.
1344 * Hide each code cell's output area.
1347 *
1345 *
1348 * @method collapse_all_output
1346 * @method collapse_all_output
1349 */
1347 */
1350 Notebook.prototype.collapse_all_output = function () {
1348 Notebook.prototype.collapse_all_output = function () {
1351 $.map(this.get_cells(), function (cell, i) {
1349 $.map(this.get_cells(), function (cell, i) {
1352 if (cell instanceof codecell.CodeCell) {
1350 if (cell instanceof codecell.CodeCell) {
1353 cell.collapse_output();
1351 cell.collapse_output();
1354 }
1352 }
1355 });
1353 });
1356 // this should not be set if the `collapse` key is removed from nbformat
1354 // this should not be set if the `collapse` key is removed from nbformat
1357 this.set_dirty(true);
1355 this.set_dirty(true);
1358 };
1356 };
1359
1357
1360 /**
1358 /**
1361 * Show a cell's output.
1359 * Show a cell's output.
1362 *
1360 *
1363 * @method expand_output
1361 * @method expand_output
1364 * @param {Number} index A cell's numeric index
1362 * @param {Number} index A cell's numeric index
1365 */
1363 */
1366 Notebook.prototype.expand_output = function (index) {
1364 Notebook.prototype.expand_output = function (index) {
1367 var i = this.index_or_selected(index);
1365 var i = this.index_or_selected(index);
1368 var cell = this.get_cell(i);
1366 var cell = this.get_cell(i);
1369 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1367 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1370 cell.expand_output();
1368 cell.expand_output();
1371 this.set_dirty(true);
1369 this.set_dirty(true);
1372 }
1370 }
1373 };
1371 };
1374
1372
1375 /**
1373 /**
1376 * Expand each code cell's output area, and remove scrollbars.
1374 * Expand each code cell's output area, and remove scrollbars.
1377 *
1375 *
1378 * @method expand_all_output
1376 * @method expand_all_output
1379 */
1377 */
1380 Notebook.prototype.expand_all_output = function () {
1378 Notebook.prototype.expand_all_output = function () {
1381 $.map(this.get_cells(), function (cell, i) {
1379 $.map(this.get_cells(), function (cell, i) {
1382 if (cell instanceof codecell.CodeCell) {
1380 if (cell instanceof codecell.CodeCell) {
1383 cell.expand_output();
1381 cell.expand_output();
1384 }
1382 }
1385 });
1383 });
1386 // this should not be set if the `collapse` key is removed from nbformat
1384 // this should not be set if the `collapse` key is removed from nbformat
1387 this.set_dirty(true);
1385 this.set_dirty(true);
1388 };
1386 };
1389
1387
1390 /**
1388 /**
1391 * Clear the selected CodeCell's output area.
1389 * Clear the selected CodeCell's output area.
1392 *
1390 *
1393 * @method clear_output
1391 * @method clear_output
1394 * @param {Number} index A cell's numeric index
1392 * @param {Number} index A cell's numeric index
1395 */
1393 */
1396 Notebook.prototype.clear_output = function (index) {
1394 Notebook.prototype.clear_output = function (index) {
1397 var i = this.index_or_selected(index);
1395 var i = this.index_or_selected(index);
1398 var cell = this.get_cell(i);
1396 var cell = this.get_cell(i);
1399 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1397 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1400 cell.clear_output();
1398 cell.clear_output();
1401 this.set_dirty(true);
1399 this.set_dirty(true);
1402 }
1400 }
1403 };
1401 };
1404
1402
1405 /**
1403 /**
1406 * Clear each code cell's output area.
1404 * Clear each code cell's output area.
1407 *
1405 *
1408 * @method clear_all_output
1406 * @method clear_all_output
1409 */
1407 */
1410 Notebook.prototype.clear_all_output = function () {
1408 Notebook.prototype.clear_all_output = function () {
1411 $.map(this.get_cells(), function (cell, i) {
1409 $.map(this.get_cells(), function (cell, i) {
1412 if (cell instanceof codecell.CodeCell) {
1410 if (cell instanceof codecell.CodeCell) {
1413 cell.clear_output();
1411 cell.clear_output();
1414 }
1412 }
1415 });
1413 });
1416 this.set_dirty(true);
1414 this.set_dirty(true);
1417 };
1415 };
1418
1416
1419 /**
1417 /**
1420 * Scroll the selected CodeCell's output area.
1418 * Scroll the selected CodeCell's output area.
1421 *
1419 *
1422 * @method scroll_output
1420 * @method scroll_output
1423 * @param {Number} index A cell's numeric index
1421 * @param {Number} index A cell's numeric index
1424 */
1422 */
1425 Notebook.prototype.scroll_output = function (index) {
1423 Notebook.prototype.scroll_output = function (index) {
1426 var i = this.index_or_selected(index);
1424 var i = this.index_or_selected(index);
1427 var cell = this.get_cell(i);
1425 var cell = this.get_cell(i);
1428 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1426 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1429 cell.scroll_output();
1427 cell.scroll_output();
1430 this.set_dirty(true);
1428 this.set_dirty(true);
1431 }
1429 }
1432 };
1430 };
1433
1431
1434 /**
1432 /**
1435 * Expand each code cell's output area, and add a scrollbar for long output.
1433 * Expand each code cell's output area, and add a scrollbar for long output.
1436 *
1434 *
1437 * @method scroll_all_output
1435 * @method scroll_all_output
1438 */
1436 */
1439 Notebook.prototype.scroll_all_output = function () {
1437 Notebook.prototype.scroll_all_output = function () {
1440 $.map(this.get_cells(), function (cell, i) {
1438 $.map(this.get_cells(), function (cell, i) {
1441 if (cell instanceof codecell.CodeCell) {
1439 if (cell instanceof codecell.CodeCell) {
1442 cell.scroll_output();
1440 cell.scroll_output();
1443 }
1441 }
1444 });
1442 });
1445 // this should not be set if the `collapse` key is removed from nbformat
1443 // this should not be set if the `collapse` key is removed from nbformat
1446 this.set_dirty(true);
1444 this.set_dirty(true);
1447 };
1445 };
1448
1446
1449 /** Toggle whether a cell's output is collapsed or expanded.
1447 /** Toggle whether a cell's output is collapsed or expanded.
1450 *
1448 *
1451 * @method toggle_output
1449 * @method toggle_output
1452 * @param {Number} index A cell's numeric index
1450 * @param {Number} index A cell's numeric index
1453 */
1451 */
1454 Notebook.prototype.toggle_output = function (index) {
1452 Notebook.prototype.toggle_output = function (index) {
1455 var i = this.index_or_selected(index);
1453 var i = this.index_or_selected(index);
1456 var cell = this.get_cell(i);
1454 var cell = this.get_cell(i);
1457 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1455 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1458 cell.toggle_output();
1456 cell.toggle_output();
1459 this.set_dirty(true);
1457 this.set_dirty(true);
1460 }
1458 }
1461 };
1459 };
1462
1460
1463 /**
1461 /**
1464 * Hide/show the output of all cells.
1462 * Hide/show the output of all cells.
1465 *
1463 *
1466 * @method toggle_all_output
1464 * @method toggle_all_output
1467 */
1465 */
1468 Notebook.prototype.toggle_all_output = function () {
1466 Notebook.prototype.toggle_all_output = function () {
1469 $.map(this.get_cells(), function (cell, i) {
1467 $.map(this.get_cells(), function (cell, i) {
1470 if (cell instanceof codecell.CodeCell) {
1468 if (cell instanceof codecell.CodeCell) {
1471 cell.toggle_output();
1469 cell.toggle_output();
1472 }
1470 }
1473 });
1471 });
1474 // this should not be set if the `collapse` key is removed from nbformat
1472 // this should not be set if the `collapse` key is removed from nbformat
1475 this.set_dirty(true);
1473 this.set_dirty(true);
1476 };
1474 };
1477
1475
1478 /**
1476 /**
1479 * Toggle a scrollbar for long cell outputs.
1477 * Toggle a scrollbar for long cell outputs.
1480 *
1478 *
1481 * @method toggle_output_scroll
1479 * @method toggle_output_scroll
1482 * @param {Number} index A cell's numeric index
1480 * @param {Number} index A cell's numeric index
1483 */
1481 */
1484 Notebook.prototype.toggle_output_scroll = function (index) {
1482 Notebook.prototype.toggle_output_scroll = function (index) {
1485 var i = this.index_or_selected(index);
1483 var i = this.index_or_selected(index);
1486 var cell = this.get_cell(i);
1484 var cell = this.get_cell(i);
1487 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1485 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1488 cell.toggle_output_scroll();
1486 cell.toggle_output_scroll();
1489 this.set_dirty(true);
1487 this.set_dirty(true);
1490 }
1488 }
1491 };
1489 };
1492
1490
1493 /**
1491 /**
1494 * Toggle the scrolling of long output on all cells.
1492 * Toggle the scrolling of long output on all cells.
1495 *
1493 *
1496 * @method toggle_all_output_scrolling
1494 * @method toggle_all_output_scrolling
1497 */
1495 */
1498 Notebook.prototype.toggle_all_output_scroll = function () {
1496 Notebook.prototype.toggle_all_output_scroll = function () {
1499 $.map(this.get_cells(), function (cell, i) {
1497 $.map(this.get_cells(), function (cell, i) {
1500 if (cell instanceof codecell.CodeCell) {
1498 if (cell instanceof codecell.CodeCell) {
1501 cell.toggle_output_scroll();
1499 cell.toggle_output_scroll();
1502 }
1500 }
1503 });
1501 });
1504 // this should not be set if the `collapse` key is removed from nbformat
1502 // this should not be set if the `collapse` key is removed from nbformat
1505 this.set_dirty(true);
1503 this.set_dirty(true);
1506 };
1504 };
1507
1505
1508 // Other cell functions: line numbers, ...
1506 // Other cell functions: line numbers, ...
1509
1507
1510 /**
1508 /**
1511 * Toggle line numbers in the selected cell's input area.
1509 * Toggle line numbers in the selected cell's input area.
1512 *
1510 *
1513 * @method cell_toggle_line_numbers
1511 * @method cell_toggle_line_numbers
1514 */
1512 */
1515 Notebook.prototype.cell_toggle_line_numbers = function() {
1513 Notebook.prototype.cell_toggle_line_numbers = function() {
1516 this.get_selected_cell().toggle_line_numbers();
1514 this.get_selected_cell().toggle_line_numbers();
1517 };
1515 };
1518
1516
1519 /**
1517 /**
1520 * Set the codemirror mode for all code cells, including the default for
1518 * Set the codemirror mode for all code cells, including the default for
1521 * new code cells.
1519 * new code cells.
1522 *
1520 *
1523 * @method set_codemirror_mode
1521 * @method set_codemirror_mode
1524 */
1522 */
1525 Notebook.prototype.set_codemirror_mode = function(newmode){
1523 Notebook.prototype.set_codemirror_mode = function(newmode){
1526 if (newmode === this.codemirror_mode) {
1524 if (newmode === this.codemirror_mode) {
1527 return;
1525 return;
1528 }
1526 }
1529 this.codemirror_mode = newmode;
1527 this.codemirror_mode = newmode;
1530 codecell.CodeCell.options_default.cm_config.mode = newmode;
1528 codecell.CodeCell.options_default.cm_config.mode = newmode;
1531 modename = newmode.mode || newmode.name || newmode
1529 modename = newmode.mode || newmode.name || newmode
1532
1530
1533 that = this;
1531 that = this;
1534 utils.requireCodeMirrorMode(modename, function () {
1532 utils.requireCodeMirrorMode(modename, function () {
1535 $.map(that.get_cells(), function(cell, i) {
1533 $.map(that.get_cells(), function(cell, i) {
1536 if (cell.cell_type === 'code'){
1534 if (cell.cell_type === 'code'){
1537 cell.code_mirror.setOption('mode', newmode);
1535 cell.code_mirror.setOption('mode', newmode);
1538 // This is currently redundant, because cm_config ends up as
1536 // This is currently redundant, because cm_config ends up as
1539 // codemirror's own .options object, but I don't want to
1537 // codemirror's own .options object, but I don't want to
1540 // rely on that.
1538 // rely on that.
1541 cell.cm_config.mode = newmode;
1539 cell.cm_config.mode = newmode;
1542 }
1540 }
1543 });
1541 });
1544 })
1542 })
1545 };
1543 };
1546
1544
1547 // Session related things
1545 // Session related things
1548
1546
1549 /**
1547 /**
1550 * Start a new session and set it on each code cell.
1548 * Start a new session and set it on each code cell.
1551 *
1549 *
1552 * @method start_session
1550 * @method start_session
1553 */
1551 */
1554 Notebook.prototype.start_session = function (kernel_name) {
1552 Notebook.prototype.start_session = function (kernel_name) {
1555 var that = this;
1553 var that = this;
1556 if (this._session_starting) {
1554 if (this._session_starting) {
1557 throw new session.SessionAlreadyStarting();
1555 throw new session.SessionAlreadyStarting();
1558 }
1556 }
1559 this._session_starting = true;
1557 this._session_starting = true;
1560
1558
1561 var options = {
1559 var options = {
1562 base_url: this.base_url,
1560 base_url: this.base_url,
1563 ws_url: this.ws_url,
1561 ws_url: this.ws_url,
1564 notebook_path: this.notebook_path,
1562 notebook_path: this.notebook_path,
1565 notebook_name: this.notebook_name,
1563 notebook_name: this.notebook_name,
1566 kernel_name: kernel_name,
1564 kernel_name: kernel_name,
1567 notebook: this
1565 notebook: this
1568 };
1566 };
1569
1567
1570 var success = $.proxy(this._session_started, this);
1568 var success = $.proxy(this._session_started, this);
1571 var failure = $.proxy(this._session_start_failed, this);
1569 var failure = $.proxy(this._session_start_failed, this);
1572
1570
1573 if (this.session !== null) {
1571 if (this.session !== null) {
1574 this.session.restart(options, success, failure);
1572 this.session.restart(options, success, failure);
1575 } else {
1573 } else {
1576 this.session = new session.Session(options);
1574 this.session = new session.Session(options);
1577 this.session.start(success, failure);
1575 this.session.start(success, failure);
1578 }
1576 }
1579 };
1577 };
1580
1578
1581
1579
1582 /**
1580 /**
1583 * Once a session is started, link the code cells to the kernel and pass the
1581 * Once a session is started, link the code cells to the kernel and pass the
1584 * comm manager to the widget manager
1582 * comm manager to the widget manager
1585 *
1583 *
1586 */
1584 */
1587 Notebook.prototype._session_started = function (){
1585 Notebook.prototype._session_started = function (){
1588 this._session_starting = false;
1586 this._session_starting = false;
1589 this.kernel = this.session.kernel;
1587 this.kernel = this.session.kernel;
1590 var ncells = this.ncells();
1588 var ncells = this.ncells();
1591 for (var i=0; i<ncells; i++) {
1589 for (var i=0; i<ncells; i++) {
1592 var cell = this.get_cell(i);
1590 var cell = this.get_cell(i);
1593 if (cell instanceof codecell.CodeCell) {
1591 if (cell instanceof codecell.CodeCell) {
1594 cell.set_kernel(this.session.kernel);
1592 cell.set_kernel(this.session.kernel);
1595 }
1593 }
1596 }
1594 }
1597 };
1595 };
1598 Notebook.prototype._session_start_failed = function (jqxhr, status, error){
1596 Notebook.prototype._session_start_failed = function (jqxhr, status, error){
1599 this._session_starting = false;
1597 this._session_starting = false;
1600 utils.log_ajax_error(jqxhr, status, error);
1598 utils.log_ajax_error(jqxhr, status, error);
1601 };
1599 };
1602
1600
1603 /**
1601 /**
1604 * Prompt the user to restart the IPython kernel.
1602 * Prompt the user to restart the IPython kernel.
1605 *
1603 *
1606 * @method restart_kernel
1604 * @method restart_kernel
1607 */
1605 */
1608 Notebook.prototype.restart_kernel = function () {
1606 Notebook.prototype.restart_kernel = function () {
1609 var that = this;
1607 var that = this;
1610 dialog.modal({
1608 dialog.modal({
1611 notebook: this,
1609 notebook: this,
1612 keyboard_manager: this.keyboard_manager,
1610 keyboard_manager: this.keyboard_manager,
1613 title : "Restart kernel or continue running?",
1611 title : "Restart kernel or continue running?",
1614 body : $("<p/>").text(
1612 body : $("<p/>").text(
1615 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1613 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1616 ),
1614 ),
1617 buttons : {
1615 buttons : {
1618 "Continue running" : {},
1616 "Continue running" : {},
1619 "Restart" : {
1617 "Restart" : {
1620 "class" : "btn-danger",
1618 "class" : "btn-danger",
1621 "click" : function() {
1619 "click" : function() {
1622 that.kernel.restart();
1620 that.kernel.restart();
1623 }
1621 }
1624 }
1622 }
1625 }
1623 }
1626 });
1624 });
1627 };
1625 };
1628
1626
1629 /**
1627 /**
1630 * Execute or render cell outputs and go into command mode.
1628 * Execute or render cell outputs and go into command mode.
1631 *
1629 *
1632 * @method execute_cell
1630 * @method execute_cell
1633 */
1631 */
1634 Notebook.prototype.execute_cell = function () {
1632 Notebook.prototype.execute_cell = function () {
1635 // mode = shift, ctrl, alt
1633 // mode = shift, ctrl, alt
1636 var cell = this.get_selected_cell();
1634 var cell = this.get_selected_cell();
1637 var cell_index = this.find_cell_index(cell);
1635 var cell_index = this.find_cell_index(cell);
1638
1636
1639 cell.execute();
1637 cell.execute();
1640 this.command_mode();
1638 this.command_mode();
1641 this.set_dirty(true);
1639 this.set_dirty(true);
1642 };
1640 };
1643
1641
1644 /**
1642 /**
1645 * Execute or render cell outputs and insert a new cell below.
1643 * Execute or render cell outputs and insert a new cell below.
1646 *
1644 *
1647 * @method execute_cell_and_insert_below
1645 * @method execute_cell_and_insert_below
1648 */
1646 */
1649 Notebook.prototype.execute_cell_and_insert_below = function () {
1647 Notebook.prototype.execute_cell_and_insert_below = function () {
1650 var cell = this.get_selected_cell();
1648 var cell = this.get_selected_cell();
1651 var cell_index = this.find_cell_index(cell);
1649 var cell_index = this.find_cell_index(cell);
1652
1650
1653 cell.execute();
1651 cell.execute();
1654
1652
1655 // If we are at the end always insert a new cell and return
1653 // If we are at the end always insert a new cell and return
1656 if (cell_index === (this.ncells()-1)) {
1654 if (cell_index === (this.ncells()-1)) {
1657 this.command_mode();
1655 this.command_mode();
1658 this.insert_cell_below();
1656 this.insert_cell_below();
1659 this.select(cell_index+1);
1657 this.select(cell_index+1);
1660 this.edit_mode();
1658 this.edit_mode();
1661 this.scroll_to_bottom();
1659 this.scroll_to_bottom();
1662 this.set_dirty(true);
1660 this.set_dirty(true);
1663 return;
1661 return;
1664 }
1662 }
1665
1663
1666 this.command_mode();
1664 this.command_mode();
1667 this.insert_cell_below();
1665 this.insert_cell_below();
1668 this.select(cell_index+1);
1666 this.select(cell_index+1);
1669 this.edit_mode();
1667 this.edit_mode();
1670 this.set_dirty(true);
1668 this.set_dirty(true);
1671 };
1669 };
1672
1670
1673 /**
1671 /**
1674 * Execute or render cell outputs and select the next cell.
1672 * Execute or render cell outputs and select the next cell.
1675 *
1673 *
1676 * @method execute_cell_and_select_below
1674 * @method execute_cell_and_select_below
1677 */
1675 */
1678 Notebook.prototype.execute_cell_and_select_below = function () {
1676 Notebook.prototype.execute_cell_and_select_below = function () {
1679
1677
1680 var cell = this.get_selected_cell();
1678 var cell = this.get_selected_cell();
1681 var cell_index = this.find_cell_index(cell);
1679 var cell_index = this.find_cell_index(cell);
1682
1680
1683 cell.execute();
1681 cell.execute();
1684
1682
1685 // If we are at the end always insert a new cell and return
1683 // If we are at the end always insert a new cell and return
1686 if (cell_index === (this.ncells()-1)) {
1684 if (cell_index === (this.ncells()-1)) {
1687 this.command_mode();
1685 this.command_mode();
1688 this.insert_cell_below();
1686 this.insert_cell_below();
1689 this.select(cell_index+1);
1687 this.select(cell_index+1);
1690 this.edit_mode();
1688 this.edit_mode();
1691 this.scroll_to_bottom();
1689 this.scroll_to_bottom();
1692 this.set_dirty(true);
1690 this.set_dirty(true);
1693 return;
1691 return;
1694 }
1692 }
1695
1693
1696 this.command_mode();
1694 this.command_mode();
1697 this.select(cell_index+1);
1695 this.select(cell_index+1);
1698 this.focus_cell();
1696 this.focus_cell();
1699 this.set_dirty(true);
1697 this.set_dirty(true);
1700 };
1698 };
1701
1699
1702 /**
1700 /**
1703 * Execute all cells below the selected cell.
1701 * Execute all cells below the selected cell.
1704 *
1702 *
1705 * @method execute_cells_below
1703 * @method execute_cells_below
1706 */
1704 */
1707 Notebook.prototype.execute_cells_below = function () {
1705 Notebook.prototype.execute_cells_below = function () {
1708 this.execute_cell_range(this.get_selected_index(), this.ncells());
1706 this.execute_cell_range(this.get_selected_index(), this.ncells());
1709 this.scroll_to_bottom();
1707 this.scroll_to_bottom();
1710 };
1708 };
1711
1709
1712 /**
1710 /**
1713 * Execute all cells above the selected cell.
1711 * Execute all cells above the selected cell.
1714 *
1712 *
1715 * @method execute_cells_above
1713 * @method execute_cells_above
1716 */
1714 */
1717 Notebook.prototype.execute_cells_above = function () {
1715 Notebook.prototype.execute_cells_above = function () {
1718 this.execute_cell_range(0, this.get_selected_index());
1716 this.execute_cell_range(0, this.get_selected_index());
1719 };
1717 };
1720
1718
1721 /**
1719 /**
1722 * Execute all cells.
1720 * Execute all cells.
1723 *
1721 *
1724 * @method execute_all_cells
1722 * @method execute_all_cells
1725 */
1723 */
1726 Notebook.prototype.execute_all_cells = function () {
1724 Notebook.prototype.execute_all_cells = function () {
1727 this.execute_cell_range(0, this.ncells());
1725 this.execute_cell_range(0, this.ncells());
1728 this.scroll_to_bottom();
1726 this.scroll_to_bottom();
1729 };
1727 };
1730
1728
1731 /**
1729 /**
1732 * Execute a contiguous range of cells.
1730 * Execute a contiguous range of cells.
1733 *
1731 *
1734 * @method execute_cell_range
1732 * @method execute_cell_range
1735 * @param {Number} start Index of the first cell to execute (inclusive)
1733 * @param {Number} start Index of the first cell to execute (inclusive)
1736 * @param {Number} end Index of the last cell to execute (exclusive)
1734 * @param {Number} end Index of the last cell to execute (exclusive)
1737 */
1735 */
1738 Notebook.prototype.execute_cell_range = function (start, end) {
1736 Notebook.prototype.execute_cell_range = function (start, end) {
1739 this.command_mode();
1737 this.command_mode();
1740 for (var i=start; i<end; i++) {
1738 for (var i=start; i<end; i++) {
1741 this.select(i);
1739 this.select(i);
1742 this.execute_cell();
1740 this.execute_cell();
1743 }
1741 }
1744 };
1742 };
1745
1743
1746 // Persistance and loading
1744 // Persistance and loading
1747
1745
1748 /**
1746 /**
1749 * Getter method for this notebook's name.
1747 * Getter method for this notebook's name.
1750 *
1748 *
1751 * @method get_notebook_name
1749 * @method get_notebook_name
1752 * @return {String} This notebook's name (excluding file extension)
1750 * @return {String} This notebook's name (excluding file extension)
1753 */
1751 */
1754 Notebook.prototype.get_notebook_name = function () {
1752 Notebook.prototype.get_notebook_name = function () {
1755 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1753 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1756 return nbname;
1754 return nbname;
1757 };
1755 };
1758
1756
1759 /**
1757 /**
1760 * Setter method for this notebook's name.
1758 * Setter method for this notebook's name.
1761 *
1759 *
1762 * @method set_notebook_name
1760 * @method set_notebook_name
1763 * @param {String} name A new name for this notebook
1761 * @param {String} name A new name for this notebook
1764 */
1762 */
1765 Notebook.prototype.set_notebook_name = function (name) {
1763 Notebook.prototype.set_notebook_name = function (name) {
1766 this.notebook_name = name;
1764 this.notebook_name = name;
1767 };
1765 };
1768
1766
1769 /**
1767 /**
1770 * Check that a notebook's name is valid.
1768 * Check that a notebook's name is valid.
1771 *
1769 *
1772 * @method test_notebook_name
1770 * @method test_notebook_name
1773 * @param {String} nbname A name for this notebook
1771 * @param {String} nbname A name for this notebook
1774 * @return {Boolean} True if the name is valid, false if invalid
1772 * @return {Boolean} True if the name is valid, false if invalid
1775 */
1773 */
1776 Notebook.prototype.test_notebook_name = function (nbname) {
1774 Notebook.prototype.test_notebook_name = function (nbname) {
1777 nbname = nbname || '';
1775 nbname = nbname || '';
1778 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1776 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1779 return true;
1777 return true;
1780 } else {
1778 } else {
1781 return false;
1779 return false;
1782 }
1780 }
1783 };
1781 };
1784
1782
1785 /**
1783 /**
1786 * Load a notebook from JSON (.ipynb).
1784 * Load a notebook from JSON (.ipynb).
1787 *
1785 *
1788 * This currently handles one worksheet: others are deleted.
1789 *
1790 * @method fromJSON
1786 * @method fromJSON
1791 * @param {Object} data JSON representation of a notebook
1787 * @param {Object} data JSON representation of a notebook
1792 */
1788 */
1793 Notebook.prototype.fromJSON = function (data) {
1789 Notebook.prototype.fromJSON = function (data) {
1794
1790
1795 var content = data.content;
1791 var content = data.content;
1796 var ncells = this.ncells();
1792 var ncells = this.ncells();
1797 var i;
1793 var i;
1798 for (i=0; i<ncells; i++) {
1794 for (i=0; i<ncells; i++) {
1799 // Always delete cell 0 as they get renumbered as they are deleted.
1795 // Always delete cell 0 as they get renumbered as they are deleted.
1800 this.delete_cell(0);
1796 this.delete_cell(0);
1801 }
1797 }
1802 // Save the metadata and name.
1798 // Save the metadata and name.
1803 this.metadata = content.metadata;
1799 this.metadata = content.metadata;
1804 this.notebook_name = data.name;
1800 this.notebook_name = data.name;
1805 var trusted = true;
1801 var trusted = true;
1806
1802
1807 // Trigger an event changing the kernel spec - this will set the default
1803 // Trigger an event changing the kernel spec - this will set the default
1808 // codemirror mode
1804 // codemirror mode
1809 if (this.metadata.kernelspec !== undefined) {
1805 if (this.metadata.kernelspec !== undefined) {
1810 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1806 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1811 }
1807 }
1812
1808
1813 // Set the codemirror mode from language_info metadata
1809 // Set the codemirror mode from language_info metadata
1814 if (this.metadata.language_info !== undefined) {
1810 if (this.metadata.language_info !== undefined) {
1815 var langinfo = this.metadata.language_info;
1811 var langinfo = this.metadata.language_info;
1816 // Mode 'null' should be plain, unhighlighted text.
1812 // Mode 'null' should be plain, unhighlighted text.
1817 var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null'
1813 var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null'
1818 this.set_codemirror_mode(cm_mode);
1814 this.set_codemirror_mode(cm_mode);
1819 }
1815 }
1820
1816
1821 // Only handle 1 worksheet for now.
1817 var new_cells = content.cells;
1822 var worksheet = content.worksheets[0];
1818 ncells = new_cells.length;
1823 if (worksheet !== undefined) {
1819 var cell_data = null;
1824 if (worksheet.metadata) {
1820 var new_cell = null;
1825 this.worksheet_metadata = worksheet.metadata;
1821 for (i=0; i<ncells; i++) {
1826 }
1822 cell_data = new_cells[i];
1827 var new_cells = worksheet.cells;
1823 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1828 ncells = new_cells.length;
1824 new_cell.fromJSON(cell_data);
1829 var cell_data = null;
1825 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1830 var new_cell = null;
1826 trusted = false;
1831 for (i=0; i<ncells; i++) {
1832 cell_data = new_cells[i];
1833 // VERSIONHACK: plaintext -> raw
1834 // handle never-released plaintext name for raw cells
1835 if (cell_data.cell_type === 'plaintext'){
1836 cell_data.cell_type = 'raw';
1837 }
1838
1839 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1840 new_cell.fromJSON(cell_data);
1841 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1842 trusted = false;
1843 }
1844 }
1827 }
1845 }
1828 }
1846 if (trusted !== this.trusted) {
1829 if (trusted !== this.trusted) {
1847 this.trusted = trusted;
1830 this.trusted = trusted;
1848 this.events.trigger("trust_changed.Notebook", trusted);
1831 this.events.trigger("trust_changed.Notebook", trusted);
1849 }
1832 }
1850 if (content.worksheets.length > 1) {
1851 dialog.modal({
1852 notebook: this,
1853 keyboard_manager: this.keyboard_manager,
1854 title : "Multiple worksheets",
1855 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1856 "but this version of IPython can only handle the first. " +
1857 "If you save this notebook, worksheets after the first will be lost.",
1858 buttons : {
1859 OK : {
1860 class : "btn-danger"
1861 }
1862 }
1863 });
1864 }
1865 };
1833 };
1866
1834
1867 /**
1835 /**
1868 * Dump this notebook into a JSON-friendly object.
1836 * Dump this notebook into a JSON-friendly object.
1869 *
1837 *
1870 * @method toJSON
1838 * @method toJSON
1871 * @return {Object} A JSON-friendly representation of this notebook.
1839 * @return {Object} A JSON-friendly representation of this notebook.
1872 */
1840 */
1873 Notebook.prototype.toJSON = function () {
1841 Notebook.prototype.toJSON = function () {
1842 // remove the conversion indicator, which only belongs in-memory
1843 delete this.metadata.orig_nbformat;
1844 delete this.metadata.orig_nbformat_minor;
1845
1874 var cells = this.get_cells();
1846 var cells = this.get_cells();
1875 var ncells = cells.length;
1847 var ncells = cells.length;
1876 var cell_array = new Array(ncells);
1848 var cell_array = new Array(ncells);
1877 var trusted = true;
1849 var trusted = true;
1878 for (var i=0; i<ncells; i++) {
1850 for (var i=0; i<ncells; i++) {
1879 var cell = cells[i];
1851 var cell = cells[i];
1880 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1852 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1881 trusted = false;
1853 trusted = false;
1882 }
1854 }
1883 cell_array[i] = cell.toJSON();
1855 cell_array[i] = cell.toJSON();
1884 }
1856 }
1885 var data = {
1857 var data = {
1886 // Only handle 1 worksheet for now.
1858 cells: cell_array,
1887 worksheets : [{
1888 cells: cell_array,
1889 metadata: this.worksheet_metadata
1890 }],
1891 metadata : this.metadata
1859 metadata : this.metadata
1892 };
1860 };
1893 if (trusted != this.trusted) {
1861 if (trusted != this.trusted) {
1894 this.trusted = trusted;
1862 this.trusted = trusted;
1895 this.events.trigger("trust_changed.Notebook", trusted);
1863 this.events.trigger("trust_changed.Notebook", trusted);
1896 }
1864 }
1897 return data;
1865 return data;
1898 };
1866 };
1899
1867
1900 /**
1868 /**
1901 * Start an autosave timer, for periodically saving the notebook.
1869 * Start an autosave timer, for periodically saving the notebook.
1902 *
1870 *
1903 * @method set_autosave_interval
1871 * @method set_autosave_interval
1904 * @param {Integer} interval the autosave interval in milliseconds
1872 * @param {Integer} interval the autosave interval in milliseconds
1905 */
1873 */
1906 Notebook.prototype.set_autosave_interval = function (interval) {
1874 Notebook.prototype.set_autosave_interval = function (interval) {
1907 var that = this;
1875 var that = this;
1908 // clear previous interval, so we don't get simultaneous timers
1876 // clear previous interval, so we don't get simultaneous timers
1909 if (this.autosave_timer) {
1877 if (this.autosave_timer) {
1910 clearInterval(this.autosave_timer);
1878 clearInterval(this.autosave_timer);
1911 }
1879 }
1912
1880
1913 this.autosave_interval = this.minimum_autosave_interval = interval;
1881 this.autosave_interval = this.minimum_autosave_interval = interval;
1914 if (interval) {
1882 if (interval) {
1915 this.autosave_timer = setInterval(function() {
1883 this.autosave_timer = setInterval(function() {
1916 if (that.dirty) {
1884 if (that.dirty) {
1917 that.save_notebook();
1885 that.save_notebook();
1918 }
1886 }
1919 }, interval);
1887 }, interval);
1920 this.events.trigger("autosave_enabled.Notebook", interval);
1888 this.events.trigger("autosave_enabled.Notebook", interval);
1921 } else {
1889 } else {
1922 this.autosave_timer = null;
1890 this.autosave_timer = null;
1923 this.events.trigger("autosave_disabled.Notebook");
1891 this.events.trigger("autosave_disabled.Notebook");
1924 }
1892 }
1925 };
1893 };
1926
1894
1927 /**
1895 /**
1928 * Save this notebook on the server. This becomes a notebook instance's
1896 * Save this notebook on the server. This becomes a notebook instance's
1929 * .save_notebook method *after* the entire notebook has been loaded.
1897 * .save_notebook method *after* the entire notebook has been loaded.
1930 *
1898 *
1931 * @method save_notebook
1899 * @method save_notebook
1932 */
1900 */
1933 Notebook.prototype.save_notebook = function (extra_settings) {
1901 Notebook.prototype.save_notebook = function (extra_settings) {
1934 // Create a JSON model to be sent to the server.
1902 // Create a JSON model to be sent to the server.
1935 var model = {};
1903 var model = {};
1936 model.name = this.notebook_name;
1904 model.name = this.notebook_name;
1937 model.path = this.notebook_path;
1905 model.path = this.notebook_path;
1938 model.type = 'notebook';
1906 model.type = 'notebook';
1939 model.format = 'json';
1907 model.format = 'json';
1940 model.content = this.toJSON();
1908 model.content = this.toJSON();
1941 model.content.nbformat = this.nbformat;
1909 model.content.nbformat = this.nbformat;
1942 model.content.nbformat_minor = this.nbformat_minor;
1910 model.content.nbformat_minor = this.nbformat_minor;
1943 // time the ajax call for autosave tuning purposes.
1911 // time the ajax call for autosave tuning purposes.
1944 var start = new Date().getTime();
1912 var start = new Date().getTime();
1945 // We do the call with settings so we can set cache to false.
1913 // We do the call with settings so we can set cache to false.
1946 var settings = {
1914 var settings = {
1947 processData : false,
1915 processData : false,
1948 cache : false,
1916 cache : false,
1949 type : "PUT",
1917 type : "PUT",
1950 data : JSON.stringify(model),
1918 data : JSON.stringify(model),
1951 contentType: 'application/json',
1919 contentType: 'application/json',
1952 dataType : "json",
1920 dataType : "json",
1953 success : $.proxy(this.save_notebook_success, this, start),
1921 success : $.proxy(this.save_notebook_success, this, start),
1954 error : $.proxy(this.save_notebook_error, this)
1922 error : $.proxy(this.save_notebook_error, this)
1955 };
1923 };
1956 if (extra_settings) {
1924 if (extra_settings) {
1957 for (var key in extra_settings) {
1925 for (var key in extra_settings) {
1958 settings[key] = extra_settings[key];
1926 settings[key] = extra_settings[key];
1959 }
1927 }
1960 }
1928 }
1961 this.events.trigger('notebook_saving.Notebook');
1929 this.events.trigger('notebook_saving.Notebook');
1962 var url = utils.url_join_encode(
1930 var url = utils.url_join_encode(
1963 this.base_url,
1931 this.base_url,
1964 'api/contents',
1932 'api/contents',
1965 this.notebook_path,
1933 this.notebook_path,
1966 this.notebook_name
1934 this.notebook_name
1967 );
1935 );
1968 $.ajax(url, settings);
1936 $.ajax(url, settings);
1969 };
1937 };
1970
1938
1971 /**
1939 /**
1972 * Success callback for saving a notebook.
1940 * Success callback for saving a notebook.
1973 *
1941 *
1974 * @method save_notebook_success
1942 * @method save_notebook_success
1975 * @param {Integer} start the time when the save request started
1943 * @param {Integer} start the time when the save request started
1976 * @param {Object} data JSON representation of a notebook
1944 * @param {Object} data JSON representation of a notebook
1977 * @param {String} status Description of response status
1945 * @param {String} status Description of response status
1978 * @param {jqXHR} xhr jQuery Ajax object
1946 * @param {jqXHR} xhr jQuery Ajax object
1979 */
1947 */
1980 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1948 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1981 this.set_dirty(false);
1949 this.set_dirty(false);
1982 if (data.message) {
1950 if (data.message) {
1983 // save succeeded, but validation failed.
1951 // save succeeded, but validation failed.
1984 var body = $("<div>");
1952 var body = $("<div>");
1985 var title = "Notebook validation failed";
1953 var title = "Notebook validation failed";
1986
1954
1987 body.append($("<p>").text(
1955 body.append($("<p>").text(
1988 "The save operation succeeded," +
1956 "The save operation succeeded," +
1989 " but the notebook does not appear to be valid." +
1957 " but the notebook does not appear to be valid." +
1990 " The validation error was:"
1958 " The validation error was:"
1991 )).append($("<div>").addClass("validation-error").append(
1959 )).append($("<div>").addClass("validation-error").append(
1992 $("<pre>").text(data.message)
1960 $("<pre>").text(data.message)
1993 ));
1961 ));
1994 dialog.modal({
1962 dialog.modal({
1995 notebook: this,
1963 notebook: this,
1996 keyboard_manager: this.keyboard_manager,
1964 keyboard_manager: this.keyboard_manager,
1997 title: title,
1965 title: title,
1998 body: body,
1966 body: body,
1999 buttons : {
1967 buttons : {
2000 OK : {
1968 OK : {
2001 "class" : "btn-primary"
1969 "class" : "btn-primary"
2002 }
1970 }
2003 }
1971 }
2004 });
1972 });
2005 }
1973 }
2006 this.events.trigger('notebook_saved.Notebook');
1974 this.events.trigger('notebook_saved.Notebook');
2007 this._update_autosave_interval(start);
1975 this._update_autosave_interval(start);
2008 if (this._checkpoint_after_save) {
1976 if (this._checkpoint_after_save) {
2009 this.create_checkpoint();
1977 this.create_checkpoint();
2010 this._checkpoint_after_save = false;
1978 this._checkpoint_after_save = false;
2011 }
1979 }
2012 };
1980 };
2013
1981
2014 /**
1982 /**
2015 * update the autosave interval based on how long the last save took
1983 * update the autosave interval based on how long the last save took
2016 *
1984 *
2017 * @method _update_autosave_interval
1985 * @method _update_autosave_interval
2018 * @param {Integer} timestamp when the save request started
1986 * @param {Integer} timestamp when the save request started
2019 */
1987 */
2020 Notebook.prototype._update_autosave_interval = function (start) {
1988 Notebook.prototype._update_autosave_interval = function (start) {
2021 var duration = (new Date().getTime() - start);
1989 var duration = (new Date().getTime() - start);
2022 if (this.autosave_interval) {
1990 if (this.autosave_interval) {
2023 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1991 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
2024 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1992 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
2025 // round to 10 seconds, otherwise we will be setting a new interval too often
1993 // round to 10 seconds, otherwise we will be setting a new interval too often
2026 interval = 10000 * Math.round(interval / 10000);
1994 interval = 10000 * Math.round(interval / 10000);
2027 // set new interval, if it's changed
1995 // set new interval, if it's changed
2028 if (interval != this.autosave_interval) {
1996 if (interval != this.autosave_interval) {
2029 this.set_autosave_interval(interval);
1997 this.set_autosave_interval(interval);
2030 }
1998 }
2031 }
1999 }
2032 };
2000 };
2033
2001
2034 /**
2002 /**
2035 * Failure callback for saving a notebook.
2003 * Failure callback for saving a notebook.
2036 *
2004 *
2037 * @method save_notebook_error
2005 * @method save_notebook_error
2038 * @param {jqXHR} xhr jQuery Ajax object
2006 * @param {jqXHR} xhr jQuery Ajax object
2039 * @param {String} status Description of response status
2007 * @param {String} status Description of response status
2040 * @param {String} error HTTP error message
2008 * @param {String} error HTTP error message
2041 */
2009 */
2042 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
2010 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
2043 this.events.trigger('notebook_save_failed.Notebook', [xhr, status, error]);
2011 this.events.trigger('notebook_save_failed.Notebook', [xhr, status, error]);
2044 };
2012 };
2045
2013
2046 /**
2014 /**
2047 * Explicitly trust the output of this notebook.
2015 * Explicitly trust the output of this notebook.
2048 *
2016 *
2049 * @method trust_notebook
2017 * @method trust_notebook
2050 */
2018 */
2051 Notebook.prototype.trust_notebook = function (extra_settings) {
2019 Notebook.prototype.trust_notebook = function (extra_settings) {
2052 var body = $("<div>").append($("<p>")
2020 var body = $("<div>").append($("<p>")
2053 .text("A trusted IPython notebook may execute hidden malicious code ")
2021 .text("A trusted IPython notebook may execute hidden malicious code ")
2054 .append($("<strong>")
2022 .append($("<strong>")
2055 .append(
2023 .append(
2056 $("<em>").text("when you open it")
2024 $("<em>").text("when you open it")
2057 )
2025 )
2058 ).append(".").append(
2026 ).append(".").append(
2059 " Selecting trust will immediately reload this notebook in a trusted state."
2027 " Selecting trust will immediately reload this notebook in a trusted state."
2060 ).append(
2028 ).append(
2061 " For more information, see the "
2029 " For more information, see the "
2062 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
2030 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
2063 .text("IPython security documentation")
2031 .text("IPython security documentation")
2064 ).append(".")
2032 ).append(".")
2065 );
2033 );
2066
2034
2067 var nb = this;
2035 var nb = this;
2068 dialog.modal({
2036 dialog.modal({
2069 notebook: this,
2037 notebook: this,
2070 keyboard_manager: this.keyboard_manager,
2038 keyboard_manager: this.keyboard_manager,
2071 title: "Trust this notebook?",
2039 title: "Trust this notebook?",
2072 body: body,
2040 body: body,
2073
2041
2074 buttons: {
2042 buttons: {
2075 Cancel : {},
2043 Cancel : {},
2076 Trust : {
2044 Trust : {
2077 class : "btn-danger",
2045 class : "btn-danger",
2078 click : function () {
2046 click : function () {
2079 var cells = nb.get_cells();
2047 var cells = nb.get_cells();
2080 for (var i = 0; i < cells.length; i++) {
2048 for (var i = 0; i < cells.length; i++) {
2081 var cell = cells[i];
2049 var cell = cells[i];
2082 if (cell.cell_type == 'code') {
2050 if (cell.cell_type == 'code') {
2083 cell.output_area.trusted = true;
2051 cell.output_area.trusted = true;
2084 }
2052 }
2085 }
2053 }
2086 nb.events.on('notebook_saved.Notebook', function () {
2054 nb.events.on('notebook_saved.Notebook', function () {
2087 window.location.reload();
2055 window.location.reload();
2088 });
2056 });
2089 nb.save_notebook();
2057 nb.save_notebook();
2090 }
2058 }
2091 }
2059 }
2092 }
2060 }
2093 });
2061 });
2094 };
2062 };
2095
2063
2096 Notebook.prototype.new_notebook = function(){
2064 Notebook.prototype.new_notebook = function(){
2097 var path = this.notebook_path;
2065 var path = this.notebook_path;
2098 var base_url = this.base_url;
2066 var base_url = this.base_url;
2099 var settings = {
2067 var settings = {
2100 processData : false,
2068 processData : false,
2101 cache : false,
2069 cache : false,
2102 type : "POST",
2070 type : "POST",
2103 dataType : "json",
2071 dataType : "json",
2104 async : false,
2072 async : false,
2105 success : function (data, status, xhr){
2073 success : function (data, status, xhr){
2106 var notebook_name = data.name;
2074 var notebook_name = data.name;
2107 window.open(
2075 window.open(
2108 utils.url_join_encode(
2076 utils.url_join_encode(
2109 base_url,
2077 base_url,
2110 'notebooks',
2078 'notebooks',
2111 path,
2079 path,
2112 notebook_name
2080 notebook_name
2113 ),
2081 ),
2114 '_blank'
2082 '_blank'
2115 );
2083 );
2116 },
2084 },
2117 error : utils.log_ajax_error,
2085 error : utils.log_ajax_error,
2118 };
2086 };
2119 var url = utils.url_join_encode(
2087 var url = utils.url_join_encode(
2120 base_url,
2088 base_url,
2121 'api/contents',
2089 'api/contents',
2122 path
2090 path
2123 );
2091 );
2124 $.ajax(url,settings);
2092 $.ajax(url,settings);
2125 };
2093 };
2126
2094
2127
2095
2128 Notebook.prototype.copy_notebook = function(){
2096 Notebook.prototype.copy_notebook = function(){
2129 var path = this.notebook_path;
2097 var path = this.notebook_path;
2130 var base_url = this.base_url;
2098 var base_url = this.base_url;
2131 var settings = {
2099 var settings = {
2132 processData : false,
2100 processData : false,
2133 cache : false,
2101 cache : false,
2134 type : "POST",
2102 type : "POST",
2135 dataType : "json",
2103 dataType : "json",
2136 data : JSON.stringify({copy_from : this.notebook_name}),
2104 data : JSON.stringify({copy_from : this.notebook_name}),
2137 async : false,
2105 async : false,
2138 success : function (data, status, xhr) {
2106 success : function (data, status, xhr) {
2139 window.open(utils.url_join_encode(
2107 window.open(utils.url_join_encode(
2140 base_url,
2108 base_url,
2141 'notebooks',
2109 'notebooks',
2142 data.path,
2110 data.path,
2143 data.name
2111 data.name
2144 ), '_blank');
2112 ), '_blank');
2145 },
2113 },
2146 error : utils.log_ajax_error,
2114 error : utils.log_ajax_error,
2147 };
2115 };
2148 var url = utils.url_join_encode(
2116 var url = utils.url_join_encode(
2149 base_url,
2117 base_url,
2150 'api/contents',
2118 'api/contents',
2151 path
2119 path
2152 );
2120 );
2153 $.ajax(url,settings);
2121 $.ajax(url,settings);
2154 };
2122 };
2155
2123
2156 Notebook.prototype.rename = function (nbname) {
2124 Notebook.prototype.rename = function (nbname) {
2157 var that = this;
2125 var that = this;
2158 if (!nbname.match(/\.ipynb$/)) {
2126 if (!nbname.match(/\.ipynb$/)) {
2159 nbname = nbname + ".ipynb";
2127 nbname = nbname + ".ipynb";
2160 }
2128 }
2161 var data = {name: nbname};
2129 var data = {name: nbname};
2162 var settings = {
2130 var settings = {
2163 processData : false,
2131 processData : false,
2164 cache : false,
2132 cache : false,
2165 type : "PATCH",
2133 type : "PATCH",
2166 data : JSON.stringify(data),
2134 data : JSON.stringify(data),
2167 dataType: "json",
2135 dataType: "json",
2168 contentType: 'application/json',
2136 contentType: 'application/json',
2169 success : $.proxy(that.rename_success, this),
2137 success : $.proxy(that.rename_success, this),
2170 error : $.proxy(that.rename_error, this)
2138 error : $.proxy(that.rename_error, this)
2171 };
2139 };
2172 this.events.trigger('rename_notebook.Notebook', data);
2140 this.events.trigger('rename_notebook.Notebook', data);
2173 var url = utils.url_join_encode(
2141 var url = utils.url_join_encode(
2174 this.base_url,
2142 this.base_url,
2175 'api/contents',
2143 'api/contents',
2176 this.notebook_path,
2144 this.notebook_path,
2177 this.notebook_name
2145 this.notebook_name
2178 );
2146 );
2179 $.ajax(url, settings);
2147 $.ajax(url, settings);
2180 };
2148 };
2181
2149
2182 Notebook.prototype.delete = function () {
2150 Notebook.prototype.delete = function () {
2183 var that = this;
2151 var that = this;
2184 var settings = {
2152 var settings = {
2185 processData : false,
2153 processData : false,
2186 cache : false,
2154 cache : false,
2187 type : "DELETE",
2155 type : "DELETE",
2188 dataType: "json",
2156 dataType: "json",
2189 error : utils.log_ajax_error,
2157 error : utils.log_ajax_error,
2190 };
2158 };
2191 var url = utils.url_join_encode(
2159 var url = utils.url_join_encode(
2192 this.base_url,
2160 this.base_url,
2193 'api/contents',
2161 'api/contents',
2194 this.notebook_path,
2162 this.notebook_path,
2195 this.notebook_name
2163 this.notebook_name
2196 );
2164 );
2197 $.ajax(url, settings);
2165 $.ajax(url, settings);
2198 };
2166 };
2199
2167
2200
2168
2201 Notebook.prototype.rename_success = function (json, status, xhr) {
2169 Notebook.prototype.rename_success = function (json, status, xhr) {
2202 var name = this.notebook_name = json.name;
2170 var name = this.notebook_name = json.name;
2203 var path = json.path;
2171 var path = json.path;
2204 this.session.rename_notebook(name, path);
2172 this.session.rename_notebook(name, path);
2205 this.events.trigger('notebook_renamed.Notebook', json);
2173 this.events.trigger('notebook_renamed.Notebook', json);
2206 };
2174 };
2207
2175
2208 Notebook.prototype.rename_error = function (xhr, status, error) {
2176 Notebook.prototype.rename_error = function (xhr, status, error) {
2209 var that = this;
2177 var that = this;
2210 var dialog_body = $('<div/>').append(
2178 var dialog_body = $('<div/>').append(
2211 $("<p/>").text('This notebook name already exists.')
2179 $("<p/>").text('This notebook name already exists.')
2212 );
2180 );
2213 this.events.trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
2181 this.events.trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
2214 dialog.modal({
2182 dialog.modal({
2215 notebook: this,
2183 notebook: this,
2216 keyboard_manager: this.keyboard_manager,
2184 keyboard_manager: this.keyboard_manager,
2217 title: "Notebook Rename Error!",
2185 title: "Notebook Rename Error!",
2218 body: dialog_body,
2186 body: dialog_body,
2219 buttons : {
2187 buttons : {
2220 "Cancel": {},
2188 "Cancel": {},
2221 "OK": {
2189 "OK": {
2222 class: "btn-primary",
2190 class: "btn-primary",
2223 click: function () {
2191 click: function () {
2224 this.save_widget.rename_notebook({notebook:that});
2192 this.save_widget.rename_notebook({notebook:that});
2225 }}
2193 }}
2226 },
2194 },
2227 open : function (event, ui) {
2195 open : function (event, ui) {
2228 var that = $(this);
2196 var that = $(this);
2229 // Upon ENTER, click the OK button.
2197 // Upon ENTER, click the OK button.
2230 that.find('input[type="text"]').keydown(function (event, ui) {
2198 that.find('input[type="text"]').keydown(function (event, ui) {
2231 if (event.which === this.keyboard.keycodes.enter) {
2199 if (event.which === this.keyboard.keycodes.enter) {
2232 that.find('.btn-primary').first().click();
2200 that.find('.btn-primary').first().click();
2233 }
2201 }
2234 });
2202 });
2235 that.find('input[type="text"]').focus();
2203 that.find('input[type="text"]').focus();
2236 }
2204 }
2237 });
2205 });
2238 };
2206 };
2239
2207
2240 /**
2208 /**
2241 * Request a notebook's data from the server.
2209 * Request a notebook's data from the server.
2242 *
2210 *
2243 * @method load_notebook
2211 * @method load_notebook
2244 * @param {String} notebook_name and path A notebook to load
2212 * @param {String} notebook_name and path A notebook to load
2245 */
2213 */
2246 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
2214 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
2247 var that = this;
2215 var that = this;
2248 this.notebook_name = notebook_name;
2216 this.notebook_name = notebook_name;
2249 this.notebook_path = notebook_path;
2217 this.notebook_path = notebook_path;
2250 // We do the call with settings so we can set cache to false.
2218 // We do the call with settings so we can set cache to false.
2251 var settings = {
2219 var settings = {
2252 processData : false,
2220 processData : false,
2253 cache : false,
2221 cache : false,
2254 type : "GET",
2222 type : "GET",
2255 dataType : "json",
2223 dataType : "json",
2256 success : $.proxy(this.load_notebook_success,this),
2224 success : $.proxy(this.load_notebook_success,this),
2257 error : $.proxy(this.load_notebook_error,this),
2225 error : $.proxy(this.load_notebook_error,this),
2258 };
2226 };
2259 this.events.trigger('notebook_loading.Notebook');
2227 this.events.trigger('notebook_loading.Notebook');
2260 var url = utils.url_join_encode(
2228 var url = utils.url_join_encode(
2261 this.base_url,
2229 this.base_url,
2262 'api/contents',
2230 'api/contents',
2263 this.notebook_path,
2231 this.notebook_path,
2264 this.notebook_name
2232 this.notebook_name
2265 );
2233 );
2266 $.ajax(url, settings);
2234 $.ajax(url, settings);
2267 };
2235 };
2268
2236
2269 /**
2237 /**
2270 * Success callback for loading a notebook from the server.
2238 * Success callback for loading a notebook from the server.
2271 *
2239 *
2272 * Load notebook data from the JSON response.
2240 * Load notebook data from the JSON response.
2273 *
2241 *
2274 * @method load_notebook_success
2242 * @method load_notebook_success
2275 * @param {Object} data JSON representation of a notebook
2243 * @param {Object} data JSON representation of a notebook
2276 * @param {String} status Description of response status
2244 * @param {String} status Description of response status
2277 * @param {jqXHR} xhr jQuery Ajax object
2245 * @param {jqXHR} xhr jQuery Ajax object
2278 */
2246 */
2279 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
2247 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
2280 var failed;
2248 var failed;
2281 try {
2249 try {
2282 this.fromJSON(data);
2250 this.fromJSON(data);
2283 } catch (e) {
2251 } catch (e) {
2284 failed = e;
2252 failed = e;
2285 console.log("Notebook failed to load from JSON:", e);
2253 console.log("Notebook failed to load from JSON:", e);
2286 }
2254 }
2287 if (failed || data.message) {
2255 if (failed || data.message) {
2288 // *either* fromJSON failed or validation failed
2256 // *either* fromJSON failed or validation failed
2289 var body = $("<div>");
2257 var body = $("<div>");
2290 var title;
2258 var title;
2291 if (failed) {
2259 if (failed) {
2292 title = "Notebook failed to load";
2260 title = "Notebook failed to load";
2293 body.append($("<p>").text(
2261 body.append($("<p>").text(
2294 "The error was: "
2262 "The error was: "
2295 )).append($("<div>").addClass("js-error").text(
2263 )).append($("<div>").addClass("js-error").text(
2296 failed.toString()
2264 failed.toString()
2297 )).append($("<p>").text(
2265 )).append($("<p>").text(
2298 "See the error console for details."
2266 "See the error console for details."
2299 ));
2267 ));
2300 } else {
2268 } else {
2301 title = "Notebook validation failed";
2269 title = "Notebook validation failed";
2302 }
2270 }
2303
2271
2304 if (data.message) {
2272 if (data.message) {
2305 var msg;
2273 var msg;
2306 if (failed) {
2274 if (failed) {
2307 msg = "The notebook also failed validation:"
2275 msg = "The notebook also failed validation:"
2308 } else {
2276 } else {
2309 msg = "An invalid notebook may not function properly." +
2277 msg = "An invalid notebook may not function properly." +
2310 " The validation error was:"
2278 " The validation error was:"
2311 }
2279 }
2312 body.append($("<p>").text(
2280 body.append($("<p>").text(
2313 msg
2281 msg
2314 )).append($("<div>").addClass("validation-error").append(
2282 )).append($("<div>").addClass("validation-error").append(
2315 $("<pre>").text(data.message)
2283 $("<pre>").text(data.message)
2316 ));
2284 ));
2317 }
2285 }
2318
2286
2319 dialog.modal({
2287 dialog.modal({
2320 notebook: this,
2288 notebook: this,
2321 keyboard_manager: this.keyboard_manager,
2289 keyboard_manager: this.keyboard_manager,
2322 title: title,
2290 title: title,
2323 body: body,
2291 body: body,
2324 buttons : {
2292 buttons : {
2325 OK : {
2293 OK : {
2326 "class" : "btn-primary"
2294 "class" : "btn-primary"
2327 }
2295 }
2328 }
2296 }
2329 });
2297 });
2330 }
2298 }
2331 if (this.ncells() === 0) {
2299 if (this.ncells() === 0) {
2332 this.insert_cell_below('code');
2300 this.insert_cell_below('code');
2333 this.edit_mode(0);
2301 this.edit_mode(0);
2334 } else {
2302 } else {
2335 this.select(0);
2303 this.select(0);
2336 this.handle_command_mode(this.get_cell(0));
2304 this.handle_command_mode(this.get_cell(0));
2337 }
2305 }
2338 this.set_dirty(false);
2306 this.set_dirty(false);
2339 this.scroll_to_top();
2307 this.scroll_to_top();
2340 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
2308 var nbmodel = data.content;
2309 var orig_nbformat = nbmodel.metadata.orig_nbformat;
2310 var orig_nbformat_minor = nbmodel.metadata.orig_nbformat_minor;
2311 if (orig_nbformat !== undefined && nbmodel.nbformat !== orig_nbformat) {
2341 var msg = "This notebook has been converted from an older " +
2312 var msg = "This notebook has been converted from an older " +
2342 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
2313 "notebook format (v"+orig_nbformat+") to the current notebook " +
2343 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
2314 "format (v"+nbmodel.nbformat+"). The next time you save this notebook, the " +
2344 "newer notebook format will be used and older versions of IPython " +
2315 "newer notebook format will be used and older versions of IPython " +
2345 "may not be able to read it. To keep the older version, close the " +
2316 "may not be able to read it. To keep the older version, close the " +
2346 "notebook without saving it.";
2317 "notebook without saving it.";
2347 dialog.modal({
2318 dialog.modal({
2348 notebook: this,
2319 notebook: this,
2349 keyboard_manager: this.keyboard_manager,
2320 keyboard_manager: this.keyboard_manager,
2350 title : "Notebook converted",
2321 title : "Notebook converted",
2351 body : msg,
2322 body : msg,
2352 buttons : {
2323 buttons : {
2353 OK : {
2324 OK : {
2354 class : "btn-primary"
2325 class : "btn-primary"
2355 }
2326 }
2356 }
2327 }
2357 });
2328 });
2358 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
2329 } else if (orig_nbformat_minor !== undefined && nbmodel.nbformat_minor !== orig_nbformat_minor) {
2359 var that = this;
2330 var that = this;
2360 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
2331 var orig_vs = 'v' + nbmodel.nbformat + '.' + orig_nbformat_minor;
2361 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
2332 var this_vs = 'v' + nbmodel.nbformat + '.' + this.nbformat_minor;
2362 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2333 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2363 this_vs + ". You can still work with this notebook, but some features " +
2334 this_vs + ". You can still work with this notebook, but some features " +
2364 "introduced in later notebook versions may not be available.";
2335 "introduced in later notebook versions may not be available.";
2365
2336
2366 dialog.modal({
2337 dialog.modal({
2367 notebook: this,
2338 notebook: this,
2368 keyboard_manager: this.keyboard_manager,
2339 keyboard_manager: this.keyboard_manager,
2369 title : "Newer Notebook",
2340 title : "Newer Notebook",
2370 body : msg,
2341 body : msg,
2371 buttons : {
2342 buttons : {
2372 OK : {
2343 OK : {
2373 class : "btn-danger"
2344 class : "btn-danger"
2374 }
2345 }
2375 }
2346 }
2376 });
2347 });
2377
2348
2378 }
2349 }
2379
2350
2380 // Create the session after the notebook is completely loaded to prevent
2351 // Create the session after the notebook is completely loaded to prevent
2381 // code execution upon loading, which is a security risk.
2352 // code execution upon loading, which is a security risk.
2382 if (this.session === null) {
2353 if (this.session === null) {
2383 var kernelspec = this.metadata.kernelspec || {};
2354 var kernelspec = this.metadata.kernelspec || {};
2384 var kernel_name = kernelspec.name;
2355 var kernel_name = kernelspec.name;
2385
2356
2386 this.start_session(kernel_name);
2357 this.start_session(kernel_name);
2387 }
2358 }
2388 // load our checkpoint list
2359 // load our checkpoint list
2389 this.list_checkpoints();
2360 this.list_checkpoints();
2390
2361
2391 // load toolbar state
2362 // load toolbar state
2392 if (this.metadata.celltoolbar) {
2363 if (this.metadata.celltoolbar) {
2393 celltoolbar.CellToolbar.global_show();
2364 celltoolbar.CellToolbar.global_show();
2394 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2365 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2395 } else {
2366 } else {
2396 celltoolbar.CellToolbar.global_hide();
2367 celltoolbar.CellToolbar.global_hide();
2397 }
2368 }
2398
2369
2399 // now that we're fully loaded, it is safe to restore save functionality
2370 // now that we're fully loaded, it is safe to restore save functionality
2400 delete(this.save_notebook);
2371 delete(this.save_notebook);
2401 this.events.trigger('notebook_loaded.Notebook');
2372 this.events.trigger('notebook_loaded.Notebook');
2402 };
2373 };
2403
2374
2404 /**
2375 /**
2405 * Failure callback for loading a notebook from the server.
2376 * Failure callback for loading a notebook from the server.
2406 *
2377 *
2407 * @method load_notebook_error
2378 * @method load_notebook_error
2408 * @param {jqXHR} xhr jQuery Ajax object
2379 * @param {jqXHR} xhr jQuery Ajax object
2409 * @param {String} status Description of response status
2380 * @param {String} status Description of response status
2410 * @param {String} error HTTP error message
2381 * @param {String} error HTTP error message
2411 */
2382 */
2412 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
2383 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
2413 this.events.trigger('notebook_load_failed.Notebook', [xhr, status, error]);
2384 this.events.trigger('notebook_load_failed.Notebook', [xhr, status, error]);
2414 utils.log_ajax_error(xhr, status, error);
2385 utils.log_ajax_error(xhr, status, error);
2415 var msg;
2386 var msg;
2416 if (xhr.status === 400) {
2387 if (xhr.status === 400) {
2417 msg = escape(utils.ajax_error_msg(xhr));
2388 msg = escape(utils.ajax_error_msg(xhr));
2418 } else if (xhr.status === 500) {
2389 } else if (xhr.status === 500) {
2419 msg = "An unknown error occurred while loading this notebook. " +
2390 msg = "An unknown error occurred while loading this notebook. " +
2420 "This version can load notebook formats " +
2391 "This version can load notebook formats " +
2421 "v" + this.nbformat + " or earlier. See the server log for details.";
2392 "v" + this.nbformat + " or earlier. See the server log for details.";
2422 }
2393 }
2423 dialog.modal({
2394 dialog.modal({
2424 notebook: this,
2395 notebook: this,
2425 keyboard_manager: this.keyboard_manager,
2396 keyboard_manager: this.keyboard_manager,
2426 title: "Error loading notebook",
2397 title: "Error loading notebook",
2427 body : msg,
2398 body : msg,
2428 buttons : {
2399 buttons : {
2429 "OK": {}
2400 "OK": {}
2430 }
2401 }
2431 });
2402 });
2432 };
2403 };
2433
2404
2434 /********************* checkpoint-related *********************/
2405 /********************* checkpoint-related *********************/
2435
2406
2436 /**
2407 /**
2437 * Save the notebook then immediately create a checkpoint.
2408 * Save the notebook then immediately create a checkpoint.
2438 *
2409 *
2439 * @method save_checkpoint
2410 * @method save_checkpoint
2440 */
2411 */
2441 Notebook.prototype.save_checkpoint = function () {
2412 Notebook.prototype.save_checkpoint = function () {
2442 this._checkpoint_after_save = true;
2413 this._checkpoint_after_save = true;
2443 this.save_notebook();
2414 this.save_notebook();
2444 };
2415 };
2445
2416
2446 /**
2417 /**
2447 * Add a checkpoint for this notebook.
2418 * Add a checkpoint for this notebook.
2448 * for use as a callback from checkpoint creation.
2419 * for use as a callback from checkpoint creation.
2449 *
2420 *
2450 * @method add_checkpoint
2421 * @method add_checkpoint
2451 */
2422 */
2452 Notebook.prototype.add_checkpoint = function (checkpoint) {
2423 Notebook.prototype.add_checkpoint = function (checkpoint) {
2453 var found = false;
2424 var found = false;
2454 for (var i = 0; i < this.checkpoints.length; i++) {
2425 for (var i = 0; i < this.checkpoints.length; i++) {
2455 var existing = this.checkpoints[i];
2426 var existing = this.checkpoints[i];
2456 if (existing.id == checkpoint.id) {
2427 if (existing.id == checkpoint.id) {
2457 found = true;
2428 found = true;
2458 this.checkpoints[i] = checkpoint;
2429 this.checkpoints[i] = checkpoint;
2459 break;
2430 break;
2460 }
2431 }
2461 }
2432 }
2462 if (!found) {
2433 if (!found) {
2463 this.checkpoints.push(checkpoint);
2434 this.checkpoints.push(checkpoint);
2464 }
2435 }
2465 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2436 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2466 };
2437 };
2467
2438
2468 /**
2439 /**
2469 * List checkpoints for this notebook.
2440 * List checkpoints for this notebook.
2470 *
2441 *
2471 * @method list_checkpoints
2442 * @method list_checkpoints
2472 */
2443 */
2473 Notebook.prototype.list_checkpoints = function () {
2444 Notebook.prototype.list_checkpoints = function () {
2474 var url = utils.url_join_encode(
2445 var url = utils.url_join_encode(
2475 this.base_url,
2446 this.base_url,
2476 'api/contents',
2447 'api/contents',
2477 this.notebook_path,
2448 this.notebook_path,
2478 this.notebook_name,
2449 this.notebook_name,
2479 'checkpoints'
2450 'checkpoints'
2480 );
2451 );
2481 $.get(url).done(
2452 $.get(url).done(
2482 $.proxy(this.list_checkpoints_success, this)
2453 $.proxy(this.list_checkpoints_success, this)
2483 ).fail(
2454 ).fail(
2484 $.proxy(this.list_checkpoints_error, this)
2455 $.proxy(this.list_checkpoints_error, this)
2485 );
2456 );
2486 };
2457 };
2487
2458
2488 /**
2459 /**
2489 * Success callback for listing checkpoints.
2460 * Success callback for listing checkpoints.
2490 *
2461 *
2491 * @method list_checkpoint_success
2462 * @method list_checkpoint_success
2492 * @param {Object} data JSON representation of a checkpoint
2463 * @param {Object} data JSON representation of a checkpoint
2493 * @param {String} status Description of response status
2464 * @param {String} status Description of response status
2494 * @param {jqXHR} xhr jQuery Ajax object
2465 * @param {jqXHR} xhr jQuery Ajax object
2495 */
2466 */
2496 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2467 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2497 data = $.parseJSON(data);
2468 data = $.parseJSON(data);
2498 this.checkpoints = data;
2469 this.checkpoints = data;
2499 if (data.length) {
2470 if (data.length) {
2500 this.last_checkpoint = data[data.length - 1];
2471 this.last_checkpoint = data[data.length - 1];
2501 } else {
2472 } else {
2502 this.last_checkpoint = null;
2473 this.last_checkpoint = null;
2503 }
2474 }
2504 this.events.trigger('checkpoints_listed.Notebook', [data]);
2475 this.events.trigger('checkpoints_listed.Notebook', [data]);
2505 };
2476 };
2506
2477
2507 /**
2478 /**
2508 * Failure callback for listing a checkpoint.
2479 * Failure callback for listing a checkpoint.
2509 *
2480 *
2510 * @method list_checkpoint_error
2481 * @method list_checkpoint_error
2511 * @param {jqXHR} xhr jQuery Ajax object
2482 * @param {jqXHR} xhr jQuery Ajax object
2512 * @param {String} status Description of response status
2483 * @param {String} status Description of response status
2513 * @param {String} error_msg HTTP error message
2484 * @param {String} error_msg HTTP error message
2514 */
2485 */
2515 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2486 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2516 this.events.trigger('list_checkpoints_failed.Notebook');
2487 this.events.trigger('list_checkpoints_failed.Notebook');
2517 };
2488 };
2518
2489
2519 /**
2490 /**
2520 * Create a checkpoint of this notebook on the server from the most recent save.
2491 * Create a checkpoint of this notebook on the server from the most recent save.
2521 *
2492 *
2522 * @method create_checkpoint
2493 * @method create_checkpoint
2523 */
2494 */
2524 Notebook.prototype.create_checkpoint = function () {
2495 Notebook.prototype.create_checkpoint = function () {
2525 var url = utils.url_join_encode(
2496 var url = utils.url_join_encode(
2526 this.base_url,
2497 this.base_url,
2527 'api/contents',
2498 'api/contents',
2528 this.notebook_path,
2499 this.notebook_path,
2529 this.notebook_name,
2500 this.notebook_name,
2530 'checkpoints'
2501 'checkpoints'
2531 );
2502 );
2532 $.post(url).done(
2503 $.post(url).done(
2533 $.proxy(this.create_checkpoint_success, this)
2504 $.proxy(this.create_checkpoint_success, this)
2534 ).fail(
2505 ).fail(
2535 $.proxy(this.create_checkpoint_error, this)
2506 $.proxy(this.create_checkpoint_error, this)
2536 );
2507 );
2537 };
2508 };
2538
2509
2539 /**
2510 /**
2540 * Success callback for creating a checkpoint.
2511 * Success callback for creating a checkpoint.
2541 *
2512 *
2542 * @method create_checkpoint_success
2513 * @method create_checkpoint_success
2543 * @param {Object} data JSON representation of a checkpoint
2514 * @param {Object} data JSON representation of a checkpoint
2544 * @param {String} status Description of response status
2515 * @param {String} status Description of response status
2545 * @param {jqXHR} xhr jQuery Ajax object
2516 * @param {jqXHR} xhr jQuery Ajax object
2546 */
2517 */
2547 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2518 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2548 data = $.parseJSON(data);
2519 data = $.parseJSON(data);
2549 this.add_checkpoint(data);
2520 this.add_checkpoint(data);
2550 this.events.trigger('checkpoint_created.Notebook', data);
2521 this.events.trigger('checkpoint_created.Notebook', data);
2551 };
2522 };
2552
2523
2553 /**
2524 /**
2554 * Failure callback for creating a checkpoint.
2525 * Failure callback for creating a checkpoint.
2555 *
2526 *
2556 * @method create_checkpoint_error
2527 * @method create_checkpoint_error
2557 * @param {jqXHR} xhr jQuery Ajax object
2528 * @param {jqXHR} xhr jQuery Ajax object
2558 * @param {String} status Description of response status
2529 * @param {String} status Description of response status
2559 * @param {String} error_msg HTTP error message
2530 * @param {String} error_msg HTTP error message
2560 */
2531 */
2561 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2532 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2562 this.events.trigger('checkpoint_failed.Notebook');
2533 this.events.trigger('checkpoint_failed.Notebook');
2563 };
2534 };
2564
2535
2565 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2536 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2566 var that = this;
2537 var that = this;
2567 checkpoint = checkpoint || this.last_checkpoint;
2538 checkpoint = checkpoint || this.last_checkpoint;
2568 if ( ! checkpoint ) {
2539 if ( ! checkpoint ) {
2569 console.log("restore dialog, but no checkpoint to restore to!");
2540 console.log("restore dialog, but no checkpoint to restore to!");
2570 return;
2541 return;
2571 }
2542 }
2572 var body = $('<div/>').append(
2543 var body = $('<div/>').append(
2573 $('<p/>').addClass("p-space").text(
2544 $('<p/>').addClass("p-space").text(
2574 "Are you sure you want to revert the notebook to " +
2545 "Are you sure you want to revert the notebook to " +
2575 "the latest checkpoint?"
2546 "the latest checkpoint?"
2576 ).append(
2547 ).append(
2577 $("<strong/>").text(
2548 $("<strong/>").text(
2578 " This cannot be undone."
2549 " This cannot be undone."
2579 )
2550 )
2580 )
2551 )
2581 ).append(
2552 ).append(
2582 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2553 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2583 ).append(
2554 ).append(
2584 $('<p/>').addClass("p-space").text(
2555 $('<p/>').addClass("p-space").text(
2585 Date(checkpoint.last_modified)
2556 Date(checkpoint.last_modified)
2586 ).css("text-align", "center")
2557 ).css("text-align", "center")
2587 );
2558 );
2588
2559
2589 dialog.modal({
2560 dialog.modal({
2590 notebook: this,
2561 notebook: this,
2591 keyboard_manager: this.keyboard_manager,
2562 keyboard_manager: this.keyboard_manager,
2592 title : "Revert notebook to checkpoint",
2563 title : "Revert notebook to checkpoint",
2593 body : body,
2564 body : body,
2594 buttons : {
2565 buttons : {
2595 Revert : {
2566 Revert : {
2596 class : "btn-danger",
2567 class : "btn-danger",
2597 click : function () {
2568 click : function () {
2598 that.restore_checkpoint(checkpoint.id);
2569 that.restore_checkpoint(checkpoint.id);
2599 }
2570 }
2600 },
2571 },
2601 Cancel : {}
2572 Cancel : {}
2602 }
2573 }
2603 });
2574 });
2604 };
2575 };
2605
2576
2606 /**
2577 /**
2607 * Restore the notebook to a checkpoint state.
2578 * Restore the notebook to a checkpoint state.
2608 *
2579 *
2609 * @method restore_checkpoint
2580 * @method restore_checkpoint
2610 * @param {String} checkpoint ID
2581 * @param {String} checkpoint ID
2611 */
2582 */
2612 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2583 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2613 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2584 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2614 var url = utils.url_join_encode(
2585 var url = utils.url_join_encode(
2615 this.base_url,
2586 this.base_url,
2616 'api/contents',
2587 'api/contents',
2617 this.notebook_path,
2588 this.notebook_path,
2618 this.notebook_name,
2589 this.notebook_name,
2619 'checkpoints',
2590 'checkpoints',
2620 checkpoint
2591 checkpoint
2621 );
2592 );
2622 $.post(url).done(
2593 $.post(url).done(
2623 $.proxy(this.restore_checkpoint_success, this)
2594 $.proxy(this.restore_checkpoint_success, this)
2624 ).fail(
2595 ).fail(
2625 $.proxy(this.restore_checkpoint_error, this)
2596 $.proxy(this.restore_checkpoint_error, this)
2626 );
2597 );
2627 };
2598 };
2628
2599
2629 /**
2600 /**
2630 * Success callback for restoring a notebook to a checkpoint.
2601 * Success callback for restoring a notebook to a checkpoint.
2631 *
2602 *
2632 * @method restore_checkpoint_success
2603 * @method restore_checkpoint_success
2633 * @param {Object} data (ignored, should be empty)
2604 * @param {Object} data (ignored, should be empty)
2634 * @param {String} status Description of response status
2605 * @param {String} status Description of response status
2635 * @param {jqXHR} xhr jQuery Ajax object
2606 * @param {jqXHR} xhr jQuery Ajax object
2636 */
2607 */
2637 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2608 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2638 this.events.trigger('checkpoint_restored.Notebook');
2609 this.events.trigger('checkpoint_restored.Notebook');
2639 this.load_notebook(this.notebook_name, this.notebook_path);
2610 this.load_notebook(this.notebook_name, this.notebook_path);
2640 };
2611 };
2641
2612
2642 /**
2613 /**
2643 * Failure callback for restoring a notebook to a checkpoint.
2614 * Failure callback for restoring a notebook to a checkpoint.
2644 *
2615 *
2645 * @method restore_checkpoint_error
2616 * @method restore_checkpoint_error
2646 * @param {jqXHR} xhr jQuery Ajax object
2617 * @param {jqXHR} xhr jQuery Ajax object
2647 * @param {String} status Description of response status
2618 * @param {String} status Description of response status
2648 * @param {String} error_msg HTTP error message
2619 * @param {String} error_msg HTTP error message
2649 */
2620 */
2650 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2621 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2651 this.events.trigger('checkpoint_restore_failed.Notebook');
2622 this.events.trigger('checkpoint_restore_failed.Notebook');
2652 };
2623 };
2653
2624
2654 /**
2625 /**
2655 * Delete a notebook checkpoint.
2626 * Delete a notebook checkpoint.
2656 *
2627 *
2657 * @method delete_checkpoint
2628 * @method delete_checkpoint
2658 * @param {String} checkpoint ID
2629 * @param {String} checkpoint ID
2659 */
2630 */
2660 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2631 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2661 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2632 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2662 var url = utils.url_join_encode(
2633 var url = utils.url_join_encode(
2663 this.base_url,
2634 this.base_url,
2664 'api/contents',
2635 'api/contents',
2665 this.notebook_path,
2636 this.notebook_path,
2666 this.notebook_name,
2637 this.notebook_name,
2667 'checkpoints',
2638 'checkpoints',
2668 checkpoint
2639 checkpoint
2669 );
2640 );
2670 $.ajax(url, {
2641 $.ajax(url, {
2671 type: 'DELETE',
2642 type: 'DELETE',
2672 success: $.proxy(this.delete_checkpoint_success, this),
2643 success: $.proxy(this.delete_checkpoint_success, this),
2673 error: $.proxy(this.delete_checkpoint_error, this)
2644 error: $.proxy(this.delete_checkpoint_error, this)
2674 });
2645 });
2675 };
2646 };
2676
2647
2677 /**
2648 /**
2678 * Success callback for deleting a notebook checkpoint
2649 * Success callback for deleting a notebook checkpoint
2679 *
2650 *
2680 * @method delete_checkpoint_success
2651 * @method delete_checkpoint_success
2681 * @param {Object} data (ignored, should be empty)
2652 * @param {Object} data (ignored, should be empty)
2682 * @param {String} status Description of response status
2653 * @param {String} status Description of response status
2683 * @param {jqXHR} xhr jQuery Ajax object
2654 * @param {jqXHR} xhr jQuery Ajax object
2684 */
2655 */
2685 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2656 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2686 this.events.trigger('checkpoint_deleted.Notebook', data);
2657 this.events.trigger('checkpoint_deleted.Notebook', data);
2687 this.load_notebook(this.notebook_name, this.notebook_path);
2658 this.load_notebook(this.notebook_name, this.notebook_path);
2688 };
2659 };
2689
2660
2690 /**
2661 /**
2691 * Failure callback for deleting a notebook checkpoint.
2662 * Failure callback for deleting a notebook checkpoint.
2692 *
2663 *
2693 * @method delete_checkpoint_error
2664 * @method delete_checkpoint_error
2694 * @param {jqXHR} xhr jQuery Ajax object
2665 * @param {jqXHR} xhr jQuery Ajax object
2695 * @param {String} status Description of response status
2666 * @param {String} status Description of response status
2696 * @param {String} error HTTP error message
2667 * @param {String} error HTTP error message
2697 */
2668 */
2698 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error) {
2669 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error) {
2699 this.events.trigger('checkpoint_delete_failed.Notebook', [xhr, status, error]);
2670 this.events.trigger('checkpoint_delete_failed.Notebook', [xhr, status, error]);
2700 };
2671 };
2701
2672
2702
2673
2703 // For backwards compatability.
2674 // For backwards compatability.
2704 IPython.Notebook = Notebook;
2675 IPython.Notebook = Notebook;
2705
2676
2706 return {'Notebook': Notebook};
2677 return {'Notebook': Notebook};
2707 });
2678 });
@@ -1,1002 +1,941 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 'base/js/namespace',
5 'base/js/namespace',
6 'jqueryui',
6 'jqueryui',
7 'base/js/utils',
7 'base/js/utils',
8 'base/js/security',
8 'base/js/security',
9 'base/js/keyboard',
9 'base/js/keyboard',
10 'notebook/js/mathjaxutils',
10 'notebook/js/mathjaxutils',
11 'components/marked/lib/marked',
11 'components/marked/lib/marked',
12 ], function(IPython, $, utils, security, keyboard, mathjaxutils, marked) {
12 ], function(IPython, $, utils, security, keyboard, mathjaxutils, marked) {
13 "use strict";
13 "use strict";
14
14
15 /**
15 /**
16 * @class OutputArea
16 * @class OutputArea
17 *
17 *
18 * @constructor
18 * @constructor
19 */
19 */
20
20
21 var OutputArea = function (options) {
21 var OutputArea = function (options) {
22 this.selector = options.selector;
22 this.selector = options.selector;
23 this.events = options.events;
23 this.events = options.events;
24 this.keyboard_manager = options.keyboard_manager;
24 this.keyboard_manager = options.keyboard_manager;
25 this.wrapper = $(options.selector);
25 this.wrapper = $(options.selector);
26 this.outputs = [];
26 this.outputs = [];
27 this.collapsed = false;
27 this.collapsed = false;
28 this.scrolled = false;
28 this.scrolled = false;
29 this.trusted = true;
29 this.trusted = true;
30 this.clear_queued = null;
30 this.clear_queued = null;
31 if (options.prompt_area === undefined) {
31 if (options.prompt_area === undefined) {
32 this.prompt_area = true;
32 this.prompt_area = true;
33 } else {
33 } else {
34 this.prompt_area = options.prompt_area;
34 this.prompt_area = options.prompt_area;
35 }
35 }
36 this.create_elements();
36 this.create_elements();
37 this.style();
37 this.style();
38 this.bind_events();
38 this.bind_events();
39 };
39 };
40
40
41
41
42 /**
42 /**
43 * Class prototypes
43 * Class prototypes
44 **/
44 **/
45
45
46 OutputArea.prototype.create_elements = function () {
46 OutputArea.prototype.create_elements = function () {
47 this.element = $("<div/>");
47 this.element = $("<div/>");
48 this.collapse_button = $("<div/>");
48 this.collapse_button = $("<div/>");
49 this.prompt_overlay = $("<div/>");
49 this.prompt_overlay = $("<div/>");
50 this.wrapper.append(this.prompt_overlay);
50 this.wrapper.append(this.prompt_overlay);
51 this.wrapper.append(this.element);
51 this.wrapper.append(this.element);
52 this.wrapper.append(this.collapse_button);
52 this.wrapper.append(this.collapse_button);
53 };
53 };
54
54
55
55
56 OutputArea.prototype.style = function () {
56 OutputArea.prototype.style = function () {
57 this.collapse_button.hide();
57 this.collapse_button.hide();
58 this.prompt_overlay.hide();
58 this.prompt_overlay.hide();
59
59
60 this.wrapper.addClass('output_wrapper');
60 this.wrapper.addClass('output_wrapper');
61 this.element.addClass('output');
61 this.element.addClass('output');
62
62
63 this.collapse_button.addClass("btn btn-default output_collapsed");
63 this.collapse_button.addClass("btn btn-default output_collapsed");
64 this.collapse_button.attr('title', 'click to expand output');
64 this.collapse_button.attr('title', 'click to expand output');
65 this.collapse_button.text('. . .');
65 this.collapse_button.text('. . .');
66
66
67 this.prompt_overlay.addClass('out_prompt_overlay prompt');
67 this.prompt_overlay.addClass('out_prompt_overlay prompt');
68 this.prompt_overlay.attr('title', 'click to expand output; double click to hide output');
68 this.prompt_overlay.attr('title', 'click to expand output; double click to hide output');
69
69
70 this.collapse();
70 this.collapse();
71 };
71 };
72
72
73 /**
73 /**
74 * Should the OutputArea scroll?
74 * Should the OutputArea scroll?
75 * Returns whether the height (in lines) exceeds a threshold.
75 * Returns whether the height (in lines) exceeds a threshold.
76 *
76 *
77 * @private
77 * @private
78 * @method _should_scroll
78 * @method _should_scroll
79 * @param [lines=100]{Integer}
79 * @param [lines=100]{Integer}
80 * @return {Bool}
80 * @return {Bool}
81 *
81 *
82 */
82 */
83 OutputArea.prototype._should_scroll = function (lines) {
83 OutputArea.prototype._should_scroll = function (lines) {
84 if (lines <=0 ){ return }
84 if (lines <=0 ){ return }
85 if (!lines) {
85 if (!lines) {
86 lines = 100;
86 lines = 100;
87 }
87 }
88 // line-height from http://stackoverflow.com/questions/1185151
88 // line-height from http://stackoverflow.com/questions/1185151
89 var fontSize = this.element.css('font-size');
89 var fontSize = this.element.css('font-size');
90 var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
90 var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
91
91
92 return (this.element.height() > lines * lineHeight);
92 return (this.element.height() > lines * lineHeight);
93 };
93 };
94
94
95
95
96 OutputArea.prototype.bind_events = function () {
96 OutputArea.prototype.bind_events = function () {
97 var that = this;
97 var that = this;
98 this.prompt_overlay.dblclick(function () { that.toggle_output(); });
98 this.prompt_overlay.dblclick(function () { that.toggle_output(); });
99 this.prompt_overlay.click(function () { that.toggle_scroll(); });
99 this.prompt_overlay.click(function () { that.toggle_scroll(); });
100
100
101 this.element.resize(function () {
101 this.element.resize(function () {
102 // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled
102 // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled
103 if ( utils.browser[0] === "Firefox" ) {
103 if ( utils.browser[0] === "Firefox" ) {
104 return;
104 return;
105 }
105 }
106 // maybe scroll output,
106 // maybe scroll output,
107 // if it's grown large enough and hasn't already been scrolled.
107 // if it's grown large enough and hasn't already been scrolled.
108 if ( !that.scrolled && that._should_scroll(OutputArea.auto_scroll_threshold)) {
108 if ( !that.scrolled && that._should_scroll(OutputArea.auto_scroll_threshold)) {
109 that.scroll_area();
109 that.scroll_area();
110 }
110 }
111 });
111 });
112 this.collapse_button.click(function () {
112 this.collapse_button.click(function () {
113 that.expand();
113 that.expand();
114 });
114 });
115 };
115 };
116
116
117
117
118 OutputArea.prototype.collapse = function () {
118 OutputArea.prototype.collapse = function () {
119 if (!this.collapsed) {
119 if (!this.collapsed) {
120 this.element.hide();
120 this.element.hide();
121 this.prompt_overlay.hide();
121 this.prompt_overlay.hide();
122 if (this.element.html()){
122 if (this.element.html()){
123 this.collapse_button.show();
123 this.collapse_button.show();
124 }
124 }
125 this.collapsed = true;
125 this.collapsed = true;
126 }
126 }
127 };
127 };
128
128
129
129
130 OutputArea.prototype.expand = function () {
130 OutputArea.prototype.expand = function () {
131 if (this.collapsed) {
131 if (this.collapsed) {
132 this.collapse_button.hide();
132 this.collapse_button.hide();
133 this.element.show();
133 this.element.show();
134 this.prompt_overlay.show();
134 this.prompt_overlay.show();
135 this.collapsed = false;
135 this.collapsed = false;
136 }
136 }
137 };
137 };
138
138
139
139
140 OutputArea.prototype.toggle_output = function () {
140 OutputArea.prototype.toggle_output = function () {
141 if (this.collapsed) {
141 if (this.collapsed) {
142 this.expand();
142 this.expand();
143 } else {
143 } else {
144 this.collapse();
144 this.collapse();
145 }
145 }
146 };
146 };
147
147
148
148
149 OutputArea.prototype.scroll_area = function () {
149 OutputArea.prototype.scroll_area = function () {
150 this.element.addClass('output_scroll');
150 this.element.addClass('output_scroll');
151 this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide');
151 this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide');
152 this.scrolled = true;
152 this.scrolled = true;
153 };
153 };
154
154
155
155
156 OutputArea.prototype.unscroll_area = function () {
156 OutputArea.prototype.unscroll_area = function () {
157 this.element.removeClass('output_scroll');
157 this.element.removeClass('output_scroll');
158 this.prompt_overlay.attr('title', 'click to scroll output; double click to hide');
158 this.prompt_overlay.attr('title', 'click to scroll output; double click to hide');
159 this.scrolled = false;
159 this.scrolled = false;
160 };
160 };
161
161
162 /**
162 /**
163 *
163 *
164 * Scroll OutputArea if height supperior than a threshold (in lines).
164 * Scroll OutputArea if height supperior than a threshold (in lines).
165 *
165 *
166 * Threshold is a maximum number of lines. If unspecified, defaults to
166 * Threshold is a maximum number of lines. If unspecified, defaults to
167 * OutputArea.minimum_scroll_threshold.
167 * OutputArea.minimum_scroll_threshold.
168 *
168 *
169 * Negative threshold will prevent the OutputArea from ever scrolling.
169 * Negative threshold will prevent the OutputArea from ever scrolling.
170 *
170 *
171 * @method scroll_if_long
171 * @method scroll_if_long
172 *
172 *
173 * @param [lines=20]{Number} Default to 20 if not set,
173 * @param [lines=20]{Number} Default to 20 if not set,
174 * behavior undefined for value of `0`.
174 * behavior undefined for value of `0`.
175 *
175 *
176 **/
176 **/
177 OutputArea.prototype.scroll_if_long = function (lines) {
177 OutputArea.prototype.scroll_if_long = function (lines) {
178 var n = lines | OutputArea.minimum_scroll_threshold;
178 var n = lines | OutputArea.minimum_scroll_threshold;
179 if(n <= 0){
179 if(n <= 0){
180 return
180 return
181 }
181 }
182
182
183 if (this._should_scroll(n)) {
183 if (this._should_scroll(n)) {
184 // only allow scrolling long-enough output
184 // only allow scrolling long-enough output
185 this.scroll_area();
185 this.scroll_area();
186 }
186 }
187 };
187 };
188
188
189
189
190 OutputArea.prototype.toggle_scroll = function () {
190 OutputArea.prototype.toggle_scroll = function () {
191 if (this.scrolled) {
191 if (this.scrolled) {
192 this.unscroll_area();
192 this.unscroll_area();
193 } else {
193 } else {
194 // only allow scrolling long-enough output
194 // only allow scrolling long-enough output
195 this.scroll_if_long();
195 this.scroll_if_long();
196 }
196 }
197 };
197 };
198
198
199
199
200 // typeset with MathJax if MathJax is available
200 // typeset with MathJax if MathJax is available
201 OutputArea.prototype.typeset = function () {
201 OutputArea.prototype.typeset = function () {
202 if (window.MathJax){
202 if (window.MathJax){
203 MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
203 MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
204 }
204 }
205 };
205 };
206
206
207
207
208 OutputArea.prototype.handle_output = function (msg) {
208 OutputArea.prototype.handle_output = function (msg) {
209 var json = {};
209 var json = {};
210 var msg_type = json.output_type = msg.header.msg_type;
210 var msg_type = json.output_type = msg.header.msg_type;
211 var content = msg.content;
211 var content = msg.content;
212 if (msg_type === "stream") {
212 if (msg_type === "stream") {
213 json.text = content.text;
213 json.text = content.text;
214 json.stream = content.name;
214 json.name = content.name;
215 } else if (msg_type === "display_data") {
215 } else if (msg_type === "display_data") {
216 json = content.data;
216 json = content.data;
217 json.output_type = msg_type;
217 json.output_type = msg_type;
218 json.metadata = content.metadata;
218 json.metadata = content.metadata;
219 } else if (msg_type === "execute_result") {
219 } else if (msg_type === "execute_result") {
220 json = content.data;
220 json = content.data;
221 json.output_type = msg_type;
221 json.output_type = msg_type;
222 json.metadata = content.metadata;
222 json.metadata = content.metadata;
223 json.prompt_number = content.execution_count;
223 json.prompt_number = content.execution_count;
224 } else if (msg_type === "error") {
224 } else if (msg_type === "error") {
225 json.ename = content.ename;
225 json.ename = content.ename;
226 json.evalue = content.evalue;
226 json.evalue = content.evalue;
227 json.traceback = content.traceback;
227 json.traceback = content.traceback;
228 } else {
228 } else {
229 console.log("unhandled output message", msg);
229 console.log("unhandled output message", msg);
230 return;
230 return;
231 }
231 }
232 this.append_output(json);
232 this.append_output(json);
233 };
233 };
234
234
235
235
236 OutputArea.prototype.rename_keys = function (data, key_map) {
236 OutputArea.prototype.rename_keys = function (data, key_map) {
237 // TODO: This is now unused, should it be removed?
237 var remapped = {};
238 var remapped = {};
238 for (var key in data) {
239 for (var key in data) {
239 var new_key = key_map[key] || key;
240 var new_key = key_map[key] || key;
240 remapped[new_key] = data[key];
241 remapped[new_key] = data[key];
241 }
242 }
242 return remapped;
243 return remapped;
243 };
244 };
244
245
245
246
246 OutputArea.output_types = [
247 OutputArea.output_types = [
247 'application/javascript',
248 'application/javascript',
248 'text/html',
249 'text/html',
249 'text/markdown',
250 'text/markdown',
250 'text/latex',
251 'text/latex',
251 'image/svg+xml',
252 'image/svg+xml',
252 'image/png',
253 'image/png',
253 'image/jpeg',
254 'image/jpeg',
254 'application/pdf',
255 'application/pdf',
255 'text/plain'
256 'text/plain'
256 ];
257 ];
257
258
258 OutputArea.prototype.validate_output = function (json) {
259 OutputArea.prototype.validate_output = function (json) {
259 // scrub invalid outputs
260 // scrub invalid outputs
260 // TODO: right now everything is a string, but JSON really shouldn't be.
261 // TODO: right now everything is a string, but JSON really shouldn't be.
261 // nbformat 4 will fix that.
262 // nbformat 4 will fix that.
262 $.map(OutputArea.output_types, function(key){
263 $.map(OutputArea.output_types, function(key){
263 if (json[key] !== undefined && typeof json[key] !== 'string') {
264 if (key !== 'application/json' &&
265 json[key] !== undefined &&
266 typeof json[key] !== 'string'
267 ) {
264 console.log("Invalid type for " + key, json[key]);
268 console.log("Invalid type for " + key, json[key]);
265 delete json[key];
269 delete json[key];
266 }
270 }
267 });
271 });
268 return json;
272 return json;
269 };
273 };
270
274
271 OutputArea.prototype.append_output = function (json) {
275 OutputArea.prototype.append_output = function (json) {
272 this.expand();
276 this.expand();
273
277
274 // validate output data types
278 // validate output data types
275 json = this.validate_output(json);
279 json = this.validate_output(json);
276
280
277 // Clear the output if clear is queued.
281 // Clear the output if clear is queued.
278 var needs_height_reset = false;
282 var needs_height_reset = false;
279 if (this.clear_queued) {
283 if (this.clear_queued) {
280 this.clear_output(false);
284 this.clear_output(false);
281 needs_height_reset = true;
285 needs_height_reset = true;
282 }
286 }
283
287
284 var record_output = true;
288 var record_output = true;
285
289
286 if (json.output_type === 'execute_result') {
290 if (json.output_type === 'execute_result') {
287 this.append_execute_result(json);
291 this.append_execute_result(json);
288 } else if (json.output_type === 'error') {
292 } else if (json.output_type === 'error') {
289 this.append_error(json);
293 this.append_error(json);
290 } else if (json.output_type === 'stream') {
294 } else if (json.output_type === 'stream') {
291 // append_stream might have merged the output with earlier stream output
295 // append_stream might have merged the output with earlier stream output
292 record_output = this.append_stream(json);
296 record_output = this.append_stream(json);
293 }
297 }
294
298
295 // We must release the animation fixed height in a callback since Gecko
299 // We must release the animation fixed height in a callback since Gecko
296 // (FireFox) doesn't render the image immediately as the data is
300 // (FireFox) doesn't render the image immediately as the data is
297 // available.
301 // available.
298 var that = this;
302 var that = this;
299 var handle_appended = function ($el) {
303 var handle_appended = function ($el) {
300 // Only reset the height to automatic if the height is currently
304 // Only reset the height to automatic if the height is currently
301 // fixed (done by wait=True flag on clear_output).
305 // fixed (done by wait=True flag on clear_output).
302 if (needs_height_reset) {
306 if (needs_height_reset) {
303 that.element.height('');
307 that.element.height('');
304 }
308 }
305 that.element.trigger('resize');
309 that.element.trigger('resize');
306 };
310 };
307 if (json.output_type === 'display_data') {
311 if (json.output_type === 'display_data') {
308 this.append_display_data(json, handle_appended);
312 this.append_display_data(json, handle_appended);
309 } else {
313 } else {
310 handle_appended();
314 handle_appended();
311 }
315 }
312
316
313 if (record_output) {
317 if (record_output) {
314 this.outputs.push(json);
318 this.outputs.push(json);
315 }
319 }
316 };
320 };
317
321
318
322
319 OutputArea.prototype.create_output_area = function () {
323 OutputArea.prototype.create_output_area = function () {
320 var oa = $("<div/>").addClass("output_area");
324 var oa = $("<div/>").addClass("output_area");
321 if (this.prompt_area) {
325 if (this.prompt_area) {
322 oa.append($('<div/>').addClass('prompt'));
326 oa.append($('<div/>').addClass('prompt'));
323 }
327 }
324 return oa;
328 return oa;
325 };
329 };
326
330
327
331
328 function _get_metadata_key(metadata, key, mime) {
332 function _get_metadata_key(metadata, key, mime) {
329 var mime_md = metadata[mime];
333 var mime_md = metadata[mime];
330 // mime-specific higher priority
334 // mime-specific higher priority
331 if (mime_md && mime_md[key] !== undefined) {
335 if (mime_md && mime_md[key] !== undefined) {
332 return mime_md[key];
336 return mime_md[key];
333 }
337 }
334 // fallback on global
338 // fallback on global
335 return metadata[key];
339 return metadata[key];
336 }
340 }
337
341
338 OutputArea.prototype.create_output_subarea = function(md, classes, mime) {
342 OutputArea.prototype.create_output_subarea = function(md, classes, mime) {
339 var subarea = $('<div/>').addClass('output_subarea').addClass(classes);
343 var subarea = $('<div/>').addClass('output_subarea').addClass(classes);
340 if (_get_metadata_key(md, 'isolated', mime)) {
344 if (_get_metadata_key(md, 'isolated', mime)) {
341 // Create an iframe to isolate the subarea from the rest of the
345 // Create an iframe to isolate the subarea from the rest of the
342 // document
346 // document
343 var iframe = $('<iframe/>').addClass('box-flex1');
347 var iframe = $('<iframe/>').addClass('box-flex1');
344 iframe.css({'height':1, 'width':'100%', 'display':'block'});
348 iframe.css({'height':1, 'width':'100%', 'display':'block'});
345 iframe.attr('frameborder', 0);
349 iframe.attr('frameborder', 0);
346 iframe.attr('scrolling', 'auto');
350 iframe.attr('scrolling', 'auto');
347
351
348 // Once the iframe is loaded, the subarea is dynamically inserted
352 // Once the iframe is loaded, the subarea is dynamically inserted
349 iframe.on('load', function() {
353 iframe.on('load', function() {
350 // Workaround needed by Firefox, to properly render svg inside
354 // Workaround needed by Firefox, to properly render svg inside
351 // iframes, see http://stackoverflow.com/questions/10177190/
355 // iframes, see http://stackoverflow.com/questions/10177190/
352 // svg-dynamically-added-to-iframe-does-not-render-correctly
356 // svg-dynamically-added-to-iframe-does-not-render-correctly
353 this.contentDocument.open();
357 this.contentDocument.open();
354
358
355 // Insert the subarea into the iframe
359 // Insert the subarea into the iframe
356 // We must directly write the html. When using Jquery's append
360 // We must directly write the html. When using Jquery's append
357 // method, javascript is evaluated in the parent document and
361 // method, javascript is evaluated in the parent document and
358 // not in the iframe document. At this point, subarea doesn't
362 // not in the iframe document. At this point, subarea doesn't
359 // contain any user content.
363 // contain any user content.
360 this.contentDocument.write(subarea.html());
364 this.contentDocument.write(subarea.html());
361
365
362 this.contentDocument.close();
366 this.contentDocument.close();
363
367
364 var body = this.contentDocument.body;
368 var body = this.contentDocument.body;
365 // Adjust the iframe height automatically
369 // Adjust the iframe height automatically
366 iframe.height(body.scrollHeight + 'px');
370 iframe.height(body.scrollHeight + 'px');
367 });
371 });
368
372
369 // Elements should be appended to the inner subarea and not to the
373 // Elements should be appended to the inner subarea and not to the
370 // iframe
374 // iframe
371 iframe.append = function(that) {
375 iframe.append = function(that) {
372 subarea.append(that);
376 subarea.append(that);
373 };
377 };
374
378
375 return iframe;
379 return iframe;
376 } else {
380 } else {
377 return subarea;
381 return subarea;
378 }
382 }
379 }
383 }
380
384
381
385
382 OutputArea.prototype._append_javascript_error = function (err, element) {
386 OutputArea.prototype._append_javascript_error = function (err, element) {
383 // display a message when a javascript error occurs in display output
387 // display a message when a javascript error occurs in display output
384 var msg = "Javascript error adding output!"
388 var msg = "Javascript error adding output!"
385 if ( element === undefined ) return;
389 if ( element === undefined ) return;
386 element
390 element
387 .append($('<div/>').text(msg).addClass('js-error'))
391 .append($('<div/>').text(msg).addClass('js-error'))
388 .append($('<div/>').text(err.toString()).addClass('js-error'))
392 .append($('<div/>').text(err.toString()).addClass('js-error'))
389 .append($('<div/>').text('See your browser Javascript console for more details.').addClass('js-error'));
393 .append($('<div/>').text('See your browser Javascript console for more details.').addClass('js-error'));
390 };
394 };
391
395
392 OutputArea.prototype._safe_append = function (toinsert) {
396 OutputArea.prototype._safe_append = function (toinsert) {
393 // safely append an item to the document
397 // safely append an item to the document
394 // this is an object created by user code,
398 // this is an object created by user code,
395 // and may have errors, which should not be raised
399 // and may have errors, which should not be raised
396 // under any circumstances.
400 // under any circumstances.
397 try {
401 try {
398 this.element.append(toinsert);
402 this.element.append(toinsert);
399 } catch(err) {
403 } catch(err) {
400 console.log(err);
404 console.log(err);
401 // Create an actual output_area and output_subarea, which creates
405 // Create an actual output_area and output_subarea, which creates
402 // the prompt area and the proper indentation.
406 // the prompt area and the proper indentation.
403 var toinsert = this.create_output_area();
407 var toinsert = this.create_output_area();
404 var subarea = $('<div/>').addClass('output_subarea');
408 var subarea = $('<div/>').addClass('output_subarea');
405 toinsert.append(subarea);
409 toinsert.append(subarea);
406 this._append_javascript_error(err, subarea);
410 this._append_javascript_error(err, subarea);
407 this.element.append(toinsert);
411 this.element.append(toinsert);
408 }
412 }
409 };
413 };
410
414
411
415
412 OutputArea.prototype.append_execute_result = function (json) {
416 OutputArea.prototype.append_execute_result = function (json) {
413 var n = json.prompt_number || ' ';
417 var n = json.prompt_number || ' ';
414 var toinsert = this.create_output_area();
418 var toinsert = this.create_output_area();
415 if (this.prompt_area) {
419 if (this.prompt_area) {
416 toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:');
420 toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:');
417 }
421 }
418 var inserted = this.append_mime_type(json, toinsert);
422 var inserted = this.append_mime_type(json, toinsert);
419 if (inserted) {
423 if (inserted) {
420 inserted.addClass('output_result');
424 inserted.addClass('output_result');
421 }
425 }
422 this._safe_append(toinsert);
426 this._safe_append(toinsert);
423 // If we just output latex, typeset it.
427 // If we just output latex, typeset it.
424 if ((json['text/latex'] !== undefined) ||
428 if ((json['text/latex'] !== undefined) ||
425 (json['text/html'] !== undefined) ||
429 (json['text/html'] !== undefined) ||
426 (json['text/markdown'] !== undefined)) {
430 (json['text/markdown'] !== undefined)) {
427 this.typeset();
431 this.typeset();
428 }
432 }
429 };
433 };
430
434
431
435
432 OutputArea.prototype.append_error = function (json) {
436 OutputArea.prototype.append_error = function (json) {
433 var tb = json.traceback;
437 var tb = json.traceback;
434 if (tb !== undefined && tb.length > 0) {
438 if (tb !== undefined && tb.length > 0) {
435 var s = '';
439 var s = '';
436 var len = tb.length;
440 var len = tb.length;
437 for (var i=0; i<len; i++) {
441 for (var i=0; i<len; i++) {
438 s = s + tb[i] + '\n';
442 s = s + tb[i] + '\n';
439 }
443 }
440 s = s + '\n';
444 s = s + '\n';
441 var toinsert = this.create_output_area();
445 var toinsert = this.create_output_area();
442 var append_text = OutputArea.append_map['text/plain'];
446 var append_text = OutputArea.append_map['text/plain'];
443 if (append_text) {
447 if (append_text) {
444 append_text.apply(this, [s, {}, toinsert]).addClass('output_error');
448 append_text.apply(this, [s, {}, toinsert]).addClass('output_error');
445 }
449 }
446 this._safe_append(toinsert);
450 this._safe_append(toinsert);
447 }
451 }
448 };
452 };
449
453
450
454
451 OutputArea.prototype.append_stream = function (json) {
455 OutputArea.prototype.append_stream = function (json) {
452 // temporary fix: if stream undefined (json file written prior to this patch),
456 var text = json.data;
453 // default to most likely stdout:
457 var subclass = "output_"+json.name;
454 if (json.stream === undefined){
455 json.stream = 'stdout';
456 }
457 var text = json.text;
458 var subclass = "output_"+json.stream;
459 if (this.outputs.length > 0){
458 if (this.outputs.length > 0){
460 // have at least one output to consider
459 // have at least one output to consider
461 var last = this.outputs[this.outputs.length-1];
460 var last = this.outputs[this.outputs.length-1];
462 if (last.output_type == 'stream' && json.stream == last.stream){
461 if (last.output_type == 'stream' && json.name == last.name){
463 // latest output was in the same stream,
462 // latest output was in the same stream,
464 // so append directly into its pre tag
463 // so append directly into its pre tag
465 // escape ANSI & HTML specials:
464 // escape ANSI & HTML specials:
466 last.text = utils.fixCarriageReturn(last.text + json.text);
465 last.data = utils.fixCarriageReturn(last.data + json.data);
467 var pre = this.element.find('div.'+subclass).last().find('pre');
466 var pre = this.element.find('div.'+subclass).last().find('pre');
468 var html = utils.fixConsole(last.text);
467 var html = utils.fixConsole(last.data);
469 // The only user content injected with this HTML call is
468 // The only user content injected with this HTML call is
470 // escaped by the fixConsole() method.
469 // escaped by the fixConsole() method.
471 pre.html(html);
470 pre.html(html);
472 // return false signals that we merged this output with the previous one,
471 // return false signals that we merged this output with the previous one,
473 // and the new output shouldn't be recorded.
472 // and the new output shouldn't be recorded.
474 return false;
473 return false;
475 }
474 }
476 }
475 }
477
476
478 if (!text.replace("\r", "")) {
477 if (!text.replace("\r", "")) {
479 // text is nothing (empty string, \r, etc.)
478 // text is nothing (empty string, \r, etc.)
480 // so don't append any elements, which might add undesirable space
479 // so don't append any elements, which might add undesirable space
481 // return true to indicate the output should be recorded.
480 // return true to indicate the output should be recorded.
482 return true;
481 return true;
483 }
482 }
484
483
485 // If we got here, attach a new div
484 // If we got here, attach a new div
486 var toinsert = this.create_output_area();
485 var toinsert = this.create_output_area();
487 var append_text = OutputArea.append_map['text/plain'];
486 var append_text = OutputArea.append_map['text/plain'];
488 if (append_text) {
487 if (append_text) {
489 append_text.apply(this, [text, {}, toinsert]).addClass("output_stream " + subclass);
488 append_text.apply(this, [text, {}, toinsert]).addClass("output_stream " + subclass);
490 }
489 }
491 this._safe_append(toinsert);
490 this._safe_append(toinsert);
492 return true;
491 return true;
493 };
492 };
494
493
495
494
496 OutputArea.prototype.append_display_data = function (json, handle_inserted) {
495 OutputArea.prototype.append_display_data = function (json, handle_inserted) {
497 var toinsert = this.create_output_area();
496 var toinsert = this.create_output_area();
498 if (this.append_mime_type(json, toinsert, handle_inserted)) {
497 if (this.append_mime_type(json, toinsert, handle_inserted)) {
499 this._safe_append(toinsert);
498 this._safe_append(toinsert);
500 // If we just output latex, typeset it.
499 // If we just output latex, typeset it.
501 if ((json['text/latex'] !== undefined) ||
500 if ((json['text/latex'] !== undefined) ||
502 (json['text/html'] !== undefined) ||
501 (json['text/html'] !== undefined) ||
503 (json['text/markdown'] !== undefined)) {
502 (json['text/markdown'] !== undefined)) {
504 this.typeset();
503 this.typeset();
505 }
504 }
506 }
505 }
507 };
506 };
508
507
509
508
510 OutputArea.safe_outputs = {
509 OutputArea.safe_outputs = {
511 'text/plain' : true,
510 'text/plain' : true,
512 'text/latex' : true,
511 'text/latex' : true,
513 'image/png' : true,
512 'image/png' : true,
514 'image/jpeg' : true
513 'image/jpeg' : true
515 };
514 };
516
515
517 OutputArea.prototype.append_mime_type = function (json, element, handle_inserted) {
516 OutputArea.prototype.append_mime_type = function (json, element, handle_inserted) {
518 for (var i=0; i < OutputArea.display_order.length; i++) {
517 for (var i=0; i < OutputArea.display_order.length; i++) {
519 var type = OutputArea.display_order[i];
518 var type = OutputArea.display_order[i];
520 var append = OutputArea.append_map[type];
519 var append = OutputArea.append_map[type];
521 if ((json[type] !== undefined) && append) {
520 if ((json[type] !== undefined) && append) {
522 var value = json[type];
521 var value = json[type];
523 if (!this.trusted && !OutputArea.safe_outputs[type]) {
522 if (!this.trusted && !OutputArea.safe_outputs[type]) {
524 // not trusted, sanitize HTML
523 // not trusted, sanitize HTML
525 if (type==='text/html' || type==='text/svg') {
524 if (type==='text/html' || type==='text/svg') {
526 value = security.sanitize_html(value);
525 value = security.sanitize_html(value);
527 } else {
526 } else {
528 // don't display if we don't know how to sanitize it
527 // don't display if we don't know how to sanitize it
529 console.log("Ignoring untrusted " + type + " output.");
528 console.log("Ignoring untrusted " + type + " output.");
530 continue;
529 continue;
531 }
530 }
532 }
531 }
533 var md = json.metadata || {};
532 var md = json.metadata || {};
534 var toinsert = append.apply(this, [value, md, element, handle_inserted]);
533 var toinsert = append.apply(this, [value, md, element, handle_inserted]);
535 // Since only the png and jpeg mime types call the inserted
534 // Since only the png and jpeg mime types call the inserted
536 // callback, if the mime type is something other we must call the
535 // callback, if the mime type is something other we must call the
537 // inserted callback only when the element is actually inserted
536 // inserted callback only when the element is actually inserted
538 // into the DOM. Use a timeout of 0 to do this.
537 // into the DOM. Use a timeout of 0 to do this.
539 if (['image/png', 'image/jpeg'].indexOf(type) < 0 && handle_inserted !== undefined) {
538 if (['image/png', 'image/jpeg'].indexOf(type) < 0 && handle_inserted !== undefined) {
540 setTimeout(handle_inserted, 0);
539 setTimeout(handle_inserted, 0);
541 }
540 }
542 this.events.trigger('output_appended.OutputArea', [type, value, md, toinsert]);
541 this.events.trigger('output_appended.OutputArea', [type, value, md, toinsert]);
543 return toinsert;
542 return toinsert;
544 }
543 }
545 }
544 }
546 return null;
545 return null;
547 };
546 };
548
547
549
548
550 var append_html = function (html, md, element) {
549 var append_html = function (html, md, element) {
551 var type = 'text/html';
550 var type = 'text/html';
552 var toinsert = this.create_output_subarea(md, "output_html rendered_html", type);
551 var toinsert = this.create_output_subarea(md, "output_html rendered_html", type);
553 this.keyboard_manager.register_events(toinsert);
552 this.keyboard_manager.register_events(toinsert);
554 toinsert.append(html);
553 toinsert.append(html);
555 element.append(toinsert);
554 element.append(toinsert);
556 return toinsert;
555 return toinsert;
557 };
556 };
558
557
559
558
560 var append_markdown = function(markdown, md, element) {
559 var append_markdown = function(markdown, md, element) {
561 var type = 'text/markdown';
560 var type = 'text/markdown';
562 var toinsert = this.create_output_subarea(md, "output_markdown", type);
561 var toinsert = this.create_output_subarea(md, "output_markdown", type);
563 var text_and_math = mathjaxutils.remove_math(markdown);
562 var text_and_math = mathjaxutils.remove_math(markdown);
564 var text = text_and_math[0];
563 var text = text_and_math[0];
565 var math = text_and_math[1];
564 var math = text_and_math[1];
566 var html = marked.parser(marked.lexer(text));
565 var html = marked.parser(marked.lexer(text));
567 html = mathjaxutils.replace_math(html, math);
566 html = mathjaxutils.replace_math(html, math);
568 toinsert.append(html);
567 toinsert.append(html);
569 element.append(toinsert);
568 element.append(toinsert);
570 return toinsert;
569 return toinsert;
571 };
570 };
572
571
573
572
574 var append_javascript = function (js, md, element) {
573 var append_javascript = function (js, md, element) {
575 // We just eval the JS code, element appears in the local scope.
574 // We just eval the JS code, element appears in the local scope.
576 var type = 'application/javascript';
575 var type = 'application/javascript';
577 var toinsert = this.create_output_subarea(md, "output_javascript", type);
576 var toinsert = this.create_output_subarea(md, "output_javascript", type);
578 this.keyboard_manager.register_events(toinsert);
577 this.keyboard_manager.register_events(toinsert);
579 element.append(toinsert);
578 element.append(toinsert);
580
579
581 // Fix for ipython/issues/5293, make sure `element` is the area which
580 // Fix for ipython/issues/5293, make sure `element` is the area which
582 // output can be inserted into at the time of JS execution.
581 // output can be inserted into at the time of JS execution.
583 element = toinsert;
582 element = toinsert;
584 try {
583 try {
585 eval(js);
584 eval(js);
586 } catch(err) {
585 } catch(err) {
587 console.log(err);
586 console.log(err);
588 this._append_javascript_error(err, toinsert);
587 this._append_javascript_error(err, toinsert);
589 }
588 }
590 return toinsert;
589 return toinsert;
591 };
590 };
592
591
593
592
594 var append_text = function (data, md, element) {
593 var append_text = function (data, md, element) {
595 var type = 'text/plain';
594 var type = 'text/plain';
596 var toinsert = this.create_output_subarea(md, "output_text", type);
595 var toinsert = this.create_output_subarea(md, "output_text", type);
597 // escape ANSI & HTML specials in plaintext:
596 // escape ANSI & HTML specials in plaintext:
598 data = utils.fixConsole(data);
597 data = utils.fixConsole(data);
599 data = utils.fixCarriageReturn(data);
598 data = utils.fixCarriageReturn(data);
600 data = utils.autoLinkUrls(data);
599 data = utils.autoLinkUrls(data);
601 // The only user content injected with this HTML call is
600 // The only user content injected with this HTML call is
602 // escaped by the fixConsole() method.
601 // escaped by the fixConsole() method.
603 toinsert.append($("<pre/>").html(data));
602 toinsert.append($("<pre/>").html(data));
604 element.append(toinsert);
603 element.append(toinsert);
605 return toinsert;
604 return toinsert;
606 };
605 };
607
606
608
607
609 var append_svg = function (svg_html, md, element) {
608 var append_svg = function (svg_html, md, element) {
610 var type = 'image/svg+xml';
609 var type = 'image/svg+xml';
611 var toinsert = this.create_output_subarea(md, "output_svg", type);
610 var toinsert = this.create_output_subarea(md, "output_svg", type);
612
611
613 // Get the svg element from within the HTML.
612 // Get the svg element from within the HTML.
614 var svg = $('<div />').html(svg_html).find('svg');
613 var svg = $('<div />').html(svg_html).find('svg');
615 var svg_area = $('<div />');
614 var svg_area = $('<div />');
616 var width = svg.attr('width');
615 var width = svg.attr('width');
617 var height = svg.attr('height');
616 var height = svg.attr('height');
618 svg
617 svg
619 .width('100%')
618 .width('100%')
620 .height('100%');
619 .height('100%');
621 svg_area
620 svg_area
622 .width(width)
621 .width(width)
623 .height(height);
622 .height(height);
624
623
625 // The jQuery resize handlers don't seem to work on the svg element.
624 // The jQuery resize handlers don't seem to work on the svg element.
626 // When the svg renders completely, measure it's size and set the parent
625 // When the svg renders completely, measure it's size and set the parent
627 // div to that size. Then set the svg to 100% the size of the parent
626 // div to that size. Then set the svg to 100% the size of the parent
628 // div and make the parent div resizable.
627 // div and make the parent div resizable.
629 this._dblclick_to_reset_size(svg_area, true, false);
628 this._dblclick_to_reset_size(svg_area, true, false);
630
629
631 svg_area.append(svg);
630 svg_area.append(svg);
632 toinsert.append(svg_area);
631 toinsert.append(svg_area);
633 element.append(toinsert);
632 element.append(toinsert);
634
633
635 return toinsert;
634 return toinsert;
636 };
635 };
637
636
638 OutputArea.prototype._dblclick_to_reset_size = function (img, immediately, resize_parent) {
637 OutputArea.prototype._dblclick_to_reset_size = function (img, immediately, resize_parent) {
639 // Add a resize handler to an element
638 // Add a resize handler to an element
640 //
639 //
641 // img: jQuery element
640 // img: jQuery element
642 // immediately: bool=False
641 // immediately: bool=False
643 // Wait for the element to load before creating the handle.
642 // Wait for the element to load before creating the handle.
644 // resize_parent: bool=True
643 // resize_parent: bool=True
645 // Should the parent of the element be resized when the element is
644 // Should the parent of the element be resized when the element is
646 // reset (by double click).
645 // reset (by double click).
647 var callback = function (){
646 var callback = function (){
648 var h0 = img.height();
647 var h0 = img.height();
649 var w0 = img.width();
648 var w0 = img.width();
650 if (!(h0 && w0)) {
649 if (!(h0 && w0)) {
651 // zero size, don't make it resizable
650 // zero size, don't make it resizable
652 return;
651 return;
653 }
652 }
654 img.resizable({
653 img.resizable({
655 aspectRatio: true,
654 aspectRatio: true,
656 autoHide: true
655 autoHide: true
657 });
656 });
658 img.dblclick(function () {
657 img.dblclick(function () {
659 // resize wrapper & image together for some reason:
658 // resize wrapper & image together for some reason:
660 img.height(h0);
659 img.height(h0);
661 img.width(w0);
660 img.width(w0);
662 if (resize_parent === undefined || resize_parent) {
661 if (resize_parent === undefined || resize_parent) {
663 img.parent().height(h0);
662 img.parent().height(h0);
664 img.parent().width(w0);
663 img.parent().width(w0);
665 }
664 }
666 });
665 });
667 };
666 };
668
667
669 if (immediately) {
668 if (immediately) {
670 callback();
669 callback();
671 } else {
670 } else {
672 img.on("load", callback);
671 img.on("load", callback);
673 }
672 }
674 };
673 };
675
674
676 var set_width_height = function (img, md, mime) {
675 var set_width_height = function (img, md, mime) {
677 // set width and height of an img element from metadata
676 // set width and height of an img element from metadata
678 var height = _get_metadata_key(md, 'height', mime);
677 var height = _get_metadata_key(md, 'height', mime);
679 if (height !== undefined) img.attr('height', height);
678 if (height !== undefined) img.attr('height', height);
680 var width = _get_metadata_key(md, 'width', mime);
679 var width = _get_metadata_key(md, 'width', mime);
681 if (width !== undefined) img.attr('width', width);
680 if (width !== undefined) img.attr('width', width);
682 };
681 };
683
682
684 var append_png = function (png, md, element, handle_inserted) {
683 var append_png = function (png, md, element, handle_inserted) {
685 var type = 'image/png';
684 var type = 'image/png';
686 var toinsert = this.create_output_subarea(md, "output_png", type);
685 var toinsert = this.create_output_subarea(md, "output_png", type);
687 var img = $("<img/>");
686 var img = $("<img/>");
688 if (handle_inserted !== undefined) {
687 if (handle_inserted !== undefined) {
689 img.on('load', function(){
688 img.on('load', function(){
690 handle_inserted(img);
689 handle_inserted(img);
691 });
690 });
692 }
691 }
693 img[0].src = 'data:image/png;base64,'+ png;
692 img[0].src = 'data:image/png;base64,'+ png;
694 set_width_height(img, md, 'image/png');
693 set_width_height(img, md, 'image/png');
695 this._dblclick_to_reset_size(img);
694 this._dblclick_to_reset_size(img);
696 toinsert.append(img);
695 toinsert.append(img);
697 element.append(toinsert);
696 element.append(toinsert);
698 return toinsert;
697 return toinsert;
699 };
698 };
700
699
701
700
702 var append_jpeg = function (jpeg, md, element, handle_inserted) {
701 var append_jpeg = function (jpeg, md, element, handle_inserted) {
703 var type = 'image/jpeg';
702 var type = 'image/jpeg';
704 var toinsert = this.create_output_subarea(md, "output_jpeg", type);
703 var toinsert = this.create_output_subarea(md, "output_jpeg", type);
705 var img = $("<img/>");
704 var img = $("<img/>");
706 if (handle_inserted !== undefined) {
705 if (handle_inserted !== undefined) {
707 img.on('load', function(){
706 img.on('load', function(){
708 handle_inserted(img);
707 handle_inserted(img);
709 });
708 });
710 }
709 }
711 img[0].src = 'data:image/jpeg;base64,'+ jpeg;
710 img[0].src = 'data:image/jpeg;base64,'+ jpeg;
712 set_width_height(img, md, 'image/jpeg');
711 set_width_height(img, md, 'image/jpeg');
713 this._dblclick_to_reset_size(img);
712 this._dblclick_to_reset_size(img);
714 toinsert.append(img);
713 toinsert.append(img);
715 element.append(toinsert);
714 element.append(toinsert);
716 return toinsert;
715 return toinsert;
717 };
716 };
718
717
719
718
720 var append_pdf = function (pdf, md, element) {
719 var append_pdf = function (pdf, md, element) {
721 var type = 'application/pdf';
720 var type = 'application/pdf';
722 var toinsert = this.create_output_subarea(md, "output_pdf", type);
721 var toinsert = this.create_output_subarea(md, "output_pdf", type);
723 var a = $('<a/>').attr('href', 'data:application/pdf;base64,'+pdf);
722 var a = $('<a/>').attr('href', 'data:application/pdf;base64,'+pdf);
724 a.attr('target', '_blank');
723 a.attr('target', '_blank');
725 a.text('View PDF')
724 a.text('View PDF')
726 toinsert.append(a);
725 toinsert.append(a);
727 element.append(toinsert);
726 element.append(toinsert);
728 return toinsert;
727 return toinsert;
729 }
728 }
730
729
731 var append_latex = function (latex, md, element) {
730 var append_latex = function (latex, md, element) {
732 // This method cannot do the typesetting because the latex first has to
731 // This method cannot do the typesetting because the latex first has to
733 // be on the page.
732 // be on the page.
734 var type = 'text/latex';
733 var type = 'text/latex';
735 var toinsert = this.create_output_subarea(md, "output_latex", type);
734 var toinsert = this.create_output_subarea(md, "output_latex", type);
736 toinsert.append(latex);
735 toinsert.append(latex);
737 element.append(toinsert);
736 element.append(toinsert);
738 return toinsert;
737 return toinsert;
739 };
738 };
740
739
741
740
742 OutputArea.prototype.append_raw_input = function (msg) {
741 OutputArea.prototype.append_raw_input = function (msg) {
743 var that = this;
742 var that = this;
744 this.expand();
743 this.expand();
745 var content = msg.content;
744 var content = msg.content;
746 var area = this.create_output_area();
745 var area = this.create_output_area();
747
746
748 // disable any other raw_inputs, if they are left around
747 // disable any other raw_inputs, if they are left around
749 $("div.output_subarea.raw_input_container").remove();
748 $("div.output_subarea.raw_input_container").remove();
750
749
751 var input_type = content.password ? 'password' : 'text';
750 var input_type = content.password ? 'password' : 'text';
752
751
753 area.append(
752 area.append(
754 $("<div/>")
753 $("<div/>")
755 .addClass("box-flex1 output_subarea raw_input_container")
754 .addClass("box-flex1 output_subarea raw_input_container")
756 .append(
755 .append(
757 $("<span/>")
756 $("<span/>")
758 .addClass("raw_input_prompt")
757 .addClass("raw_input_prompt")
759 .text(content.prompt)
758 .text(content.prompt)
760 )
759 )
761 .append(
760 .append(
762 $("<input/>")
761 $("<input/>")
763 .addClass("raw_input")
762 .addClass("raw_input")
764 .attr('type', input_type)
763 .attr('type', input_type)
765 .attr("size", 47)
764 .attr("size", 47)
766 .keydown(function (event, ui) {
765 .keydown(function (event, ui) {
767 // make sure we submit on enter,
766 // make sure we submit on enter,
768 // and don't re-execute the *cell* on shift-enter
767 // and don't re-execute the *cell* on shift-enter
769 if (event.which === keyboard.keycodes.enter) {
768 if (event.which === keyboard.keycodes.enter) {
770 that._submit_raw_input();
769 that._submit_raw_input();
771 return false;
770 return false;
772 }
771 }
773 })
772 })
774 )
773 )
775 );
774 );
776
775
777 this.element.append(area);
776 this.element.append(area);
778 var raw_input = area.find('input.raw_input');
777 var raw_input = area.find('input.raw_input');
779 // Register events that enable/disable the keyboard manager while raw
778 // Register events that enable/disable the keyboard manager while raw
780 // input is focused.
779 // input is focused.
781 this.keyboard_manager.register_events(raw_input);
780 this.keyboard_manager.register_events(raw_input);
782 // Note, the following line used to read raw_input.focus().focus().
781 // Note, the following line used to read raw_input.focus().focus().
783 // This seemed to be needed otherwise only the cell would be focused.
782 // This seemed to be needed otherwise only the cell would be focused.
784 // But with the modal UI, this seems to work fine with one call to focus().
783 // But with the modal UI, this seems to work fine with one call to focus().
785 raw_input.focus();
784 raw_input.focus();
786 }
785 }
787
786
788 OutputArea.prototype._submit_raw_input = function (evt) {
787 OutputArea.prototype._submit_raw_input = function (evt) {
789 var container = this.element.find("div.raw_input_container");
788 var container = this.element.find("div.raw_input_container");
790 var theprompt = container.find("span.raw_input_prompt");
789 var theprompt = container.find("span.raw_input_prompt");
791 var theinput = container.find("input.raw_input");
790 var theinput = container.find("input.raw_input");
792 var value = theinput.val();
791 var value = theinput.val();
793 var echo = value;
792 var echo = value;
794 // don't echo if it's a password
793 // don't echo if it's a password
795 if (theinput.attr('type') == 'password') {
794 if (theinput.attr('type') == 'password') {
796 echo = 'Β·Β·Β·Β·Β·Β·Β·Β·';
795 echo = 'Β·Β·Β·Β·Β·Β·Β·Β·';
797 }
796 }
798 var content = {
797 var content = {
799 output_type : 'stream',
798 output_type : 'stream',
800 stream : 'stdout',
799 stream : 'stdout',
801 text : theprompt.text() + echo + '\n'
800 text : theprompt.text() + echo + '\n'
802 }
801 }
803 // remove form container
802 // remove form container
804 container.parent().remove();
803 container.parent().remove();
805 // replace with plaintext version in stdout
804 // replace with plaintext version in stdout
806 this.append_output(content, false);
805 this.append_output(content, false);
807 this.events.trigger('send_input_reply.Kernel', value);
806 this.events.trigger('send_input_reply.Kernel', value);
808 }
807 }
809
808
810
809
811 OutputArea.prototype.handle_clear_output = function (msg) {
810 OutputArea.prototype.handle_clear_output = function (msg) {
812 // msg spec v4 had stdout, stderr, display keys
811 // msg spec v4 had stdout, stderr, display keys
813 // v4.1 replaced these with just wait
812 // v4.1 replaced these with just wait
814 // The default behavior is the same (stdout=stderr=display=True, wait=False),
813 // The default behavior is the same (stdout=stderr=display=True, wait=False),
815 // so v4 messages will still be properly handled,
814 // so v4 messages will still be properly handled,
816 // except for the rarely used clearing less than all output.
815 // except for the rarely used clearing less than all output.
817 this.clear_output(msg.content.wait || false);
816 this.clear_output(msg.content.wait || false);
818 };
817 };
819
818
820
819
821 OutputArea.prototype.clear_output = function(wait) {
820 OutputArea.prototype.clear_output = function(wait) {
822 if (wait) {
821 if (wait) {
823
822
824 // If a clear is queued, clear before adding another to the queue.
823 // If a clear is queued, clear before adding another to the queue.
825 if (this.clear_queued) {
824 if (this.clear_queued) {
826 this.clear_output(false);
825 this.clear_output(false);
827 };
826 };
828
827
829 this.clear_queued = true;
828 this.clear_queued = true;
830 } else {
829 } else {
831
830
832 // Fix the output div's height if the clear_output is waiting for
831 // Fix the output div's height if the clear_output is waiting for
833 // new output (it is being used in an animation).
832 // new output (it is being used in an animation).
834 if (this.clear_queued) {
833 if (this.clear_queued) {
835 var height = this.element.height();
834 var height = this.element.height();
836 this.element.height(height);
835 this.element.height(height);
837 this.clear_queued = false;
836 this.clear_queued = false;
838 }
837 }
839
838
840 // Clear all
839 // Clear all
841 // Remove load event handlers from img tags because we don't want
840 // Remove load event handlers from img tags because we don't want
842 // them to fire if the image is never added to the page.
841 // them to fire if the image is never added to the page.
843 this.element.find('img').off('load');
842 this.element.find('img').off('load');
844 this.element.html("");
843 this.element.html("");
845 this.outputs = [];
844 this.outputs = [];
846 this.trusted = true;
845 this.trusted = true;
847 this.unscroll_area();
846 this.unscroll_area();
848 return;
847 return;
849 };
848 };
850 };
849 };
851
850
852
851
853 // JSON serialization
852 // JSON serialization
854
853
855 OutputArea.prototype.fromJSON = function (outputs) {
854 OutputArea.prototype.fromJSON = function (outputs, metadata) {
856 var len = outputs.length;
855 var len = outputs.length;
857 var data;
856 metadata = metadata || {};
858
857
859 for (var i=0; i<len; i++) {
858 for (var i=0; i<len; i++) {
860 data = outputs[i];
859 this.append_output(outputs[i]);
861 var msg_type = data.output_type;
860 }
862 if (msg_type == "pyout") {
861
863 // pyout message has been renamed to execute_result,
862 if (metadata.collapsed !== undefined) {
864 // but the nbformat has not been updated,
863 this.collapsed = metadata.collapsed;
865 // so transform back to pyout for json.
864 if (metadata.collapsed) {
866 msg_type = data.output_type = "execute_result";
865 this.collapse_output();
867 } else if (msg_type == "pyerr") {
868 // pyerr message has been renamed to error,
869 // but the nbformat has not been updated,
870 // so transform back to pyerr for json.
871 msg_type = data.output_type = "error";
872 }
866 }
873 if (msg_type === "display_data" || msg_type === "execute_result") {
867 }
874 // convert short keys to mime keys
868 if (metadata.autoscroll !== undefined) {
875 // TODO: remove mapping of short keys when we update to nbformat 4
869 this.collapsed = metadata.collapsed;
876 data = this.rename_keys(data, OutputArea.mime_map_r);
870 if (metadata.collapsed) {
877 data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map_r);
871 this.collapse_output();
878 // msg spec JSON is an object, nbformat v3 JSON is a JSON string
872 } else {
879 if (data["application/json"] !== undefined && typeof data["application/json"] === 'string') {
873 this.expand_output();
880 data["application/json"] = JSON.parse(data["application/json"]);
881 }
882 }
874 }
883
884 this.append_output(data);
885 }
875 }
886 };
876 };
887
877
888
878
889 OutputArea.prototype.toJSON = function () {
879 OutputArea.prototype.toJSON = function () {
890 var outputs = [];
880 return this.outputs;
891 var len = this.outputs.length;
892 var data;
893 for (var i=0; i<len; i++) {
894 data = this.outputs[i];
895 var msg_type = data.output_type;
896 if (msg_type === "display_data" || msg_type === "execute_result") {
897 // convert mime keys to short keys
898 data = this.rename_keys(data, OutputArea.mime_map);
899 data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map);
900 // msg spec JSON is an object, nbformat v3 JSON is a JSON string
901 if (data.json !== undefined && typeof data.json !== 'string') {
902 data.json = JSON.stringify(data.json);
903 }
904 }
905 if (msg_type == "execute_result") {
906 // pyout message has been renamed to execute_result,
907 // but the nbformat has not been updated,
908 // so transform back to pyout for json.
909 data.output_type = "pyout";
910 } else if (msg_type == "error") {
911 // pyerr message has been renamed to error,
912 // but the nbformat has not been updated,
913 // so transform back to pyerr for json.
914 data.output_type = "pyerr";
915 }
916 outputs[i] = data;
917 }
918 return outputs;
919 };
881 };
920
882
921 /**
883 /**
922 * Class properties
884 * Class properties
923 **/
885 **/
924
886
925 /**
887 /**
926 * Threshold to trigger autoscroll when the OutputArea is resized,
888 * Threshold to trigger autoscroll when the OutputArea is resized,
927 * typically when new outputs are added.
889 * typically when new outputs are added.
928 *
890 *
929 * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold,
891 * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold,
930 * unless it is < 0, in which case autoscroll will never be triggered
892 * unless it is < 0, in which case autoscroll will never be triggered
931 *
893 *
932 * @property auto_scroll_threshold
894 * @property auto_scroll_threshold
933 * @type Number
895 * @type Number
934 * @default 100
896 * @default 100
935 *
897 *
936 **/
898 **/
937 OutputArea.auto_scroll_threshold = 100;
899 OutputArea.auto_scroll_threshold = 100;
938
900
939 /**
901 /**
940 * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas
902 * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas
941 * shorter than this are never scrolled.
903 * shorter than this are never scrolled.
942 *
904 *
943 * @property minimum_scroll_threshold
905 * @property minimum_scroll_threshold
944 * @type Number
906 * @type Number
945 * @default 20
907 * @default 20
946 *
908 *
947 **/
909 **/
948 OutputArea.minimum_scroll_threshold = 20;
910 OutputArea.minimum_scroll_threshold = 20;
949
911
950
912
951
952 OutputArea.mime_map = {
953 "text/plain" : "text",
954 "text/html" : "html",
955 "image/svg+xml" : "svg",
956 "image/png" : "png",
957 "image/jpeg" : "jpeg",
958 "text/latex" : "latex",
959 "application/json" : "json",
960 "application/javascript" : "javascript",
961 };
962
963 OutputArea.mime_map_r = {
964 "text" : "text/plain",
965 "html" : "text/html",
966 "svg" : "image/svg+xml",
967 "png" : "image/png",
968 "jpeg" : "image/jpeg",
969 "latex" : "text/latex",
970 "json" : "application/json",
971 "javascript" : "application/javascript",
972 };
973
974 OutputArea.display_order = [
913 OutputArea.display_order = [
975 'application/javascript',
914 'application/javascript',
976 'text/html',
915 'text/html',
977 'text/markdown',
916 'text/markdown',
978 'text/latex',
917 'text/latex',
979 'image/svg+xml',
918 'image/svg+xml',
980 'image/png',
919 'image/png',
981 'image/jpeg',
920 'image/jpeg',
982 'application/pdf',
921 'application/pdf',
983 'text/plain'
922 'text/plain'
984 ];
923 ];
985
924
986 OutputArea.append_map = {
925 OutputArea.append_map = {
987 "text/plain" : append_text,
926 "text/plain" : append_text,
988 "text/html" : append_html,
927 "text/html" : append_html,
989 "text/markdown": append_markdown,
928 "text/markdown": append_markdown,
990 "image/svg+xml" : append_svg,
929 "image/svg+xml" : append_svg,
991 "image/png" : append_png,
930 "image/png" : append_png,
992 "image/jpeg" : append_jpeg,
931 "image/jpeg" : append_jpeg,
993 "text/latex" : append_latex,
932 "text/latex" : append_latex,
994 "application/javascript" : append_javascript,
933 "application/javascript" : append_javascript,
995 "application/pdf" : append_pdf
934 "application/pdf" : append_pdf
996 };
935 };
997
936
998 // For backwards compatability.
937 // For backwards compatability.
999 IPython.OutputArea = OutputArea;
938 IPython.OutputArea = OutputArea;
1000
939
1001 return {'OutputArea': OutputArea};
940 return {'OutputArea': OutputArea};
1002 });
941 });
@@ -1,98 +1,98 b''
1 //
1 //
2 // Various output tests
2 // Various output tests
3 //
3 //
4
4
5 casper.notebook_test(function () {
5 casper.notebook_test(function () {
6
6
7 this.test_coalesced_output = function (msg, code, expected) {
7 this.test_coalesced_output = function (msg, code, expected) {
8 this.then(function () {
8 this.then(function () {
9 this.echo("Test coalesced output: " + msg);
9 this.echo("Test coalesced output: " + msg);
10 });
10 });
11
11
12 this.thenEvaluate(function (code) {
12 this.thenEvaluate(function (code) {
13 IPython.notebook.insert_cell_at_index(0, "code");
13 IPython.notebook.insert_cell_at_index(0, "code");
14 var cell = IPython.notebook.get_cell(0);
14 var cell = IPython.notebook.get_cell(0);
15 cell.set_text(code);
15 cell.set_text(code);
16 cell.execute();
16 cell.execute();
17 }, {code: code});
17 }, {code: code});
18
18
19 this.wait_for_output(0);
19 this.wait_for_output(0);
20
20
21 this.then(function () {
21 this.then(function () {
22 var results = this.evaluate(function () {
22 var results = this.evaluate(function () {
23 var cell = IPython.notebook.get_cell(0);
23 var cell = IPython.notebook.get_cell(0);
24 return cell.output_area.outputs;
24 return cell.output_area.outputs;
25 });
25 });
26 this.test.assertEquals(results.length, expected.length, "correct number of outputs");
26 this.test.assertEquals(results.length, expected.length, "correct number of outputs");
27 for (var i = 0; i < results.length; i++) {
27 for (var i = 0; i < results.length; i++) {
28 var r = results[i];
28 var r = results[i];
29 var ex = expected[i];
29 var ex = expected[i];
30 this.test.assertEquals(r.output_type, ex.output_type, "output " + i);
30 this.test.assertEquals(r.output_type, ex.output_type, "output " + i);
31 if (r.output_type === 'stream') {
31 if (r.output_type === 'stream') {
32 this.test.assertEquals(r.stream, ex.stream, "stream " + i);
32 this.test.assertEquals(r.name, ex.name, "stream " + i);
33 this.test.assertEquals(r.text, ex.text, "content " + i);
33 this.test.assertEquals(r.text, ex.text, "content " + i);
34 }
34 }
35 }
35 }
36 });
36 });
37
37
38 };
38 };
39
39
40 this.thenEvaluate(function () {
40 this.thenEvaluate(function () {
41 IPython.notebook.insert_cell_at_index(0, "code");
41 IPython.notebook.insert_cell_at_index(0, "code");
42 var cell = IPython.notebook.get_cell(0);
42 var cell = IPython.notebook.get_cell(0);
43 cell.set_text([
43 cell.set_text([
44 "from __future__ import print_function",
44 "from __future__ import print_function",
45 "import sys",
45 "import sys",
46 "from IPython.display import display"
46 "from IPython.display import display"
47 ].join("\n")
47 ].join("\n")
48 );
48 );
49 cell.execute();
49 cell.execute();
50 });
50 });
51
51
52 this.test_coalesced_output("stdout", [
52 this.test_coalesced_output("stdout", [
53 "print(1)",
53 "print(1)",
54 "sys.stdout.flush()",
54 "sys.stdout.flush()",
55 "print(2)",
55 "print(2)",
56 "sys.stdout.flush()",
56 "sys.stdout.flush()",
57 "print(3)"
57 "print(3)"
58 ].join("\n"), [{
58 ].join("\n"), [{
59 output_type: "stream",
59 output_type: "stream",
60 stream: "stdout",
60 name: "stdout",
61 text: "1\n2\n3\n"
61 text: "1\n2\n3\n"
62 }]
62 }]
63 );
63 );
64
64
65 this.test_coalesced_output("stdout+sdterr", [
65 this.test_coalesced_output("stdout+sdterr", [
66 "print(1)",
66 "print(1)",
67 "sys.stdout.flush()",
67 "sys.stdout.flush()",
68 "print(2)",
68 "print(2)",
69 "print(3, file=sys.stderr)"
69 "print(3, file=sys.stderr)"
70 ].join("\n"), [{
70 ].join("\n"), [{
71 output_type: "stream",
71 output_type: "stream",
72 stream: "stdout",
72 name: "stdout",
73 text: "1\n2\n"
73 text: "1\n2\n"
74 },{
74 },{
75 output_type: "stream",
75 output_type: "stream",
76 stream: "stderr",
76 name: "stderr",
77 text: "3\n"
77 text: "3\n"
78 }]
78 }]
79 );
79 );
80
80
81 this.test_coalesced_output("display splits streams", [
81 this.test_coalesced_output("display splits streams", [
82 "print(1)",
82 "print(1)",
83 "sys.stdout.flush()",
83 "sys.stdout.flush()",
84 "display(2)",
84 "display(2)",
85 "print(3)"
85 "print(3)"
86 ].join("\n"), [{
86 ].join("\n"), [{
87 output_type: "stream",
87 output_type: "stream",
88 stream: "stdout",
88 name: "stdout",
89 text: "1\n"
89 text: "1\n"
90 },{
90 },{
91 output_type: "display_data",
91 output_type: "display_data",
92 },{
92 },{
93 output_type: "stream",
93 output_type: "stream",
94 stream: "stdout",
94 name: "stdout",
95 text: "3\n"
95 text: "3\n"
96 }]
96 }]
97 );
97 );
98 });
98 });
@@ -1,245 +1,247 b''
1 // Test opening a rich notebook, saving it, and reopening it again.
1 // Test opening a rich notebook, saving it, and reopening it again.
2 //
2 //
3 //toJSON fromJSON toJSON and do a string comparison
3 //toJSON fromJSON toJSON and do a string comparison
4
4
5
5
6 // this is just a copy of OutputArea.mime_mape_r in IPython/html/static/notebook/js/outputarea.js
6 // this is just a copy of OutputArea.mime_mape_r in IPython/html/static/notebook/js/outputarea.js
7 mime = {
7 mime = {
8 "text" : "text/plain",
8 "text" : "text/plain",
9 "html" : "text/html",
9 "html" : "text/html",
10 "svg" : "image/svg+xml",
10 "svg" : "image/svg+xml",
11 "png" : "image/png",
11 "png" : "image/png",
12 "jpeg" : "image/jpeg",
12 "jpeg" : "image/jpeg",
13 "latex" : "text/latex",
13 "latex" : "text/latex",
14 "json" : "application/json",
14 "json" : "application/json",
15 "javascript" : "application/javascript",
15 "javascript" : "application/javascript",
16 };
16 };
17
17
18 var black_dot_jpeg="u\"\"\"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDACodICUgGiolIiUvLSoyP2lEPzo6P4FcYUxpmYagnpaG\nk5GovfLNqLPltZGT0v/V5fr/////o8v///////L/////2wBDAS0vLz83P3xERHz/rpOu////////\n////////////////////////////////////////////////////////////wgARCAABAAEDAREA\nAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAABP/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEA\nAhADEAAAARn/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAEFAn//xAAUEQEAAAAAAAAAAAAA\nAAAAAAAA/9oACAEDAQE/AX//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAECAQE/AX//xAAUEAEA\nAAAAAAAAAAAAAAAAAAAA/9oACAEBAAY/An//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/\nIX//2gAMAwEAAgADAAAAEB//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAEDAQE/EH//xAAUEQEA\nAAAAAAAAAAAAAAAAAAAA/9oACAECAQE/EH//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/\nEH//2Q==\"\"\"";
18 var black_dot_jpeg="u\"\"\"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDACodICUgGiolIiUvLSoyP2lEPzo6P4FcYUxpmYagnpaG\nk5GovfLNqLPltZGT0v/V5fr/////o8v///////L/////2wBDAS0vLz83P3xERHz/rpOu////////\n////////////////////////////////////////////////////////////wgARCAABAAEDAREA\nAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAABP/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEA\nAhADEAAAARn/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAEFAn//xAAUEQEAAAAAAAAAAAAA\nAAAAAAAA/9oACAEDAQE/AX//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAECAQE/AX//xAAUEAEA\nAAAAAAAAAAAAAAAAAAAA/9oACAEBAAY/An//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/\nIX//2gAMAwEAAgADAAAAEB//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAEDAQE/EH//xAAUEQEA\nAAAAAAAAAAAAAAAAAAAA/9oACAECAQE/EH//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/\nEH//2Q==\"\"\"";
19 var black_dot_png = 'u\"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAWJLR0QA\\niAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB94BCRQnOqNu0b4AAAAKSURBVAjXY2AA\\nAAACAAHiIbwzAAAAAElFTkSuQmCC\"';
19 var black_dot_png = 'u\"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAWJLR0QA\\niAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB94BCRQnOqNu0b4AAAAKSURBVAjXY2AA\\nAAACAAHiIbwzAAAAAElFTkSuQmCC\"';
20 var svg = "\"<svg width='1cm' height='1cm' viewBox='0 0 1000 500'><defs><style>rect {fill:red;}; </style></defs><rect id='r1' x='200' y='100' width='600' height='300' /></svg>\"";
20 var svg = "\"<svg width='1cm' height='1cm' viewBox='0 0 1000 500'><defs><style>rect {fill:red;}; </style></defs><rect id='r1' x='200' y='100' width='600' height='300' /></svg>\"";
21
21
22 // helper function to ensure that the short_name is found in the toJSON
22 // helper function to ensure that the short_name is found in the toJSON
23 // represetnation, while the original in-memory cell retains its long mimetype
23 // represetnation, while the original in-memory cell retains its long mimetype
24 // name, and that fromJSON also gets its long mimetype name
24 // name, and that fromJSON also gets its long mimetype name
25 function assert_has(short_name, json, result, result2) {
25 function assert_has(short_name, json, result, result2) {
26 long_name = mime[short_name];
26 long_name = mime[short_name];
27 this.test.assertTrue(json[0].hasOwnProperty(short_name),
27 this.test.assertFalse(json[0].hasOwnProperty(short_name),
28 'toJSON() representation uses ' + short_name);
28 "toJSON() representation doesn't use " + short_name);
29 this.test.assertTrue(json[0].hasOwnProperty(long_name),
30 'toJSON() representation uses ' + long_name);
29 this.test.assertTrue(result.hasOwnProperty(long_name),
31 this.test.assertTrue(result.hasOwnProperty(long_name),
30 'toJSON() original embedded JSON keeps ' + long_name);
32 'toJSON() original embedded JSON keeps ' + long_name);
31 this.test.assertTrue(result2.hasOwnProperty(long_name),
33 this.test.assertTrue(result2.hasOwnProperty(long_name),
32 'fromJSON() embedded ' + short_name + ' gets mime key ' + long_name);
34 'fromJSON() embedded ' + short_name + ' gets mime key ' + long_name);
33 }
35 }
34
36
35 // helper function for checkout that the first two cells have a particular
37 // helper function for checkout that the first two cells have a particular
36 // output_type (either 'execute_result' or 'display_data'), and checks the to/fromJSON
38 // output_type (either 'execute_result' or 'display_data'), and checks the to/fromJSON
37 // for a set of mimetype keys, using their short names ('javascript', 'text',
39 // for a set of mimetype keys, ensuring the old short names ('javascript', 'text',
38 // 'png', etc).
40 // 'png', etc) are not used.
39 function check_output_area(output_type, keys) {
41 function check_output_area(output_type, keys) {
40 this.wait_for_output(0);
42 this.wait_for_output(0);
41 json = this.evaluate(function() {
43 json = this.evaluate(function() {
42 var json = IPython.notebook.get_cell(0).output_area.toJSON();
44 var json = IPython.notebook.get_cell(0).output_area.toJSON();
43 // appended cell will initially be empty, let's add some output
45 // appended cell will initially be empty, let's add some output
44 IPython.notebook.get_cell(1).output_area.fromJSON(json);
46 IPython.notebook.get_cell(1).output_area.fromJSON(json);
45 return json;
47 return json;
46 });
48 });
47 // The evaluate call above happens asynchronously: wait for cell[1] to have output
49 // The evaluate call above happens asynchronously: wait for cell[1] to have output
48 this.wait_for_output(1);
50 this.wait_for_output(1);
49 var result = this.get_output_cell(0);
51 var result = this.get_output_cell(0);
50 var result2 = this.get_output_cell(1);
52 var result2 = this.get_output_cell(1);
51 this.test.assertEquals(result.output_type, output_type,
53 this.test.assertEquals(result.output_type, output_type,
52 'testing ' + output_type + ' for ' + keys.join(' and '));
54 'testing ' + output_type + ' for ' + keys.join(' and '));
53
55
54 for (var idx in keys) {
56 for (var idx in keys) {
55 assert_has.apply(this, [keys[idx], json, result, result2]);
57 assert_has.apply(this, [keys[idx], json, result, result2]);
56 }
58 }
57 }
59 }
58
60
59
61
60 // helper function to clear the first two cells, set the text of and execute
62 // helper function to clear the first two cells, set the text of and execute
61 // the first one
63 // the first one
62 function clear_and_execute(that, code) {
64 function clear_and_execute(that, code) {
63 that.evaluate(function() {
65 that.evaluate(function() {
64 IPython.notebook.get_cell(0).clear_output();
66 IPython.notebook.get_cell(0).clear_output();
65 IPython.notebook.get_cell(1).clear_output();
67 IPython.notebook.get_cell(1).clear_output();
66 });
68 });
67 that.then(function () {
69 that.then(function () {
68 that.set_cell_text(0, code);
70 that.set_cell_text(0, code);
69 that.execute_cell(0);
71 that.execute_cell(0);
70 that.wait_for_idle();
72 that.wait_for_idle();
71 });
73 });
72 };
74 };
73
75
74 casper.notebook_test(function () {
76 casper.notebook_test(function () {
75 this.evaluate(function () {
77 this.evaluate(function () {
76 var cell = IPython.notebook.get_cell(0);
78 var cell = IPython.notebook.get_cell(0);
77 // "we have to make messes to find out who we are"
79 // "we have to make messes to find out who we are"
78 cell.set_text([
80 cell.set_text([
79 "%%javascript",
81 "%%javascript",
80 "IPython.notebook.insert_cell_below('code')"
82 "IPython.notebook.insert_cell_below('code')"
81 ].join('\n')
83 ].join('\n')
82 );
84 );
83 });
85 });
84
86
85 this.execute_cell_then(0, function () {
87 this.execute_cell_then(0, function () {
86 var result = this.get_output_cell(0);
88 var result = this.get_output_cell(0);
87 var num_cells = this.get_cells_length();
89 var num_cells = this.get_cells_length();
88 this.test.assertEquals(num_cells, 2, '%%javascript magic works');
90 this.test.assertEquals(num_cells, 2, '%%javascript magic works');
89 this.test.assertTrue(result.hasOwnProperty('application/javascript'),
91 this.test.assertTrue(result.hasOwnProperty('application/javascript'),
90 'testing JS embedded with mime key');
92 'testing JS embedded with mime key');
91 });
93 });
92
94
93 //this.thenEvaluate(function() { IPython.notebook.save_notebook(); });
95 //this.thenEvaluate(function() { IPython.notebook.save_notebook(); });
94 this.then(function () {
96 this.then(function () {
95 clear_and_execute(this, [
97 clear_and_execute(this, [
96 "%%javascript",
98 "%%javascript",
97 "var a=5;"
99 "var a=5;"
98 ].join('\n'));
100 ].join('\n'));
99 });
101 });
100
102
101
103
102 this.then(function () {
104 this.then(function () {
103 check_output_area.apply(this, ['display_data', ['javascript']]);
105 check_output_area.apply(this, ['display_data', ['javascript']]);
104
106
105 });
107 });
106
108
107 this.then(function() {
109 this.then(function() {
108 clear_and_execute(this, '%lsmagic');
110 clear_and_execute(this, '%lsmagic');
109 });
111 });
110
112
111 this.then(function () {
113 this.then(function () {
112 check_output_area.apply(this, ['execute_result', ['text', 'json']]);
114 check_output_area.apply(this, ['execute_result', ['text', 'json']]);
113 });
115 });
114
116
115 this.then(function() {
117 this.then(function() {
116 clear_and_execute(this,
118 clear_and_execute(this,
117 "x = %lsmagic\nfrom IPython.display import display; display(x)");
119 "x = %lsmagic\nfrom IPython.display import display; display(x)");
118 });
120 });
119
121
120 this.then(function ( ) {
122 this.then(function ( ) {
121 check_output_area.apply(this, ['display_data', ['text', 'json']]);
123 check_output_area.apply(this, ['display_data', ['text', 'json']]);
122 });
124 });
123
125
124 this.then(function() {
126 this.then(function() {
125 clear_and_execute(this,
127 clear_and_execute(this,
126 "from IPython.display import Latex; Latex('$X^2$')");
128 "from IPython.display import Latex; Latex('$X^2$')");
127 });
129 });
128
130
129 this.then(function ( ) {
131 this.then(function ( ) {
130 check_output_area.apply(this, ['execute_result', ['text', 'latex']]);
132 check_output_area.apply(this, ['execute_result', ['text', 'latex']]);
131 });
133 });
132
134
133 this.then(function() {
135 this.then(function() {
134 clear_and_execute(this,
136 clear_and_execute(this,
135 "from IPython.display import Latex, display; display(Latex('$X^2$'))");
137 "from IPython.display import Latex, display; display(Latex('$X^2$'))");
136 });
138 });
137
139
138 this.then(function ( ) {
140 this.then(function ( ) {
139 check_output_area.apply(this, ['display_data', ['text', 'latex']]);
141 check_output_area.apply(this, ['display_data', ['text', 'latex']]);
140 });
142 });
141
143
142 this.then(function() {
144 this.then(function() {
143 clear_and_execute(this,
145 clear_and_execute(this,
144 "from IPython.display import HTML; HTML('<b>it works!</b>')");
146 "from IPython.display import HTML; HTML('<b>it works!</b>')");
145 });
147 });
146
148
147 this.then(function ( ) {
149 this.then(function ( ) {
148 check_output_area.apply(this, ['execute_result', ['text', 'html']]);
150 check_output_area.apply(this, ['execute_result', ['text', 'html']]);
149 });
151 });
150
152
151 this.then(function() {
153 this.then(function() {
152 clear_and_execute(this,
154 clear_and_execute(this,
153 "from IPython.display import HTML, display; display(HTML('<b>it works!</b>'))");
155 "from IPython.display import HTML, display; display(HTML('<b>it works!</b>'))");
154 });
156 });
155
157
156 this.then(function ( ) {
158 this.then(function ( ) {
157 check_output_area.apply(this, ['display_data', ['text', 'html']]);
159 check_output_area.apply(this, ['display_data', ['text', 'html']]);
158 });
160 });
159
161
160
162
161 this.then(function() {
163 this.then(function() {
162 clear_and_execute(this,
164 clear_and_execute(this,
163 "from IPython.display import Image; Image(" + black_dot_png + ")");
165 "from IPython.display import Image; Image(" + black_dot_png + ")");
164 });
166 });
165 this.thenEvaluate(function() { IPython.notebook.save_notebook(); });
167 this.thenEvaluate(function() { IPython.notebook.save_notebook(); });
166
168
167 this.then(function ( ) {
169 this.then(function ( ) {
168 check_output_area.apply(this, ['execute_result', ['text', 'png']]);
170 check_output_area.apply(this, ['execute_result', ['text', 'png']]);
169 });
171 });
170
172
171 this.then(function() {
173 this.then(function() {
172 clear_and_execute(this,
174 clear_and_execute(this,
173 "from IPython.display import Image, display; display(Image(" + black_dot_png + "))");
175 "from IPython.display import Image, display; display(Image(" + black_dot_png + "))");
174 });
176 });
175
177
176 this.then(function ( ) {
178 this.then(function ( ) {
177 check_output_area.apply(this, ['display_data', ['text', 'png']]);
179 check_output_area.apply(this, ['display_data', ['text', 'png']]);
178 });
180 });
179
181
180
182
181 this.then(function() {
183 this.then(function() {
182 clear_and_execute(this,
184 clear_and_execute(this,
183 "from IPython.display import Image; Image(" + black_dot_jpeg + ", format='jpeg')");
185 "from IPython.display import Image; Image(" + black_dot_jpeg + ", format='jpeg')");
184 });
186 });
185
187
186 this.then(function ( ) {
188 this.then(function ( ) {
187 check_output_area.apply(this, ['execute_result', ['text', 'jpeg']]);
189 check_output_area.apply(this, ['execute_result', ['text', 'jpeg']]);
188 });
190 });
189
191
190 this.then(function() {
192 this.then(function() {
191 clear_and_execute(this,
193 clear_and_execute(this,
192 "from IPython.display import Image, display; display(Image(" + black_dot_jpeg + ", format='jpeg'))");
194 "from IPython.display import Image, display; display(Image(" + black_dot_jpeg + ", format='jpeg'))");
193 });
195 });
194
196
195 this.then(function ( ) {
197 this.then(function ( ) {
196 check_output_area.apply(this, ['display_data', ['text', 'jpeg']]);
198 check_output_area.apply(this, ['display_data', ['text', 'jpeg']]);
197 });
199 });
198
200
199 this.then(function() {
201 this.then(function() {
200 clear_and_execute(this,
202 clear_and_execute(this,
201 "from IPython.core.display import SVG; SVG(" + svg + ")");
203 "from IPython.core.display import SVG; SVG(" + svg + ")");
202 });
204 });
203
205
204 this.then(function ( ) {
206 this.then(function ( ) {
205 check_output_area.apply(this, ['execute_result', ['text', 'svg']]);
207 check_output_area.apply(this, ['execute_result', ['text', 'svg']]);
206 });
208 });
207
209
208 this.then(function() {
210 this.then(function() {
209 clear_and_execute(this,
211 clear_and_execute(this,
210 "from IPython.core.display import SVG, display; display(SVG(" + svg + "))");
212 "from IPython.core.display import SVG, display; display(SVG(" + svg + "))");
211 });
213 });
212
214
213 this.then(function ( ) {
215 this.then(function ( ) {
214 check_output_area.apply(this, ['display_data', ['text', 'svg']]);
216 check_output_area.apply(this, ['display_data', ['text', 'svg']]);
215 });
217 });
216
218
217 this.thenEvaluate(function() { IPython.notebook.save_notebook(); });
219 this.thenEvaluate(function() { IPython.notebook.save_notebook(); });
218
220
219 this.then(function() {
221 this.then(function() {
220 clear_and_execute(this, [
222 clear_and_execute(this, [
221 "from IPython.core.formatters import HTMLFormatter",
223 "from IPython.core.formatters import HTMLFormatter",
222 "x = HTMLFormatter()",
224 "x = HTMLFormatter()",
223 "x.format_type = 'text/superfancymimetype'",
225 "x.format_type = 'text/superfancymimetype'",
224 "get_ipython().display_formatter.formatters['text/superfancymimetype'] = x",
226 "get_ipython().display_formatter.formatters['text/superfancymimetype'] = x",
225 "from IPython.display import HTML, display",
227 "from IPython.display import HTML, display",
226 'display(HTML("yo"))',
228 'display(HTML("yo"))',
227 "HTML('hello')"].join('\n')
229 "HTML('hello')"].join('\n')
228 );
230 );
229
231
230 });
232 });
231
233
232 this.wait_for_output(0, 1);
234 this.wait_for_output(0, 1);
233
235
234 this.then(function () {
236 this.then(function () {
235 var long_name = 'text/superfancymimetype';
237 var long_name = 'text/superfancymimetype';
236 var result = this.get_output_cell(0);
238 var result = this.get_output_cell(0);
237 this.test.assertTrue(result.hasOwnProperty(long_name),
239 this.test.assertTrue(result.hasOwnProperty(long_name),
238 'display_data custom mimetype ' + long_name);
240 'display_data custom mimetype ' + long_name);
239 var result = this.get_output_cell(0, 1);
241 var result = this.get_output_cell(0, 1);
240 this.test.assertTrue(result.hasOwnProperty(long_name),
242 this.test.assertTrue(result.hasOwnProperty(long_name),
241 'execute_result custom mimetype ' + long_name);
243 'execute_result custom mimetype ' + long_name);
242
244
243 });
245 });
244
246
245 });
247 });
@@ -1,151 +1,151 b''
1 # coding: utf-8
1 # coding: utf-8
2 """Test the /files/ handler."""
2 """Test the /files/ handler."""
3
3
4 import io
4 import io
5 import os
5 import os
6 from unicodedata import normalize
6 from unicodedata import normalize
7
7
8 pjoin = os.path.join
8 pjoin = os.path.join
9
9
10 import requests
10 import requests
11 import json
11 import json
12
12
13 from IPython.nbformat.current import (new_notebook, write, new_worksheet,
13 from IPython.nbformat.current import (new_notebook, write,
14 new_heading_cell, new_code_cell,
14 new_markdown_cell, new_code_cell,
15 new_output)
15 new_output)
16
16
17 from IPython.html.utils import url_path_join
17 from IPython.html.utils import url_path_join
18 from .launchnotebook import NotebookTestBase
18 from .launchnotebook import NotebookTestBase
19 from IPython.utils import py3compat
19 from IPython.utils import py3compat
20
20
21
21
22 class FilesTest(NotebookTestBase):
22 class FilesTest(NotebookTestBase):
23 def test_hidden_files(self):
23 def test_hidden_files(self):
24 not_hidden = [
24 not_hidden = [
25 u'Γ₯ b',
25 u'Γ₯ b',
26 u'Γ₯ b/Γ§. d',
26 u'Γ₯ b/Γ§. d',
27 ]
27 ]
28 hidden = [
28 hidden = [
29 u'.Γ₯ b',
29 u'.Γ₯ b',
30 u'Γ₯ b/.Γ§ d',
30 u'Γ₯ b/.Γ§ d',
31 ]
31 ]
32 dirs = not_hidden + hidden
32 dirs = not_hidden + hidden
33
33
34 nbdir = self.notebook_dir.name
34 nbdir = self.notebook_dir.name
35 for d in dirs:
35 for d in dirs:
36 path = pjoin(nbdir, d.replace('/', os.sep))
36 path = pjoin(nbdir, d.replace('/', os.sep))
37 if not os.path.exists(path):
37 if not os.path.exists(path):
38 os.mkdir(path)
38 os.mkdir(path)
39 with open(pjoin(path, 'foo'), 'w') as f:
39 with open(pjoin(path, 'foo'), 'w') as f:
40 f.write('foo')
40 f.write('foo')
41 with open(pjoin(path, '.foo'), 'w') as f:
41 with open(pjoin(path, '.foo'), 'w') as f:
42 f.write('.foo')
42 f.write('.foo')
43 url = self.base_url()
43 url = self.base_url()
44
44
45 for d in not_hidden:
45 for d in not_hidden:
46 path = pjoin(nbdir, d.replace('/', os.sep))
46 path = pjoin(nbdir, d.replace('/', os.sep))
47 r = requests.get(url_path_join(url, 'files', d, 'foo'))
47 r = requests.get(url_path_join(url, 'files', d, 'foo'))
48 r.raise_for_status()
48 r.raise_for_status()
49 self.assertEqual(r.text, 'foo')
49 self.assertEqual(r.text, 'foo')
50 r = requests.get(url_path_join(url, 'files', d, '.foo'))
50 r = requests.get(url_path_join(url, 'files', d, '.foo'))
51 self.assertEqual(r.status_code, 404)
51 self.assertEqual(r.status_code, 404)
52
52
53 for d in hidden:
53 for d in hidden:
54 path = pjoin(nbdir, d.replace('/', os.sep))
54 path = pjoin(nbdir, d.replace('/', os.sep))
55 for foo in ('foo', '.foo'):
55 for foo in ('foo', '.foo'):
56 r = requests.get(url_path_join(url, 'files', d, foo))
56 r = requests.get(url_path_join(url, 'files', d, foo))
57 self.assertEqual(r.status_code, 404)
57 self.assertEqual(r.status_code, 404)
58
58
59 def test_contents_manager(self):
59 def test_contents_manager(self):
60 "make sure ContentsManager returns right files (ipynb, bin, txt)."
60 "make sure ContentsManager returns right files (ipynb, bin, txt)."
61
61
62 nbdir = self.notebook_dir.name
62 nbdir = self.notebook_dir.name
63 base = self.base_url()
63 base = self.base_url()
64
64
65 nb = new_notebook(name='testnb')
65 nb = new_notebook(
66
66 cells=[
67 ws = new_worksheet()
67 new_markdown_cell(u'Created by test Β³'),
68 nb.worksheets = [ws]
68 new_code_cell("print(2*6)", outputs=[
69 ws.cells.append(new_heading_cell(u'Created by test Β³'))
69 new_output("stream", text="12"),
70 cc1 = new_code_cell(input=u'print(2*6)')
70 ])
71 cc1.outputs.append(new_output(output_text=u'12', output_type='stream'))
71 ]
72 ws.cells.append(cc1)
72 )
73
73
74 with io.open(pjoin(nbdir, 'testnb.ipynb'), 'w',
74 with io.open(pjoin(nbdir, 'testnb.ipynb'), 'w',
75 encoding='utf-8') as f:
75 encoding='utf-8') as f:
76 write(nb, f, format='ipynb')
76 write(nb, f)
77
77
78 with io.open(pjoin(nbdir, 'test.bin'), 'wb') as f:
78 with io.open(pjoin(nbdir, 'test.bin'), 'wb') as f:
79 f.write(b'\xff' + os.urandom(5))
79 f.write(b'\xff' + os.urandom(5))
80 f.close()
80 f.close()
81
81
82 with io.open(pjoin(nbdir, 'test.txt'), 'w') as f:
82 with io.open(pjoin(nbdir, 'test.txt'), 'w') as f:
83 f.write(u'foobar')
83 f.write(u'foobar')
84 f.close()
84 f.close()
85
85
86 r = requests.get(url_path_join(base, 'files', 'testnb.ipynb'))
86 r = requests.get(url_path_join(base, 'files', 'testnb.ipynb'))
87 self.assertEqual(r.status_code, 200)
87 self.assertEqual(r.status_code, 200)
88 self.assertIn('print(2*6)', r.text)
88 self.assertIn('print(2*6)', r.text)
89 json.loads(r.text)
89 json.loads(r.text)
90
90
91 r = requests.get(url_path_join(base, 'files', 'test.bin'))
91 r = requests.get(url_path_join(base, 'files', 'test.bin'))
92 self.assertEqual(r.status_code, 200)
92 self.assertEqual(r.status_code, 200)
93 self.assertEqual(r.headers['content-type'], 'application/octet-stream')
93 self.assertEqual(r.headers['content-type'], 'application/octet-stream')
94 self.assertEqual(r.content[:1], b'\xff')
94 self.assertEqual(r.content[:1], b'\xff')
95 self.assertEqual(len(r.content), 6)
95 self.assertEqual(len(r.content), 6)
96
96
97 r = requests.get(url_path_join(base, 'files', 'test.txt'))
97 r = requests.get(url_path_join(base, 'files', 'test.txt'))
98 self.assertEqual(r.status_code, 200)
98 self.assertEqual(r.status_code, 200)
99 self.assertEqual(r.headers['content-type'], 'text/plain')
99 self.assertEqual(r.headers['content-type'], 'text/plain')
100 self.assertEqual(r.text, 'foobar')
100 self.assertEqual(r.text, 'foobar')
101
101
102 def test_download(self):
102 def test_download(self):
103 nbdir = self.notebook_dir.name
103 nbdir = self.notebook_dir.name
104 base = self.base_url()
104 base = self.base_url()
105
105
106 text = 'hello'
106 text = 'hello'
107 with open(pjoin(nbdir, 'test.txt'), 'w') as f:
107 with open(pjoin(nbdir, 'test.txt'), 'w') as f:
108 f.write(text)
108 f.write(text)
109
109
110 r = requests.get(url_path_join(base, 'files', 'test.txt'))
110 r = requests.get(url_path_join(base, 'files', 'test.txt'))
111 disposition = r.headers.get('Content-Disposition', '')
111 disposition = r.headers.get('Content-Disposition', '')
112 self.assertNotIn('attachment', disposition)
112 self.assertNotIn('attachment', disposition)
113
113
114 r = requests.get(url_path_join(base, 'files', 'test.txt') + '?download=1')
114 r = requests.get(url_path_join(base, 'files', 'test.txt') + '?download=1')
115 disposition = r.headers.get('Content-Disposition', '')
115 disposition = r.headers.get('Content-Disposition', '')
116 self.assertIn('attachment', disposition)
116 self.assertIn('attachment', disposition)
117 self.assertIn('filename="test.txt"', disposition)
117 self.assertIn('filename="test.txt"', disposition)
118
118
119 def test_old_files_redirect(self):
119 def test_old_files_redirect(self):
120 """pre-2.0 'files/' prefixed links are properly redirected"""
120 """pre-2.0 'files/' prefixed links are properly redirected"""
121 nbdir = self.notebook_dir.name
121 nbdir = self.notebook_dir.name
122 base = self.base_url()
122 base = self.base_url()
123
123
124 os.mkdir(pjoin(nbdir, 'files'))
124 os.mkdir(pjoin(nbdir, 'files'))
125 os.makedirs(pjoin(nbdir, 'sub', 'files'))
125 os.makedirs(pjoin(nbdir, 'sub', 'files'))
126
126
127 for prefix in ('', 'sub'):
127 for prefix in ('', 'sub'):
128 with open(pjoin(nbdir, prefix, 'files', 'f1.txt'), 'w') as f:
128 with open(pjoin(nbdir, prefix, 'files', 'f1.txt'), 'w') as f:
129 f.write(prefix + '/files/f1')
129 f.write(prefix + '/files/f1')
130 with open(pjoin(nbdir, prefix, 'files', 'f2.txt'), 'w') as f:
130 with open(pjoin(nbdir, prefix, 'files', 'f2.txt'), 'w') as f:
131 f.write(prefix + '/files/f2')
131 f.write(prefix + '/files/f2')
132 with open(pjoin(nbdir, prefix, 'f2.txt'), 'w') as f:
132 with open(pjoin(nbdir, prefix, 'f2.txt'), 'w') as f:
133 f.write(prefix + '/f2')
133 f.write(prefix + '/f2')
134 with open(pjoin(nbdir, prefix, 'f3.txt'), 'w') as f:
134 with open(pjoin(nbdir, prefix, 'f3.txt'), 'w') as f:
135 f.write(prefix + '/f3')
135 f.write(prefix + '/f3')
136
136
137 url = url_path_join(base, 'notebooks', prefix, 'files', 'f1.txt')
137 url = url_path_join(base, 'notebooks', prefix, 'files', 'f1.txt')
138 r = requests.get(url)
138 r = requests.get(url)
139 self.assertEqual(r.status_code, 200)
139 self.assertEqual(r.status_code, 200)
140 self.assertEqual(r.text, prefix + '/files/f1')
140 self.assertEqual(r.text, prefix + '/files/f1')
141
141
142 url = url_path_join(base, 'notebooks', prefix, 'files', 'f2.txt')
142 url = url_path_join(base, 'notebooks', prefix, 'files', 'f2.txt')
143 r = requests.get(url)
143 r = requests.get(url)
144 self.assertEqual(r.status_code, 200)
144 self.assertEqual(r.status_code, 200)
145 self.assertEqual(r.text, prefix + '/files/f2')
145 self.assertEqual(r.text, prefix + '/files/f2')
146
146
147 url = url_path_join(base, 'notebooks', prefix, 'files', 'f3.txt')
147 url = url_path_join(base, 'notebooks', prefix, 'files', 'f3.txt')
148 r = requests.get(url)
148 r = requests.get(url)
149 self.assertEqual(r.status_code, 200)
149 self.assertEqual(r.status_code, 200)
150 self.assertEqual(r.text, prefix + '/f3')
150 self.assertEqual(r.text, prefix + '/f3')
151
151
General Comments 0
You need to be logged in to leave comments. Login now