##// END OF EJS Templates
change standard money keys
Zachary Sailer -
Show More
@@ -1,104 +1,104 b''
1 1 """A base class contents manager.
2 2
3 3 Authors:
4 4
5 5 * Zach Sailer
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2013 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import datetime
20 20 import io
21 21 import os
22 22 import glob
23 23 import shutil
24 24 import ast
25 25 import base64
26 26
27 27 from tornado import web
28 28
29 29 from IPython.config.configurable import LoggingConfigurable
30 30 from IPython.nbformat import current
31 31 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
32 32 from IPython.utils import tz
33 33
34 34 #-----------------------------------------------------------------------------
35 35 # Classes
36 36 #-----------------------------------------------------------------------------
37 37
38 38 class ContentManager(LoggingConfigurable):
39 39
40 40 content_dir = Unicode(os.getcwdu(), config=True, help="""
41 41 The directory to use for contents.
42 42 """)
43 43
44 44 contents = List()
45 45
46 46 def get_content_names(self, content_path):
47 47 """List of dicts of files in content_path"""
48 48 names = glob.glob(os.path.join(self.content_dir, content_path,'*'))
49 49 contents = list()
50 50 dirs = list()
51 51 notebooks = list()
52 52 for name in names:
53 53 if os.path.isdir(name) == True:
54 54 dirs.append(os.path.split(name)[1])
55 55 elif os.path.splitext(name)[1] == '.ipynb':
56 56 notebooks.append(os.path.split(name)[1])
57 57 else:
58 58 contents.append(os.path.split(name)[1])
59 59 return dirs, notebooks, contents
60 60
61 61 def list_contents(self, content_path):
62 62 """List all contents in the named path."""
63 63 dir_names, notebook_names, content_names = self.get_content_names(content_path)
64 64 content_mapping = []
65 65 for name in dir_names:
66 66 model = self.content_model(name, content_path, type='dir')
67 67 content_mapping.append(model)
68 68 for name in content_names:
69 69 model = self.content_model(name, content_path, type='file')
70 70 content_mapping.append(model)
71 71 for name in notebook_names:
72 72 model = self.content_model(name, content_path, type='notebook')
73 73 content_mapping.append(model)
74 74 return content_mapping
75 75
76 76 def get_path_by_name(self, name, content_path):
77 77 """Return a full path to content"""
78 78 path = os.path.join(self.content_dir, content_path, name)
79 79 return path
80 80
81 81 def content_info(self, name, content_path):
82 82 """Read the content of a named file"""
83 83 file_type = os.path.splitext(os.path.basename(name))[1]
84 84 full_path = self.get_path_by_name(name, content_path)
85 85 info = os.stat(full_path)
86 86 size = info.st_size
87 87 last_modified = tz.utcfromtimestamp(info.st_mtime)
88 88 return last_modified, file_type, size
89 89
90 90 def content_model(self, name, content_path, type=None):
91 91 """Create a dict standard model for any file (other than notebooks)"""
92 92 last_modified, file_type, size = self.content_info(name, content_path)
93 93 model = {"name": name,
94 94 "path": content_path,
95 95 "type": type,
96 96 "MIME-type": "",
97 "last_modified": last_modified.ctime(),
97 "last_modified (UTC)": last_modified.ctime(),
98 98 "size": size}
99 99 return model
100 100
101 101 def delete_content(self, content_path):
102 102 """Delete a file"""
103 103 os.unlink(os.path.join(self.content_dir, content_path))
104 104 No newline at end of file
@@ -1,68 +1,85 b''
1 1 """Tornado handlers for the contents web service.
2 2
3 3 Authors:
4 4
5 5 * Zach Sailer
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2013 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 from tornado import web
20 20
21 21 from zmq.utils import jsonapi
22 22
23 23 from IPython.utils.jsonutil import date_default
24 24
25 25 from ...base.handlers import IPythonHandler
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Contents web service handlers
29 29 #-----------------------------------------------------------------------------
30 30
31 31
32 32 class ContentRootHandler(IPythonHandler):
33 33
34 34 @web.authenticated
35 35 def get(self):
36 36 cm = self.content_manager
37 37 contents = cm.list_contents("")
38 38 self.finish(jsonapi.dumps(contents))
39 39
40 40
41 41 class ContentHandler(IPythonHandler):
42 42
43 43 @web.authenticated
44 44 def get(self, content_path):
45 45 cm = self.content_manager
46 46 contents = cm.list_contents(content_path)
47 47 self.finish(jsonapi.dumps(contents))
48 48
49 49 @web.authenticated
50 50 def delete(self, content_path):
51 51 cm = self.content_manager
52 52 cm.delete_content(content_path)
53 53 self.set_status(204)
54 54 self.finish()
55 55
56 class ServicesRedirectHandler(IPythonHandler):
57
58 @web.authenticated
59 def get(self):
60 url = self.base_project_url + 'api'
61 self.redirect(url)
62
63 class ServicesHandler(IPythonHandler):
64
65 @web.authenticated
66 def get(self):
67 services = ['contents', 'notebooks', 'sessions', 'kernels', 'clusters']
68 self.finish(jsonapi.dumps(services))
69
56 70
57 71 #-----------------------------------------------------------------------------
58 72 # URL to handler mappings
59 73 #-----------------------------------------------------------------------------
60 74
61 75 _content_path_regex = r"(?P<content_path>.+)"
62 76
63 77 default_handlers = [
64 78 (r"api/contents/%s" % _content_path_regex, ContentHandler),
65 (r"api/contents", ContentRootHandler)
79 (r"api/contents", ContentRootHandler),
80 (r"api/", ServicesRedirectHandler),
81 (r"api", ServicesHandler)
82
66 83 ]
67 84
68 85
@@ -1,96 +1,96 b''
1 1 """A kernel manager relating notebooks and kernels
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2013 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 from tornado import web
20 20
21 21 from IPython.kernel.multikernelmanager import MultiKernelManager
22 22 from IPython.utils.traitlets import (
23 23 Dict, List, Unicode,
24 24 )
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Classes
28 28 #-----------------------------------------------------------------------------
29 29
30 30
31 31 class MappingKernelManager(MultiKernelManager):
32 32 """A KernelManager that handles notebook mapping and HTTP error handling"""
33 33
34 34 def _kernel_manager_class_default(self):
35 35 return "IPython.kernel.ioloop.IOLoopKernelManager"
36 36
37 37 kernel_argv = List(Unicode)
38 38 kernels = []
39 39
40 40 #-------------------------------------------------------------------------
41 41 # Methods for managing kernels and sessions
42 42 #-------------------------------------------------------------------------
43 43
44 44 def _handle_kernel_died(self, kernel_id):
45 45 """notice that a kernel died"""
46 46 self.log.warn("Kernel %s died, removing from map.", kernel_id)
47 47 self.remove_kernel(kernel_id)
48 48
49 49 def start_kernel(self, **kwargs):
50 50 """Start a kernel for a session an return its kernel_id.
51 51
52 52 Parameters
53 53 ----------
54 54 session_id : uuid
55 55 The uuid of the session to associate the new kernel with. If this
56 56 is not None, this kernel will be persistent whenever the session
57 57 requests a kernel.
58 58 """
59 59 kernel_id = None
60 60 if kernel_id is None:
61 61 kwargs['extra_arguments'] = self.kernel_argv
62 62 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
63 63 self.log.info("Kernel started: %s" % kernel_id)
64 64 self.log.debug("Kernel args: %r" % kwargs)
65 65 # register callback for failed auto-restart
66 66 self.add_restart_callback(kernel_id,
67 67 lambda : self._handle_kernel_died(kernel_id),
68 68 'dead',
69 69 )
70 70 else:
71 71 self.log.info("Using existing kernel: %s" % kernel_id)
72 72
73 73 return kernel_id
74 74
75 75 def shutdown_kernel(self, kernel_id, now=False):
76 76 """Shutdown a kernel by kernel_id"""
77 77 i = 0
78 78 for kernel in self.kernels:
79 if kernel['kernel_id'] == kernel_id:
79 if kernel['id'] == kernel_id:
80 80 del self.kernels[i]
81 81 i = i+1
82 82 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
83 83
84 84 def kernel_model(self, kernel_id, ws_url):
85 model = {"kernel_id":kernel_id, "ws_url": ws_url}
85 model = {"id":kernel_id, "ws_url": ws_url}
86 86 self.kernels.append(model)
87 87 return model
88 88
89 89 def list_kernels(self):
90 90 return self.kernels
91 91
92 92 # override _check_kernel_id to raise 404 instead of KeyError
93 93 def _check_kernel_id(self, kernel_id):
94 94 """Check a that a kernel_id exists and raise 404 if not."""
95 95 if kernel_id not in self:
96 96 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
@@ -1,266 +1,266 b''
1 1 """A base class notebook manager.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import os
20 20 import uuid
21 21
22 22 from tornado import web
23 23 from urllib import quote, unquote
24 24
25 25 from IPython.config.configurable import LoggingConfigurable
26 26 from IPython.nbformat import current
27 27 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
28 28
29 29 #-----------------------------------------------------------------------------
30 30 # Classes
31 31 #-----------------------------------------------------------------------------
32 32
33 33 class NotebookManager(LoggingConfigurable):
34 34
35 35 # Todo:
36 36 # The notebook_dir attribute is used to mean a couple of different things:
37 37 # 1. Where the notebooks are stored if FileNotebookManager is used.
38 38 # 2. The cwd of the kernel for a project.
39 39 # Right now we use this attribute in a number of different places and
40 40 # we are going to have to disentangle all of this.
41 41 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
42 42 The directory to use for notebooks.
43 43 """)
44 44
45 45 def named_notebook_path(self, notebook_path):
46 46
47 47 l = len(notebook_path)
48 48 names = notebook_path.split('/')
49 49 if len(names) > 1:
50 50 name = names[len(names)-1]
51 51 if name[(len(name)-6):(len(name))] == ".ipynb":
52 52 name = name
53 53 path = notebook_path[0:l-len(name)-1]+'/'
54 54 else:
55 55 name = None
56 56 path = notebook_path+'/'
57 57 else:
58 58 name = names[0]
59 59 if name[(len(name)-6):(len(name))] == ".ipynb":
60 60 name = name
61 61 path = None
62 62 else:
63 63 name = None
64 64 path = notebook_path+'/'
65 65 return name, path
66 66
67 67 def url_encode(self, path):
68 68 parts = path.split('/')
69 69 path=""
70 70 for part in parts:
71 71 part = quote(part)
72 72 path = os.path.join(path,part)
73 73 return path
74 74
75 75 def url_decode(self, path):
76 76 parts = path.split('/')
77 77 path=""
78 78 for part in parts:
79 79 part = unquote(part)
80 80 path = os.path.join(path,part)
81 81 return path
82 82
83 83 def _notebook_dir_changed(self, new):
84 84 """do a bit of validation of the notebook dir"""
85 85 if not os.path.isabs(new):
86 86 # If we receive a non-absolute path, make it absolute.
87 87 abs_new = os.path.abspath(new)
88 88 #self.notebook_dir = os.path.dirname(abs_new)
89 89 return
90 90 if os.path.exists(new) and not os.path.isdir(new):
91 91 raise TraitError("notebook dir %r is not a directory" % new)
92 92 if not os.path.exists(new):
93 93 self.log.info("Creating notebook dir %s", new)
94 94 try:
95 95 os.mkdir(new)
96 96 except:
97 97 raise TraitError("Couldn't create notebook dir %r" % new)
98 98
99 99 allowed_formats = List([u'json',u'py'])
100 100
101 101 def add_new_folder(self, path=None):
102 102 new_path = os.path.join(self.notebook_dir, path)
103 103 if not os.path.exists(new_path):
104 104 os.makedirs(new_path)
105 105 else:
106 106 raise web.HTTPError(409, u'Directory already exists or creation permission not allowed.')
107 107
108 108 def load_notebook_names(self, path):
109 109 """Load the notebook names into memory.
110 110
111 111 This should be called once immediately after the notebook manager
112 112 is created to load the existing notebooks into the mapping in
113 113 memory.
114 114 """
115 115 self.list_notebooks(path)
116 116
117 117 def list_notebooks(self):
118 118 """List all notebooks.
119 119
120 120 This returns a list of dicts, each of the form::
121 121
122 122 dict(notebook_id=notebook,name=name)
123 123
124 124 This list of dicts should be sorted by name::
125 125
126 126 data = sorted(data, key=lambda item: item['name'])
127 127 """
128 128 raise NotImplementedError('must be implemented in a subclass')
129 129
130 130
131 131 def notebook_exists(self, notebook_path):
132 132 """Does a notebook exist?"""
133 133
134 134
135 135 def notebook_model(self, notebook_name, notebook_path=None, content=True):
136 136 """ Creates the standard notebook model """
137 137 last_modified, contents = self.read_notebook_object(notebook_name, notebook_path)
138 model = {"notebook_name": notebook_name,
139 "notebook_path": notebook_path,
140 "last_modified": last_modified.ctime()}
138 model = {"name": notebook_name,
139 "path": notebook_path,
140 "last_modified (UTC)": last_modified.ctime()}
141 141 if content == True:
142 142 model['content'] = contents
143 143 return model
144 144
145 145 def get_notebook(self, notebook_name, notebook_path=None, format=u'json'):
146 146 """Get the representation of a notebook in format by notebook_name."""
147 147 format = unicode(format)
148 148 if format not in self.allowed_formats:
149 149 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
150 150 kwargs = {}
151 151 last_mod, nb = self.read_notebook_object(notebook_name, notebook_path)
152 152 if format == 'json':
153 153 # don't split lines for sending over the wire, because it
154 154 # should match the Python in-memory format.
155 155 kwargs['split_lines'] = False
156 156 representation = current.writes(nb, format, **kwargs)
157 157 name = nb.metadata.get('name', 'notebook')
158 158 return last_mod, representation, name
159 159
160 160 def read_notebook_object(self, notebook_name, notebook_path):
161 161 """Get the object representation of a notebook by notebook_id."""
162 162 raise NotImplementedError('must be implemented in a subclass')
163 163
164 164 def save_new_notebook(self, data, notebook_path = None, name=None, format=u'json'):
165 165 """Save a new notebook and return its name.
166 166
167 167 If a name is passed in, it overrides any values in the notebook data
168 168 and the value in the data is updated to use that value.
169 169 """
170 170 if format not in self.allowed_formats:
171 171 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
172 172
173 173 try:
174 174 nb = current.reads(data.decode('utf-8'), format)
175 175 except:
176 176 raise web.HTTPError(400, u'Invalid JSON data')
177 177
178 178 if name is None:
179 179 try:
180 180 name = nb.metadata.name
181 181 except AttributeError:
182 182 raise web.HTTPError(400, u'Missing notebook name')
183 183 nb.metadata.name = name
184 184
185 185 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
186 186 return notebook_name
187 187
188 188 def save_notebook(self, data, notebook_path=None, name=None, new_name=None, format=u'json'):
189 189 """Save an existing notebook by notebook_name."""
190 190 if format not in self.allowed_formats:
191 191 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
192 192
193 193 try:
194 194 nb = current.reads(data.decode('utf-8'), format)
195 195 except:
196 196 raise web.HTTPError(400, u'Invalid JSON data')
197 197
198 198 if name is not None:
199 199 nb.metadata.name = name
200 200 self.write_notebook_object(nb, name, notebook_path, new_name)
201 201
202 202 def write_notebook_object(self, nb, notebook_name=None, notebook_path=None, new_name=None):
203 203 """Write a notebook object and return its notebook_name.
204 204
205 205 If notebook_name is None, this method should create a new notebook_name.
206 206 If notebook_name is not None, this method should check to make sure it
207 207 exists and is valid.
208 208 """
209 209 raise NotImplementedError('must be implemented in a subclass')
210 210
211 211 def delete_notebook(self, notebook_name, notebook_path):
212 212 """Delete notebook by notebook_id."""
213 213 raise NotImplementedError('must be implemented in a subclass')
214 214
215 215 def increment_filename(self, name):
216 216 """Increment a filename to make it unique.
217 217
218 218 This exists for notebook stores that must have unique names. When a notebook
219 219 is created or copied this method constructs a unique filename, typically
220 220 by appending an integer to the name.
221 221 """
222 222 return name
223 223
224 224 def new_notebook(self, notebook_path=None):
225 225 """Create a new notebook and return its notebook_id."""
226 226 name = self.increment_filename('Untitled', notebook_path)
227 227 metadata = current.new_metadata(name=name)
228 228 nb = current.new_notebook(metadata=metadata)
229 229 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
230 230 return notebook_name
231 231
232 232 def copy_notebook(self, name, path):
233 233 """Copy an existing notebook and return its notebook_id."""
234 234 last_mod, nb = self.read_notebook_object(name, path)
235 235 name = nb.metadata.name + '-Copy'
236 236 name = self.increment_filename(name, path)
237 237 nb.metadata.name = name
238 238 notebook_name = self.write_notebook_object(nb, notebook_path = path)
239 239 return notebook_name
240 240
241 241 # Checkpoint-related
242 242
243 243 def create_checkpoint(self, notebook_name, notebook_path=None):
244 244 """Create a checkpoint of the current state of a notebook
245 245
246 246 Returns a checkpoint_id for the new checkpoint.
247 247 """
248 248 raise NotImplementedError("must be implemented in a subclass")
249 249
250 250 def list_checkpoints(self, notebook_name, notebook_path=None):
251 251 """Return a list of checkpoints for a given notebook"""
252 252 return []
253 253
254 254 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path=None):
255 255 """Restore a notebook from one of its checkpoints"""
256 256 raise NotImplementedError("must be implemented in a subclass")
257 257
258 258 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path=None):
259 259 """delete a checkpoint for a notebook"""
260 260 raise NotImplementedError("must be implemented in a subclass")
261 261
262 262 def log_info(self):
263 263 self.log.info(self.info_string())
264 264
265 265 def info_string(self):
266 266 return "Serving notebooks"
@@ -1,97 +1,97 b''
1 1 """A base class session manager.
2 2
3 3 Authors:
4 4
5 5 * Zach Sailer
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2013 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import os
20 20 import uuid
21 21
22 22 from tornado import web
23 23
24 24 from IPython.config.configurable import LoggingConfigurable
25 25 from IPython.nbformat import current
26 26 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Classes
30 30 #-----------------------------------------------------------------------------
31 31
32 32 class SessionManager(LoggingConfigurable):
33 33
34 34 # Use session_ids to map notebook names to kernel_ids
35 35 sessions = List()
36 36
37 37 def get_session(self, nb_name, nb_path=None):
38 38 """Get an existing session or create a new one"""
39 39 model = None
40 40 for session in self.sessions:
41 if session['notebook_name'] == nb_name and session['notebook_path'] == nb_path:
42 session_id = session['session_id']
41 if session['name'] == nb_name and session['path'] == nb_path:
42 session_id = session['id']
43 43 model = session
44 44 if model != None:
45 45 return session_id, model
46 46 else:
47 47 session_id = unicode(uuid.uuid4())
48 48 return session_id, model
49 49
50 50 def session_model(self, session_id, notebook_name=None, notebook_path=None, kernel=None):
51 51 """ Create a session that links notebooks with kernels """
52 model = dict(session_id=session_id,
53 notebook_name=notebook_name,
54 notebook_path=notebook_path,
52 model = dict(id=session_id,
53 name=notebook_name,
54 path=notebook_path,
55 55 kernel=kernel)
56 56 if notebook_path == None:
57 model['notebook_path']=""
57 model['path']=""
58 58 self.sessions.append(model)
59 59 return model
60 60
61 61 def list_sessions(self):
62 62 """List all sessions and their information"""
63 63 return self.sessions
64 64
65 65 def set_kernel_for_sessions(self, session_id, kernel_id):
66 66 """Maps the kernel_ids to the session_id in session_mapping"""
67 67 for session in self.sessions:
68 if session['session_id'] == session_id:
69 session['kernel_id'] = kernel_id
68 if session['id'] == session_id:
69 session['kernel']['id'] = kernel_id
70 70 return self.sessions
71 71
72 72 def delete_mapping_for_session(self, session_id):
73 73 """Delete the session from session_mapping with the given session_id"""
74 74 i = 0
75 75 for session in self.sessions:
76 if session['session_id'] == session_id:
76 if session['id'] == session_id:
77 77 del self.sessions[i]
78 78 i = i + 1
79 79 return self.sessions
80 80
81 81 def get_session_from_id(self, session_id):
82 82 for session in self.sessions:
83 if session['session_id'] == session_id:
83 if session['id'] == session_id:
84 84 return session
85 85
86 86 def get_notebook_from_session(self, session_id):
87 87 """Returns the notebook_path for the given session_id"""
88 88 for session in self.sessions:
89 if session['session_id'] == session_id:
90 return session['notebook_name']
89 if session['id'] == session_id:
90 return session['name']
91 91
92 92 def get_kernel_from_session(self, session_id):
93 93 """Returns the kernel_id for the given session_id"""
94 94 for session in self.sessions:
95 if session['session_id'] == session_id:
96 return session['kernel']['kernel_id']
95 if session['id'] == session_id:
96 return session['kernel']['id']
97 97
@@ -1,2153 +1,2143 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Notebook
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13 "use strict";
14 14
15 15 var utils = IPython.utils;
16 16 var key = IPython.utils.keycodes;
17 17
18 18 /**
19 19 * A notebook contains and manages cells.
20 20 *
21 21 * @class Notebook
22 22 * @constructor
23 23 * @param {String} selector A jQuery selector for the notebook's DOM element
24 24 * @param {Object} [options] A config object
25 25 */
26 26 var Notebook = function (selector, options) {
27 27 var options = options || {};
28 28 this._baseProjectUrl = options.baseProjectUrl;
29 29 this.notebook_path = options.notebookPath;
30 30 this.notebook_name = options.notebookName;
31 31 this.element = $(selector);
32 32 this.element.scroll();
33 33 this.element.data("notebook", this);
34 34 this.next_prompt_number = 1;
35 35 this.session = null;
36 36 this.clipboard = null;
37 37 this.undelete_backup = null;
38 38 this.undelete_index = null;
39 39 this.undelete_below = false;
40 40 this.paste_enabled = false;
41 41 this.set_dirty(false);
42 42 this.metadata = {};
43 43 this._checkpoint_after_save = false;
44 44 this.last_checkpoint = null;
45 45 this.checkpoints = [];
46 46 this.autosave_interval = 0;
47 47 this.autosave_timer = null;
48 48 // autosave *at most* every two minutes
49 49 this.minimum_autosave_interval = 120000;
50 50 // single worksheet for now
51 51 this.worksheet_metadata = {};
52 52 this.control_key_active = false;
53 53 this.notebook_name = null;
54 54 this.notebook_name_blacklist_re = /[\/\\:]/;
55 55 this.nbformat = 3 // Increment this when changing the nbformat
56 56 this.nbformat_minor = 0 // Increment this when changing the nbformat
57 57 this.style();
58 58 this.create_elements();
59 59 this.bind_events();
60 60 };
61 61
62 62 /**
63 63 * Tweak the notebook's CSS style.
64 64 *
65 65 * @method style
66 66 */
67 67 Notebook.prototype.style = function () {
68 68 $('div#notebook').addClass('border-box-sizing');
69 69 };
70 70
71 71 /**
72 72 * Get the root URL of the notebook server.
73 73 *
74 74 * @method baseProjectUrl
75 75 * @return {String} The base project URL
76 76 */
77 77 Notebook.prototype.baseProjectUrl = function(){
78 78 return this._baseProjectUrl || $('body').data('baseProjectUrl');
79 79 };
80 80
81 81 Notebook.prototype.notebookName = function() {
82 82 var name = $('body').data('notebookName');
83 83 return name;
84 84 };
85 85
86 86 Notebook.prototype.notebookPath = function() {
87 87 var path = $('body').data('notebookPath');
88 88 path = decodeURIComponent(path);
89 89 if (path != 'None') {
90 90 if (path[path.length-1] != '/') {
91 91 path = path.substring(0,path.length);
92 92 };
93 93 return path;
94 94 } else {
95 95 return '';
96 96 }
97 97 };
98 98
99 99 /**
100 100 * Create an HTML and CSS representation of the notebook.
101 101 *
102 102 * @method create_elements
103 103 */
104 104 Notebook.prototype.create_elements = function () {
105 105 // We add this end_space div to the end of the notebook div to:
106 106 // i) provide a margin between the last cell and the end of the notebook
107 107 // ii) to prevent the div from scrolling up when the last cell is being
108 108 // edited, but is too low on the page, which browsers will do automatically.
109 109 var that = this;
110 110 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
111 111 var end_space = $('<div/>').addClass('end_space');
112 112 end_space.dblclick(function (e) {
113 113 var ncells = that.ncells();
114 114 that.insert_cell_below('code',ncells-1);
115 115 });
116 116 this.element.append(this.container);
117 117 this.container.append(end_space);
118 118 $('div#notebook').addClass('border-box-sizing');
119 119 };
120 120
121 121 /**
122 122 * Bind JavaScript events: key presses and custom IPython events.
123 123 *
124 124 * @method bind_events
125 125 */
126 126 Notebook.prototype.bind_events = function () {
127 127 var that = this;
128 128
129 129 $([IPython.events]).on('set_next_input.Notebook', function (event, data) {
130 130 var index = that.find_cell_index(data.cell);
131 131 var new_cell = that.insert_cell_below('code',index);
132 132 new_cell.set_text(data.text);
133 133 that.dirty = true;
134 134 });
135 135
136 136 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
137 137 that.dirty = data.value;
138 138 });
139 139
140 140 $([IPython.events]).on('select.Cell', function (event, data) {
141 141 var index = that.find_cell_index(data.cell);
142 142 that.select(index);
143 143 });
144 144
145 145 $([IPython.events]).on('status_autorestarting.Kernel', function () {
146 146 IPython.dialog.modal({
147 147 title: "Kernel Restarting",
148 148 body: "The kernel appears to have died. It will restart automatically.",
149 149 buttons: {
150 150 OK : {
151 151 class : "btn-primary"
152 152 }
153 153 }
154 154 });
155 155 });
156 156
157 157
158 158 $(document).keydown(function (event) {
159 159
160 160 // Save (CTRL+S) or (AppleKey+S)
161 161 //metaKey = applekey on mac
162 162 if ((event.ctrlKey || event.metaKey) && event.keyCode==83) {
163 163 that.save_checkpoint();
164 164 event.preventDefault();
165 165 return false;
166 166 } else if (event.which === key.ESC) {
167 167 // Intercept escape at highest level to avoid closing
168 168 // websocket connection with firefox
169 169 IPython.pager.collapse();
170 170 event.preventDefault();
171 171 } else if (event.which === key.SHIFT) {
172 172 // ignore shift keydown
173 173 return true;
174 174 }
175 175 if (event.which === key.UPARROW && !event.shiftKey) {
176 176 var cell = that.get_selected_cell();
177 177 if (cell && cell.at_top()) {
178 178 event.preventDefault();
179 179 that.select_prev();
180 180 };
181 181 } else if (event.which === key.DOWNARROW && !event.shiftKey) {
182 182 var cell = that.get_selected_cell();
183 183 if (cell && cell.at_bottom()) {
184 184 event.preventDefault();
185 185 that.select_next();
186 186 };
187 187 } else if (event.which === key.ENTER && event.shiftKey) {
188 188 that.execute_selected_cell();
189 189 return false;
190 190 } else if (event.which === key.ENTER && event.altKey) {
191 191 // Execute code cell, and insert new in place
192 192 that.execute_selected_cell();
193 193 // Only insert a new cell, if we ended up in an already populated cell
194 194 if (/\S/.test(that.get_selected_cell().get_text()) == true) {
195 195 that.insert_cell_above('code');
196 196 }
197 197 return false;
198 198 } else if (event.which === key.ENTER && event.ctrlKey) {
199 199 that.execute_selected_cell({terminal:true});
200 200 return false;
201 201 } else if (event.which === 77 && event.ctrlKey && that.control_key_active == false) {
202 202 that.control_key_active = true;
203 203 return false;
204 204 } else if (event.which === 88 && that.control_key_active) {
205 205 // Cut selected cell = x
206 206 that.cut_cell();
207 207 that.control_key_active = false;
208 208 return false;
209 209 } else if (event.which === 67 && that.control_key_active) {
210 210 // Copy selected cell = c
211 211 that.copy_cell();
212 212 that.control_key_active = false;
213 213 return false;
214 214 } else if (event.which === 86 && that.control_key_active) {
215 215 // Paste below selected cell = v
216 216 that.paste_cell_below();
217 217 that.control_key_active = false;
218 218 return false;
219 219 } else if (event.which === 68 && that.control_key_active) {
220 220 // Delete selected cell = d
221 221 that.delete_cell();
222 222 that.control_key_active = false;
223 223 return false;
224 224 } else if (event.which === 65 && that.control_key_active) {
225 225 // Insert code cell above selected = a
226 226 that.insert_cell_above('code');
227 227 that.control_key_active = false;
228 228 return false;
229 229 } else if (event.which === 66 && that.control_key_active) {
230 230 // Insert code cell below selected = b
231 231 that.insert_cell_below('code');
232 232 that.control_key_active = false;
233 233 return false;
234 234 } else if (event.which === 89 && that.control_key_active) {
235 235 // To code = y
236 236 that.to_code();
237 237 that.control_key_active = false;
238 238 return false;
239 239 } else if (event.which === 77 && that.control_key_active) {
240 240 // To markdown = m
241 241 that.to_markdown();
242 242 that.control_key_active = false;
243 243 return false;
244 244 } else if (event.which === 84 && that.control_key_active) {
245 245 // To Raw = t
246 246 that.to_raw();
247 247 that.control_key_active = false;
248 248 return false;
249 249 } else if (event.which === 49 && that.control_key_active) {
250 250 // To Heading 1 = 1
251 251 that.to_heading(undefined, 1);
252 252 that.control_key_active = false;
253 253 return false;
254 254 } else if (event.which === 50 && that.control_key_active) {
255 255 // To Heading 2 = 2
256 256 that.to_heading(undefined, 2);
257 257 that.control_key_active = false;
258 258 return false;
259 259 } else if (event.which === 51 && that.control_key_active) {
260 260 // To Heading 3 = 3
261 261 that.to_heading(undefined, 3);
262 262 that.control_key_active = false;
263 263 return false;
264 264 } else if (event.which === 52 && that.control_key_active) {
265 265 // To Heading 4 = 4
266 266 that.to_heading(undefined, 4);
267 267 that.control_key_active = false;
268 268 return false;
269 269 } else if (event.which === 53 && that.control_key_active) {
270 270 // To Heading 5 = 5
271 271 that.to_heading(undefined, 5);
272 272 that.control_key_active = false;
273 273 return false;
274 274 } else if (event.which === 54 && that.control_key_active) {
275 275 // To Heading 6 = 6
276 276 that.to_heading(undefined, 6);
277 277 that.control_key_active = false;
278 278 return false;
279 279 } else if (event.which === 79 && that.control_key_active) {
280 280 // Toggle output = o
281 281 if (event.shiftKey){
282 282 that.toggle_output_scroll();
283 283 } else {
284 284 that.toggle_output();
285 285 }
286 286 that.control_key_active = false;
287 287 return false;
288 288 } else if (event.which === 83 && that.control_key_active) {
289 289 // Save notebook = s
290 290 that.save_checkpoint();
291 291 that.control_key_active = false;
292 292 return false;
293 293 } else if (event.which === 74 && that.control_key_active) {
294 294 // Move cell down = j
295 295 that.move_cell_down();
296 296 that.control_key_active = false;
297 297 return false;
298 298 } else if (event.which === 75 && that.control_key_active) {
299 299 // Move cell up = k
300 300 that.move_cell_up();
301 301 that.control_key_active = false;
302 302 return false;
303 303 } else if (event.which === 80 && that.control_key_active) {
304 304 // Select previous = p
305 305 that.select_prev();
306 306 that.control_key_active = false;
307 307 return false;
308 308 } else if (event.which === 78 && that.control_key_active) {
309 309 // Select next = n
310 310 that.select_next();
311 311 that.control_key_active = false;
312 312 return false;
313 313 } else if (event.which === 76 && that.control_key_active) {
314 314 // Toggle line numbers = l
315 315 that.cell_toggle_line_numbers();
316 316 that.control_key_active = false;
317 317 return false;
318 318 } else if (event.which === 73 && that.control_key_active) {
319 319 // Interrupt kernel = i
320 320 that.session.interrupt_kernel();
321 321 that.control_key_active = false;
322 322 return false;
323 323 } else if (event.which === 190 && that.control_key_active) {
324 324 // Restart kernel = . # matches qt console
325 325 that.restart_kernel();
326 326 that.control_key_active = false;
327 327 return false;
328 328 } else if (event.which === 72 && that.control_key_active) {
329 329 // Show keyboard shortcuts = h
330 330 IPython.quick_help.show_keyboard_shortcuts();
331 331 that.control_key_active = false;
332 332 return false;
333 333 } else if (event.which === 90 && that.control_key_active) {
334 334 // Undo last cell delete = z
335 335 that.undelete();
336 336 that.control_key_active = false;
337 337 return false;
338 338 } else if ((event.which === 189 || event.which === 173) &&
339 339 that.control_key_active) {
340 340 // how fun! '-' is 189 in Chrome, but 173 in FF and Opera
341 341 // Split cell = -
342 342 that.split_cell();
343 343 that.control_key_active = false;
344 344 return false;
345 345 } else if (that.control_key_active) {
346 346 that.control_key_active = false;
347 347 return true;
348 348 }
349 349 return true;
350 350 });
351 351
352 352 var collapse_time = function(time){
353 353 var app_height = $('#ipython-main-app').height(); // content height
354 354 var splitter_height = $('div#pager_splitter').outerHeight(true);
355 355 var new_height = app_height - splitter_height;
356 356 that.element.animate({height : new_height + 'px'}, time);
357 357 }
358 358
359 359 this.element.bind('collapse_pager', function (event,extrap) {
360 360 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
361 361 collapse_time(time);
362 362 });
363 363
364 364 var expand_time = function(time) {
365 365 var app_height = $('#ipython-main-app').height(); // content height
366 366 var splitter_height = $('div#pager_splitter').outerHeight(true);
367 367 var pager_height = $('div#pager').outerHeight(true);
368 368 var new_height = app_height - pager_height - splitter_height;
369 369 that.element.animate({height : new_height + 'px'}, time);
370 370 }
371 371
372 372 this.element.bind('expand_pager', function (event, extrap) {
373 373 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
374 374 expand_time(time);
375 375 });
376 376
377 377 // Firefox 22 broke $(window).on("beforeunload")
378 378 // I'm not sure why or how.
379 379 window.onbeforeunload = function (e) {
380 380 // TODO: Make killing the kernel configurable.
381 381 var kill_kernel = false;
382 382 if (kill_kernel) {
383 383 that.session.kill_kernel();
384 384 }
385 385 // if we are autosaving, trigger an autosave on nav-away.
386 386 // still warn, because if we don't the autosave may fail.
387 387 if (that.dirty) {
388 388 if ( that.autosave_interval ) {
389 389 // schedule autosave in a timeout
390 390 // this gives you a chance to forcefully discard changes
391 391 // by reloading the page if you *really* want to.
392 392 // the timer doesn't start until you *dismiss* the dialog.
393 393 setTimeout(function () {
394 394 if (that.dirty) {
395 395 that.save_notebook();
396 396 }
397 397 }, 1000);
398 398 return "Autosave in progress, latest changes may be lost.";
399 399 } else {
400 400 return "Unsaved changes will be lost.";
401 401 }
402 402 };
403 403 // Null is the *only* return value that will make the browser not
404 404 // pop up the "don't leave" dialog.
405 405 return null;
406 406 };
407 407 };
408 408
409 409 /**
410 410 * Set the dirty flag, and trigger the set_dirty.Notebook event
411 411 *
412 412 * @method set_dirty
413 413 */
414 414 Notebook.prototype.set_dirty = function (value) {
415 415 if (value === undefined) {
416 416 value = true;
417 417 }
418 418 if (this.dirty == value) {
419 419 return;
420 420 }
421 421 $([IPython.events]).trigger('set_dirty.Notebook', {value: value});
422 422 };
423 423
424 424 /**
425 425 * Scroll the top of the page to a given cell.
426 426 *
427 427 * @method scroll_to_cell
428 428 * @param {Number} cell_number An index of the cell to view
429 429 * @param {Number} time Animation time in milliseconds
430 430 * @return {Number} Pixel offset from the top of the container
431 431 */
432 432 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
433 433 var cells = this.get_cells();
434 434 var time = time || 0;
435 435 cell_number = Math.min(cells.length-1,cell_number);
436 436 cell_number = Math.max(0 ,cell_number);
437 437 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
438 438 this.element.animate({scrollTop:scroll_value}, time);
439 439 return scroll_value;
440 440 };
441 441
442 442 /**
443 443 * Scroll to the bottom of the page.
444 444 *
445 445 * @method scroll_to_bottom
446 446 */
447 447 Notebook.prototype.scroll_to_bottom = function () {
448 448 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
449 449 };
450 450
451 451 /**
452 452 * Scroll to the top of the page.
453 453 *
454 454 * @method scroll_to_top
455 455 */
456 456 Notebook.prototype.scroll_to_top = function () {
457 457 this.element.animate({scrollTop:0}, 0);
458 458 };
459 459
460 460 // Edit Notebook metadata
461 461
462 462 Notebook.prototype.edit_metadata = function () {
463 463 var that = this;
464 464 IPython.dialog.edit_metadata(this.metadata, function (md) {
465 465 that.metadata = md;
466 466 }, 'Notebook');
467 467 };
468 468
469 469 // Cell indexing, retrieval, etc.
470 470
471 471 /**
472 472 * Get all cell elements in the notebook.
473 473 *
474 474 * @method get_cell_elements
475 475 * @return {jQuery} A selector of all cell elements
476 476 */
477 477 Notebook.prototype.get_cell_elements = function () {
478 478 return this.container.children("div.cell");
479 479 };
480 480
481 481 /**
482 482 * Get a particular cell element.
483 483 *
484 484 * @method get_cell_element
485 485 * @param {Number} index An index of a cell to select
486 486 * @return {jQuery} A selector of the given cell.
487 487 */
488 488 Notebook.prototype.get_cell_element = function (index) {
489 489 var result = null;
490 490 var e = this.get_cell_elements().eq(index);
491 491 if (e.length !== 0) {
492 492 result = e;
493 493 }
494 494 return result;
495 495 };
496 496
497 497 /**
498 498 * Count the cells in this notebook.
499 499 *
500 500 * @method ncells
501 501 * @return {Number} The number of cells in this notebook
502 502 */
503 503 Notebook.prototype.ncells = function () {
504 504 return this.get_cell_elements().length;
505 505 };
506 506
507 507 /**
508 508 * Get all Cell objects in this notebook.
509 509 *
510 510 * @method get_cells
511 511 * @return {Array} This notebook's Cell objects
512 512 */
513 513 // TODO: we are often calling cells as cells()[i], which we should optimize
514 514 // to cells(i) or a new method.
515 515 Notebook.prototype.get_cells = function () {
516 516 return this.get_cell_elements().toArray().map(function (e) {
517 517 return $(e).data("cell");
518 518 });
519 519 };
520 520
521 521 /**
522 522 * Get a Cell object from this notebook.
523 523 *
524 524 * @method get_cell
525 525 * @param {Number} index An index of a cell to retrieve
526 526 * @return {Cell} A particular cell
527 527 */
528 528 Notebook.prototype.get_cell = function (index) {
529 529 var result = null;
530 530 var ce = this.get_cell_element(index);
531 531 if (ce !== null) {
532 532 result = ce.data('cell');
533 533 }
534 534 return result;
535 535 }
536 536
537 537 /**
538 538 * Get the cell below a given cell.
539 539 *
540 540 * @method get_next_cell
541 541 * @param {Cell} cell The provided cell
542 542 * @return {Cell} The next cell
543 543 */
544 544 Notebook.prototype.get_next_cell = function (cell) {
545 545 var result = null;
546 546 var index = this.find_cell_index(cell);
547 547 if (this.is_valid_cell_index(index+1)) {
548 548 result = this.get_cell(index+1);
549 549 }
550 550 return result;
551 551 }
552 552
553 553 /**
554 554 * Get the cell above a given cell.
555 555 *
556 556 * @method get_prev_cell
557 557 * @param {Cell} cell The provided cell
558 558 * @return {Cell} The previous cell
559 559 */
560 560 Notebook.prototype.get_prev_cell = function (cell) {
561 561 // TODO: off-by-one
562 562 // nb.get_prev_cell(nb.get_cell(1)) is null
563 563 var result = null;
564 564 var index = this.find_cell_index(cell);
565 565 if (index !== null && index > 1) {
566 566 result = this.get_cell(index-1);
567 567 }
568 568 return result;
569 569 }
570 570
571 571 /**
572 572 * Get the numeric index of a given cell.
573 573 *
574 574 * @method find_cell_index
575 575 * @param {Cell} cell The provided cell
576 576 * @return {Number} The cell's numeric index
577 577 */
578 578 Notebook.prototype.find_cell_index = function (cell) {
579 579 var result = null;
580 580 this.get_cell_elements().filter(function (index) {
581 581 if ($(this).data("cell") === cell) {
582 582 result = index;
583 583 };
584 584 });
585 585 return result;
586 586 };
587 587
588 588 /**
589 589 * Get a given index , or the selected index if none is provided.
590 590 *
591 591 * @method index_or_selected
592 592 * @param {Number} index A cell's index
593 593 * @return {Number} The given index, or selected index if none is provided.
594 594 */
595 595 Notebook.prototype.index_or_selected = function (index) {
596 596 var i;
597 597 if (index === undefined || index === null) {
598 598 i = this.get_selected_index();
599 599 if (i === null) {
600 600 i = 0;
601 601 }
602 602 } else {
603 603 i = index;
604 604 }
605 605 return i;
606 606 };
607 607
608 608 /**
609 609 * Get the currently selected cell.
610 610 * @method get_selected_cell
611 611 * @return {Cell} The selected cell
612 612 */
613 613 Notebook.prototype.get_selected_cell = function () {
614 614 var index = this.get_selected_index();
615 615 return this.get_cell(index);
616 616 };
617 617
618 618 /**
619 619 * Check whether a cell index is valid.
620 620 *
621 621 * @method is_valid_cell_index
622 622 * @param {Number} index A cell index
623 623 * @return True if the index is valid, false otherwise
624 624 */
625 625 Notebook.prototype.is_valid_cell_index = function (index) {
626 626 if (index !== null && index >= 0 && index < this.ncells()) {
627 627 return true;
628 628 } else {
629 629 return false;
630 630 };
631 631 }
632 632
633 633 /**
634 634 * Get the index of the currently selected cell.
635 635
636 636 * @method get_selected_index
637 637 * @return {Number} The selected cell's numeric index
638 638 */
639 639 Notebook.prototype.get_selected_index = function () {
640 640 var result = null;
641 641 this.get_cell_elements().filter(function (index) {
642 642 if ($(this).data("cell").selected === true) {
643 643 result = index;
644 644 };
645 645 });
646 646 return result;
647 647 };
648 648
649 649
650 650 // Cell selection.
651 651
652 652 /**
653 653 * Programmatically select a cell.
654 654 *
655 655 * @method select
656 656 * @param {Number} index A cell's index
657 657 * @return {Notebook} This notebook
658 658 */
659 659 Notebook.prototype.select = function (index) {
660 660 if (this.is_valid_cell_index(index)) {
661 661 var sindex = this.get_selected_index()
662 662 if (sindex !== null && index !== sindex) {
663 663 this.get_cell(sindex).unselect();
664 664 };
665 665 var cell = this.get_cell(index);
666 666 cell.select();
667 667 if (cell.cell_type === 'heading') {
668 668 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
669 669 {'cell_type':cell.cell_type,level:cell.level}
670 670 );
671 671 } else {
672 672 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
673 673 {'cell_type':cell.cell_type}
674 674 );
675 675 };
676 676 };
677 677 return this;
678 678 };
679 679
680 680 /**
681 681 * Programmatically select the next cell.
682 682 *
683 683 * @method select_next
684 684 * @return {Notebook} This notebook
685 685 */
686 686 Notebook.prototype.select_next = function () {
687 687 var index = this.get_selected_index();
688 688 this.select(index+1);
689 689 return this;
690 690 };
691 691
692 692 /**
693 693 * Programmatically select the previous cell.
694 694 *
695 695 * @method select_prev
696 696 * @return {Notebook} This notebook
697 697 */
698 698 Notebook.prototype.select_prev = function () {
699 699 var index = this.get_selected_index();
700 700 this.select(index-1);
701 701 return this;
702 702 };
703 703
704 704
705 705 // Cell movement
706 706
707 707 /**
708 708 * Move given (or selected) cell up and select it.
709 709 *
710 710 * @method move_cell_up
711 711 * @param [index] {integer} cell index
712 712 * @return {Notebook} This notebook
713 713 **/
714 714 Notebook.prototype.move_cell_up = function (index) {
715 715 var i = this.index_or_selected(index);
716 716 if (this.is_valid_cell_index(i) && i > 0) {
717 717 var pivot = this.get_cell_element(i-1);
718 718 var tomove = this.get_cell_element(i);
719 719 if (pivot !== null && tomove !== null) {
720 720 tomove.detach();
721 721 pivot.before(tomove);
722 722 this.select(i-1);
723 723 };
724 724 this.set_dirty(true);
725 725 };
726 726 return this;
727 727 };
728 728
729 729
730 730 /**
731 731 * Move given (or selected) cell down and select it
732 732 *
733 733 * @method move_cell_down
734 734 * @param [index] {integer} cell index
735 735 * @return {Notebook} This notebook
736 736 **/
737 737 Notebook.prototype.move_cell_down = function (index) {
738 738 var i = this.index_or_selected(index);
739 739 if ( this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
740 740 var pivot = this.get_cell_element(i+1);
741 741 var tomove = this.get_cell_element(i);
742 742 if (pivot !== null && tomove !== null) {
743 743 tomove.detach();
744 744 pivot.after(tomove);
745 745 this.select(i+1);
746 746 };
747 747 };
748 748 this.set_dirty();
749 749 return this;
750 750 };
751 751
752 752
753 753 // Insertion, deletion.
754 754
755 755 /**
756 756 * Delete a cell from the notebook.
757 757 *
758 758 * @method delete_cell
759 759 * @param [index] A cell's numeric index
760 760 * @return {Notebook} This notebook
761 761 */
762 762 Notebook.prototype.delete_cell = function (index) {
763 763 var i = this.index_or_selected(index);
764 764 var cell = this.get_selected_cell();
765 765 this.undelete_backup = cell.toJSON();
766 766 $('#undelete_cell').removeClass('ui-state-disabled');
767 767 if (this.is_valid_cell_index(i)) {
768 768 var ce = this.get_cell_element(i);
769 769 ce.remove();
770 770 if (i === (this.ncells())) {
771 771 this.select(i-1);
772 772 this.undelete_index = i - 1;
773 773 this.undelete_below = true;
774 774 } else {
775 775 this.select(i);
776 776 this.undelete_index = i;
777 777 this.undelete_below = false;
778 778 };
779 779 $([IPython.events]).trigger('delete.Cell', {'cell': cell, 'index': i});
780 780 this.set_dirty(true);
781 781 };
782 782 return this;
783 783 };
784 784
785 785 /**
786 786 * Insert a cell so that after insertion the cell is at given index.
787 787 *
788 788 * Similar to insert_above, but index parameter is mandatory
789 789 *
790 790 * Index will be brought back into the accissible range [0,n]
791 791 *
792 792 * @method insert_cell_at_index
793 793 * @param type {string} in ['code','markdown','heading']
794 794 * @param [index] {int} a valid index where to inser cell
795 795 *
796 796 * @return cell {cell|null} created cell or null
797 797 **/
798 798 Notebook.prototype.insert_cell_at_index = function(type, index){
799 799
800 800 var ncells = this.ncells();
801 801 var index = Math.min(index,ncells);
802 802 index = Math.max(index,0);
803 803 var cell = null;
804 804
805 805 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
806 806 if (type === 'code') {
807 807 cell = new IPython.CodeCell(this.session);
808 808 cell.set_input_prompt();
809 809 } else if (type === 'markdown') {
810 810 cell = new IPython.MarkdownCell();
811 811 } else if (type === 'raw') {
812 812 cell = new IPython.RawCell();
813 813 } else if (type === 'heading') {
814 814 cell = new IPython.HeadingCell();
815 815 }
816 816
817 817 if(this._insert_element_at_index(cell.element,index)){
818 818 cell.render();
819 819 this.select(this.find_cell_index(cell));
820 820 $([IPython.events]).trigger('create.Cell', {'cell': cell, 'index': index});
821 821 this.set_dirty(true);
822 822 }
823 823 }
824 824 return cell;
825 825
826 826 };
827 827
828 828 /**
829 829 * Insert an element at given cell index.
830 830 *
831 831 * @method _insert_element_at_index
832 832 * @param element {dom element} a cell element
833 833 * @param [index] {int} a valid index where to inser cell
834 834 * @private
835 835 *
836 836 * return true if everything whent fine.
837 837 **/
838 838 Notebook.prototype._insert_element_at_index = function(element, index){
839 839 if (element === undefined){
840 840 return false;
841 841 }
842 842
843 843 var ncells = this.ncells();
844 844
845 845 if (ncells === 0) {
846 846 // special case append if empty
847 847 this.element.find('div.end_space').before(element);
848 848 } else if ( ncells === index ) {
849 849 // special case append it the end, but not empty
850 850 this.get_cell_element(index-1).after(element);
851 851 } else if (this.is_valid_cell_index(index)) {
852 852 // otherwise always somewhere to append to
853 853 this.get_cell_element(index).before(element);
854 854 } else {
855 855 return false;
856 856 }
857 857
858 858 if (this.undelete_index !== null && index <= this.undelete_index) {
859 859 this.undelete_index = this.undelete_index + 1;
860 860 this.set_dirty(true);
861 861 }
862 862 return true;
863 863 };
864 864
865 865 /**
866 866 * Insert a cell of given type above given index, or at top
867 867 * of notebook if index smaller than 0.
868 868 *
869 869 * default index value is the one of currently selected cell
870 870 *
871 871 * @method insert_cell_above
872 872 * @param type {string} cell type
873 873 * @param [index] {integer}
874 874 *
875 875 * @return handle to created cell or null
876 876 **/
877 877 Notebook.prototype.insert_cell_above = function (type, index) {
878 878 index = this.index_or_selected(index);
879 879 return this.insert_cell_at_index(type, index);
880 880 };
881 881
882 882 /**
883 883 * Insert a cell of given type below given index, or at bottom
884 884 * of notebook if index greater thatn number of cell
885 885 *
886 886 * default index value is the one of currently selected cell
887 887 *
888 888 * @method insert_cell_below
889 889 * @param type {string} cell type
890 890 * @param [index] {integer}
891 891 *
892 892 * @return handle to created cell or null
893 893 *
894 894 **/
895 895 Notebook.prototype.insert_cell_below = function (type, index) {
896 896 index = this.index_or_selected(index);
897 897 return this.insert_cell_at_index(type, index+1);
898 898 };
899 899
900 900
901 901 /**
902 902 * Insert cell at end of notebook
903 903 *
904 904 * @method insert_cell_at_bottom
905 905 * @param {String} type cell type
906 906 *
907 907 * @return the added cell; or null
908 908 **/
909 909 Notebook.prototype.insert_cell_at_bottom = function (type){
910 910 var len = this.ncells();
911 911 return this.insert_cell_below(type,len-1);
912 912 };
913 913
914 914 /**
915 915 * Turn a cell into a code cell.
916 916 *
917 917 * @method to_code
918 918 * @param {Number} [index] A cell's index
919 919 */
920 920 Notebook.prototype.to_code = function (index) {
921 921 var i = this.index_or_selected(index);
922 922 if (this.is_valid_cell_index(i)) {
923 923 var source_element = this.get_cell_element(i);
924 924 var source_cell = source_element.data("cell");
925 925 if (!(source_cell instanceof IPython.CodeCell)) {
926 926 var target_cell = this.insert_cell_below('code',i);
927 927 var text = source_cell.get_text();
928 928 if (text === source_cell.placeholder) {
929 929 text = '';
930 930 }
931 931 target_cell.set_text(text);
932 932 // make this value the starting point, so that we can only undo
933 933 // to this state, instead of a blank cell
934 934 target_cell.code_mirror.clearHistory();
935 935 source_element.remove();
936 936 this.set_dirty(true);
937 937 };
938 938 };
939 939 };
940 940
941 941 /**
942 942 * Turn a cell into a Markdown cell.
943 943 *
944 944 * @method to_markdown
945 945 * @param {Number} [index] A cell's index
946 946 */
947 947 Notebook.prototype.to_markdown = function (index) {
948 948 var i = this.index_or_selected(index);
949 949 if (this.is_valid_cell_index(i)) {
950 950 var source_element = this.get_cell_element(i);
951 951 var source_cell = source_element.data("cell");
952 952 if (!(source_cell instanceof IPython.MarkdownCell)) {
953 953 var target_cell = this.insert_cell_below('markdown',i);
954 954 var text = source_cell.get_text();
955 955 if (text === source_cell.placeholder) {
956 956 text = '';
957 957 };
958 958 // The edit must come before the set_text.
959 959 target_cell.edit();
960 960 target_cell.set_text(text);
961 961 // make this value the starting point, so that we can only undo
962 962 // to this state, instead of a blank cell
963 963 target_cell.code_mirror.clearHistory();
964 964 source_element.remove();
965 965 this.set_dirty(true);
966 966 };
967 967 };
968 968 };
969 969
970 970 /**
971 971 * Turn a cell into a raw text cell.
972 972 *
973 973 * @method to_raw
974 974 * @param {Number} [index] A cell's index
975 975 */
976 976 Notebook.prototype.to_raw = function (index) {
977 977 var i = this.index_or_selected(index);
978 978 if (this.is_valid_cell_index(i)) {
979 979 var source_element = this.get_cell_element(i);
980 980 var source_cell = source_element.data("cell");
981 981 var target_cell = null;
982 982 if (!(source_cell instanceof IPython.RawCell)) {
983 983 target_cell = this.insert_cell_below('raw',i);
984 984 var text = source_cell.get_text();
985 985 if (text === source_cell.placeholder) {
986 986 text = '';
987 987 };
988 988 // The edit must come before the set_text.
989 989 target_cell.edit();
990 990 target_cell.set_text(text);
991 991 // make this value the starting point, so that we can only undo
992 992 // to this state, instead of a blank cell
993 993 target_cell.code_mirror.clearHistory();
994 994 source_element.remove();
995 995 this.set_dirty(true);
996 996 };
997 997 };
998 998 };
999 999
1000 1000 /**
1001 1001 * Turn a cell into a heading cell.
1002 1002 *
1003 1003 * @method to_heading
1004 1004 * @param {Number} [index] A cell's index
1005 1005 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
1006 1006 */
1007 1007 Notebook.prototype.to_heading = function (index, level) {
1008 1008 level = level || 1;
1009 1009 var i = this.index_or_selected(index);
1010 1010 if (this.is_valid_cell_index(i)) {
1011 1011 var source_element = this.get_cell_element(i);
1012 1012 var source_cell = source_element.data("cell");
1013 1013 var target_cell = null;
1014 1014 if (source_cell instanceof IPython.HeadingCell) {
1015 1015 source_cell.set_level(level);
1016 1016 } else {
1017 1017 target_cell = this.insert_cell_below('heading',i);
1018 1018 var text = source_cell.get_text();
1019 1019 if (text === source_cell.placeholder) {
1020 1020 text = '';
1021 1021 };
1022 1022 // The edit must come before the set_text.
1023 1023 target_cell.set_level(level);
1024 1024 target_cell.edit();
1025 1025 target_cell.set_text(text);
1026 1026 // make this value the starting point, so that we can only undo
1027 1027 // to this state, instead of a blank cell
1028 1028 target_cell.code_mirror.clearHistory();
1029 1029 source_element.remove();
1030 1030 this.set_dirty(true);
1031 1031 };
1032 1032 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
1033 1033 {'cell_type':'heading',level:level}
1034 1034 );
1035 1035 };
1036 1036 };
1037 1037
1038 1038
1039 1039 // Cut/Copy/Paste
1040 1040
1041 1041 /**
1042 1042 * Enable UI elements for pasting cells.
1043 1043 *
1044 1044 * @method enable_paste
1045 1045 */
1046 1046 Notebook.prototype.enable_paste = function () {
1047 1047 var that = this;
1048 1048 if (!this.paste_enabled) {
1049 1049 $('#paste_cell_replace').removeClass('ui-state-disabled')
1050 1050 .on('click', function () {that.paste_cell_replace();});
1051 1051 $('#paste_cell_above').removeClass('ui-state-disabled')
1052 1052 .on('click', function () {that.paste_cell_above();});
1053 1053 $('#paste_cell_below').removeClass('ui-state-disabled')
1054 1054 .on('click', function () {that.paste_cell_below();});
1055 1055 this.paste_enabled = true;
1056 1056 };
1057 1057 };
1058 1058
1059 1059 /**
1060 1060 * Disable UI elements for pasting cells.
1061 1061 *
1062 1062 * @method disable_paste
1063 1063 */
1064 1064 Notebook.prototype.disable_paste = function () {
1065 1065 if (this.paste_enabled) {
1066 1066 $('#paste_cell_replace').addClass('ui-state-disabled').off('click');
1067 1067 $('#paste_cell_above').addClass('ui-state-disabled').off('click');
1068 1068 $('#paste_cell_below').addClass('ui-state-disabled').off('click');
1069 1069 this.paste_enabled = false;
1070 1070 };
1071 1071 };
1072 1072
1073 1073 /**
1074 1074 * Cut a cell.
1075 1075 *
1076 1076 * @method cut_cell
1077 1077 */
1078 1078 Notebook.prototype.cut_cell = function () {
1079 1079 this.copy_cell();
1080 1080 this.delete_cell();
1081 1081 }
1082 1082
1083 1083 /**
1084 1084 * Copy a cell.
1085 1085 *
1086 1086 * @method copy_cell
1087 1087 */
1088 1088 Notebook.prototype.copy_cell = function () {
1089 1089 var cell = this.get_selected_cell();
1090 1090 this.clipboard = cell.toJSON();
1091 1091 this.enable_paste();
1092 1092 };
1093 1093
1094 1094 /**
1095 1095 * Replace the selected cell with a cell in the clipboard.
1096 1096 *
1097 1097 * @method paste_cell_replace
1098 1098 */
1099 1099 Notebook.prototype.paste_cell_replace = function () {
1100 1100 if (this.clipboard !== null && this.paste_enabled) {
1101 1101 var cell_data = this.clipboard;
1102 1102 var new_cell = this.insert_cell_above(cell_data.cell_type);
1103 1103 new_cell.fromJSON(cell_data);
1104 1104 var old_cell = this.get_next_cell(new_cell);
1105 1105 this.delete_cell(this.find_cell_index(old_cell));
1106 1106 this.select(this.find_cell_index(new_cell));
1107 1107 };
1108 1108 };
1109 1109
1110 1110 /**
1111 1111 * Paste a cell from the clipboard above the selected cell.
1112 1112 *
1113 1113 * @method paste_cell_above
1114 1114 */
1115 1115 Notebook.prototype.paste_cell_above = function () {
1116 1116 if (this.clipboard !== null && this.paste_enabled) {
1117 1117 var cell_data = this.clipboard;
1118 1118 var new_cell = this.insert_cell_above(cell_data.cell_type);
1119 1119 new_cell.fromJSON(cell_data);
1120 1120 };
1121 1121 };
1122 1122
1123 1123 /**
1124 1124 * Paste a cell from the clipboard below the selected cell.
1125 1125 *
1126 1126 * @method paste_cell_below
1127 1127 */
1128 1128 Notebook.prototype.paste_cell_below = function () {
1129 1129 if (this.clipboard !== null && this.paste_enabled) {
1130 1130 var cell_data = this.clipboard;
1131 1131 var new_cell = this.insert_cell_below(cell_data.cell_type);
1132 1132 new_cell.fromJSON(cell_data);
1133 1133 };
1134 1134 };
1135 1135
1136 1136 // Cell undelete
1137 1137
1138 1138 /**
1139 1139 * Restore the most recently deleted cell.
1140 1140 *
1141 1141 * @method undelete
1142 1142 */
1143 1143 Notebook.prototype.undelete = function() {
1144 1144 if (this.undelete_backup !== null && this.undelete_index !== null) {
1145 1145 var current_index = this.get_selected_index();
1146 1146 if (this.undelete_index < current_index) {
1147 1147 current_index = current_index + 1;
1148 1148 }
1149 1149 if (this.undelete_index >= this.ncells()) {
1150 1150 this.select(this.ncells() - 1);
1151 1151 }
1152 1152 else {
1153 1153 this.select(this.undelete_index);
1154 1154 }
1155 1155 var cell_data = this.undelete_backup;
1156 1156 var new_cell = null;
1157 1157 if (this.undelete_below) {
1158 1158 new_cell = this.insert_cell_below(cell_data.cell_type);
1159 1159 } else {
1160 1160 new_cell = this.insert_cell_above(cell_data.cell_type);
1161 1161 }
1162 1162 new_cell.fromJSON(cell_data);
1163 1163 this.select(current_index);
1164 1164 this.undelete_backup = null;
1165 1165 this.undelete_index = null;
1166 1166 }
1167 1167 $('#undelete_cell').addClass('ui-state-disabled');
1168 1168 }
1169 1169
1170 1170 // Split/merge
1171 1171
1172 1172 /**
1173 1173 * Split the selected cell into two, at the cursor.
1174 1174 *
1175 1175 * @method split_cell
1176 1176 */
1177 1177 Notebook.prototype.split_cell = function () {
1178 1178 // Todo: implement spliting for other cell types.
1179 1179 var cell = this.get_selected_cell();
1180 1180 if (cell.is_splittable()) {
1181 1181 var texta = cell.get_pre_cursor();
1182 1182 var textb = cell.get_post_cursor();
1183 1183 if (cell instanceof IPython.CodeCell) {
1184 1184 cell.set_text(texta);
1185 1185 var new_cell = this.insert_cell_below('code');
1186 1186 new_cell.set_text(textb);
1187 1187 } else if (cell instanceof IPython.MarkdownCell) {
1188 1188 cell.set_text(texta);
1189 1189 cell.render();
1190 1190 var new_cell = this.insert_cell_below('markdown');
1191 1191 new_cell.edit(); // editor must be visible to call set_text
1192 1192 new_cell.set_text(textb);
1193 1193 new_cell.render();
1194 1194 }
1195 1195 };
1196 1196 };
1197 1197
1198 1198 /**
1199 1199 * Combine the selected cell into the cell above it.
1200 1200 *
1201 1201 * @method merge_cell_above
1202 1202 */
1203 1203 Notebook.prototype.merge_cell_above = function () {
1204 1204 var index = this.get_selected_index();
1205 1205 var cell = this.get_cell(index);
1206 1206 if (!cell.is_mergeable()) {
1207 1207 return;
1208 1208 }
1209 1209 if (index > 0) {
1210 1210 var upper_cell = this.get_cell(index-1);
1211 1211 if (!upper_cell.is_mergeable()) {
1212 1212 return;
1213 1213 }
1214 1214 var upper_text = upper_cell.get_text();
1215 1215 var text = cell.get_text();
1216 1216 if (cell instanceof IPython.CodeCell) {
1217 1217 cell.set_text(upper_text+'\n'+text);
1218 1218 } else if (cell instanceof IPython.MarkdownCell) {
1219 1219 cell.edit();
1220 1220 cell.set_text(upper_text+'\n'+text);
1221 1221 cell.render();
1222 1222 };
1223 1223 this.delete_cell(index-1);
1224 1224 this.select(this.find_cell_index(cell));
1225 1225 };
1226 1226 };
1227 1227
1228 1228 /**
1229 1229 * Combine the selected cell into the cell below it.
1230 1230 *
1231 1231 * @method merge_cell_below
1232 1232 */
1233 1233 Notebook.prototype.merge_cell_below = function () {
1234 1234 var index = this.get_selected_index();
1235 1235 var cell = this.get_cell(index);
1236 1236 if (!cell.is_mergeable()) {
1237 1237 return;
1238 1238 }
1239 1239 if (index < this.ncells()-1) {
1240 1240 var lower_cell = this.get_cell(index+1);
1241 1241 if (!lower_cell.is_mergeable()) {
1242 1242 return;
1243 1243 }
1244 1244 var lower_text = lower_cell.get_text();
1245 1245 var text = cell.get_text();
1246 1246 if (cell instanceof IPython.CodeCell) {
1247 1247 cell.set_text(text+'\n'+lower_text);
1248 1248 } else if (cell instanceof IPython.MarkdownCell) {
1249 1249 cell.edit();
1250 1250 cell.set_text(text+'\n'+lower_text);
1251 1251 cell.render();
1252 1252 };
1253 1253 this.delete_cell(index+1);
1254 1254 this.select(this.find_cell_index(cell));
1255 1255 };
1256 1256 };
1257 1257
1258 1258
1259 1259 // Cell collapsing and output clearing
1260 1260
1261 1261 /**
1262 1262 * Hide a cell's output.
1263 1263 *
1264 1264 * @method collapse
1265 1265 * @param {Number} index A cell's numeric index
1266 1266 */
1267 1267 Notebook.prototype.collapse = function (index) {
1268 1268 var i = this.index_or_selected(index);
1269 1269 this.get_cell(i).collapse();
1270 1270 this.set_dirty(true);
1271 1271 };
1272 1272
1273 1273 /**
1274 1274 * Show a cell's output.
1275 1275 *
1276 1276 * @method expand
1277 1277 * @param {Number} index A cell's numeric index
1278 1278 */
1279 1279 Notebook.prototype.expand = function (index) {
1280 1280 var i = this.index_or_selected(index);
1281 1281 this.get_cell(i).expand();
1282 1282 this.set_dirty(true);
1283 1283 };
1284 1284
1285 1285 /** Toggle whether a cell's output is collapsed or expanded.
1286 1286 *
1287 1287 * @method toggle_output
1288 1288 * @param {Number} index A cell's numeric index
1289 1289 */
1290 1290 Notebook.prototype.toggle_output = function (index) {
1291 1291 var i = this.index_or_selected(index);
1292 1292 this.get_cell(i).toggle_output();
1293 1293 this.set_dirty(true);
1294 1294 };
1295 1295
1296 1296 /**
1297 1297 * Toggle a scrollbar for long cell outputs.
1298 1298 *
1299 1299 * @method toggle_output_scroll
1300 1300 * @param {Number} index A cell's numeric index
1301 1301 */
1302 1302 Notebook.prototype.toggle_output_scroll = function (index) {
1303 1303 var i = this.index_or_selected(index);
1304 1304 this.get_cell(i).toggle_output_scroll();
1305 1305 };
1306 1306
1307 1307 /**
1308 1308 * Hide each code cell's output area.
1309 1309 *
1310 1310 * @method collapse_all_output
1311 1311 */
1312 1312 Notebook.prototype.collapse_all_output = function () {
1313 1313 var ncells = this.ncells();
1314 1314 var cells = this.get_cells();
1315 1315 for (var i=0; i<ncells; i++) {
1316 1316 if (cells[i] instanceof IPython.CodeCell) {
1317 1317 cells[i].output_area.collapse();
1318 1318 }
1319 1319 };
1320 1320 // this should not be set if the `collapse` key is removed from nbformat
1321 1321 this.set_dirty(true);
1322 1322 };
1323 1323
1324 1324 /**
1325 1325 * Expand each code cell's output area, and add a scrollbar for long output.
1326 1326 *
1327 1327 * @method scroll_all_output
1328 1328 */
1329 1329 Notebook.prototype.scroll_all_output = function () {
1330 1330 var ncells = this.ncells();
1331 1331 var cells = this.get_cells();
1332 1332 for (var i=0; i<ncells; i++) {
1333 1333 if (cells[i] instanceof IPython.CodeCell) {
1334 1334 cells[i].output_area.expand();
1335 1335 cells[i].output_area.scroll_if_long();
1336 1336 }
1337 1337 };
1338 1338 // this should not be set if the `collapse` key is removed from nbformat
1339 1339 this.set_dirty(true);
1340 1340 };
1341 1341
1342 1342 /**
1343 1343 * Expand each code cell's output area, and remove scrollbars.
1344 1344 *
1345 1345 * @method expand_all_output
1346 1346 */
1347 1347 Notebook.prototype.expand_all_output = function () {
1348 1348 var ncells = this.ncells();
1349 1349 var cells = this.get_cells();
1350 1350 for (var i=0; i<ncells; i++) {
1351 1351 if (cells[i] instanceof IPython.CodeCell) {
1352 1352 cells[i].output_area.expand();
1353 1353 cells[i].output_area.unscroll_area();
1354 1354 }
1355 1355 };
1356 1356 // this should not be set if the `collapse` key is removed from nbformat
1357 1357 this.set_dirty(true);
1358 1358 };
1359 1359
1360 1360 /**
1361 1361 * Clear each code cell's output area.
1362 1362 *
1363 1363 * @method clear_all_output
1364 1364 */
1365 1365 Notebook.prototype.clear_all_output = function () {
1366 1366 var ncells = this.ncells();
1367 1367 var cells = this.get_cells();
1368 1368 for (var i=0; i<ncells; i++) {
1369 1369 if (cells[i] instanceof IPython.CodeCell) {
1370 1370 cells[i].clear_output();
1371 1371 // Make all In[] prompts blank, as well
1372 1372 // TODO: make this configurable (via checkbox?)
1373 1373 cells[i].set_input_prompt();
1374 1374 }
1375 1375 };
1376 1376 this.set_dirty(true);
1377 1377 };
1378 1378
1379 1379
1380 1380 // Other cell functions: line numbers, ...
1381 1381
1382 1382 /**
1383 1383 * Toggle line numbers in the selected cell's input area.
1384 1384 *
1385 1385 * @method cell_toggle_line_numbers
1386 1386 */
1387 1387 Notebook.prototype.cell_toggle_line_numbers = function() {
1388 1388 this.get_selected_cell().toggle_line_numbers();
1389 1389 };
1390 1390
1391 1391 // Session related things
1392 1392
1393 1393 /**
1394 1394 * Start a new session and set it on each code cell.
1395 1395 *
1396 1396 * @method start_session
1397 1397 */
1398 1398 Notebook.prototype.start_session = function () {
1399 1399 var notebook_info = this.notebookPath() + this.notebook_name;
1400 1400 this.session = new IPython.Session(notebook_info, this);
1401 1401 this.session.start();
1402 1402 this.link_cells_to_session();
1403 1403 };
1404 1404
1405 1405
1406 1406 /**
1407 1407 * Once a session is started, link the code cells to the session
1408 1408 *
1409 1409 */
1410 1410 Notebook.prototype.link_cells_to_session= function(){
1411 1411 var ncells = this.ncells();
1412 1412 for (var i=0; i<ncells; i++) {
1413 1413 var cell = this.get_cell(i);
1414 1414 if (cell instanceof IPython.CodeCell) {
1415 1415 cell.set_session(this.session);
1416 1416 };
1417 1417 };
1418 1418 };
1419 1419
1420 1420 /**
1421 1421 * Prompt the user to restart the IPython kernel.
1422 1422 *
1423 1423 * @method restart_kernel
1424 1424 */
1425 1425 Notebook.prototype.restart_kernel = function () {
1426 1426 var that = this;
1427 1427 IPython.dialog.modal({
1428 1428 title : "Restart kernel or continue running?",
1429 1429 body : $("<p/>").html(
1430 1430 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1431 1431 ),
1432 1432 buttons : {
1433 1433 "Continue running" : {},
1434 1434 "Restart" : {
1435 1435 "class" : "btn-danger",
1436 1436 "click" : function() {
1437 1437 that.session.restart_kernel();
1438 1438 }
1439 1439 }
1440 1440 }
1441 1441 });
1442 1442 };
1443 1443
1444 1444 /**
1445 1445 * Run the selected cell.
1446 1446 *
1447 1447 * Execute or render cell outputs.
1448 1448 *
1449 1449 * @method execute_selected_cell
1450 1450 * @param {Object} options Customize post-execution behavior
1451 1451 */
1452 1452 Notebook.prototype.execute_selected_cell = function (options) {
1453 1453 // add_new: should a new cell be added if we are at the end of the nb
1454 1454 // terminal: execute in terminal mode, which stays in the current cell
1455 1455 var default_options = {terminal: false, add_new: true};
1456 1456 $.extend(default_options, options);
1457 1457 var that = this;
1458 1458 var cell = that.get_selected_cell();
1459 1459 var cell_index = that.find_cell_index(cell);
1460 1460 if (cell instanceof IPython.CodeCell) {
1461 1461 cell.execute();
1462 1462 }
1463 1463 if (default_options.terminal) {
1464 1464 cell.select_all();
1465 1465 } else {
1466 1466 if ((cell_index === (that.ncells()-1)) && default_options.add_new) {
1467 1467 that.insert_cell_below('code');
1468 1468 // If we are adding a new cell at the end, scroll down to show it.
1469 1469 that.scroll_to_bottom();
1470 1470 } else {
1471 1471 that.select(cell_index+1);
1472 1472 };
1473 1473 };
1474 1474 this.set_dirty(true);
1475 1475 };
1476 1476
1477 1477 /**
1478 1478 * Execute all cells below the selected cell.
1479 1479 *
1480 1480 * @method execute_cells_below
1481 1481 */
1482 1482 Notebook.prototype.execute_cells_below = function () {
1483 1483 this.execute_cell_range(this.get_selected_index(), this.ncells());
1484 1484 this.scroll_to_bottom();
1485 1485 };
1486 1486
1487 1487 /**
1488 1488 * Execute all cells above the selected cell.
1489 1489 *
1490 1490 * @method execute_cells_above
1491 1491 */
1492 1492 Notebook.prototype.execute_cells_above = function () {
1493 1493 this.execute_cell_range(0, this.get_selected_index());
1494 1494 };
1495 1495
1496 1496 /**
1497 1497 * Execute all cells.
1498 1498 *
1499 1499 * @method execute_all_cells
1500 1500 */
1501 1501 Notebook.prototype.execute_all_cells = function () {
1502 1502 this.execute_cell_range(0, this.ncells());
1503 1503 this.scroll_to_bottom();
1504 1504 };
1505 1505
1506 1506 /**
1507 1507 * Execute a contiguous range of cells.
1508 1508 *
1509 1509 * @method execute_cell_range
1510 1510 * @param {Number} start Index of the first cell to execute (inclusive)
1511 1511 * @param {Number} end Index of the last cell to execute (exclusive)
1512 1512 */
1513 1513 Notebook.prototype.execute_cell_range = function (start, end) {
1514 1514 for (var i=start; i<end; i++) {
1515 1515 this.select(i);
1516 1516 this.execute_selected_cell({add_new:false});
1517 1517 };
1518 1518 };
1519 1519
1520 1520 // Persistance and loading
1521 1521
1522 1522 /**
1523 * Getter method for this notebook's ID.
1524 *
1525 * @method get_notebook_id
1526 * @return {String} This notebook's ID
1527 */
1528 Notebook.prototype.get_notebook_id = function () {
1529 return this.notebook_id;
1530 };
1531
1532 /**
1533 1523 * Getter method for this notebook's name.
1534 1524 *
1535 1525 * @method get_notebook_name
1536 1526 * @return {String} This notebook's name
1537 1527 */
1538 1528 Notebook.prototype.get_notebook_name = function () {
1539 1529 nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1540 1530 return nbname;
1541 1531 };
1542 1532
1543 1533 /**
1544 1534 * Setter method for this notebook's name.
1545 1535 *
1546 1536 * @method set_notebook_name
1547 1537 * @param {String} name A new name for this notebook
1548 1538 */
1549 1539 Notebook.prototype.set_notebook_name = function (name) {
1550 1540 this.notebook_name = name;
1551 1541 };
1552 1542
1553 1543 /**
1554 1544 * Check that a notebook's name is valid.
1555 1545 *
1556 1546 * @method test_notebook_name
1557 1547 * @param {String} nbname A name for this notebook
1558 1548 * @return {Boolean} True if the name is valid, false if invalid
1559 1549 */
1560 1550 Notebook.prototype.test_notebook_name = function (nbname) {
1561 1551 nbname = nbname || '';
1562 1552 if (this.notebook_name_blacklist_re.test(nbname) == false && nbname.length>0) {
1563 1553 return true;
1564 1554 } else {
1565 1555 return false;
1566 1556 };
1567 1557 };
1568 1558
1569 1559 /**
1570 1560 * Load a notebook from JSON (.ipynb).
1571 1561 *
1572 1562 * This currently handles one worksheet: others are deleted.
1573 1563 *
1574 1564 * @method fromJSON
1575 1565 * @param {Object} data JSON representation of a notebook
1576 1566 */
1577 1567 Notebook.prototype.fromJSON = function (data) {
1578 1568 data = data.content;
1579 1569 var ncells = this.ncells();
1580 1570 var i;
1581 1571 for (i=0; i<ncells; i++) {
1582 1572 // Always delete cell 0 as they get renumbered as they are deleted.
1583 1573 this.delete_cell(0);
1584 1574 };
1585 1575 // Save the metadata and name.
1586 1576 this.metadata = data.metadata;
1587 1577 this.notebook_name = data.metadata.name +'.ipynb';
1588 1578 // Only handle 1 worksheet for now.
1589 1579 var worksheet = data.worksheets[0];
1590 1580 if (worksheet !== undefined) {
1591 1581 if (worksheet.metadata) {
1592 1582 this.worksheet_metadata = worksheet.metadata;
1593 1583 }
1594 1584 var new_cells = worksheet.cells;
1595 1585 ncells = new_cells.length;
1596 1586 var cell_data = null;
1597 1587 var new_cell = null;
1598 1588 for (i=0; i<ncells; i++) {
1599 1589 cell_data = new_cells[i];
1600 1590 // VERSIONHACK: plaintext -> raw
1601 1591 // handle never-released plaintext name for raw cells
1602 1592 if (cell_data.cell_type === 'plaintext'){
1603 1593 cell_data.cell_type = 'raw';
1604 1594 }
1605 1595
1606 1596 new_cell = this.insert_cell_below(cell_data.cell_type);
1607 1597 new_cell.fromJSON(cell_data);
1608 1598 };
1609 1599 };
1610 1600 if (data.worksheets.length > 1) {
1611 1601 IPython.dialog.modal({
1612 1602 title : "Multiple worksheets",
1613 1603 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1614 1604 "but this version of IPython can only handle the first. " +
1615 1605 "If you save this notebook, worksheets after the first will be lost.",
1616 1606 buttons : {
1617 1607 OK : {
1618 1608 class : "btn-danger"
1619 1609 }
1620 1610 }
1621 1611 });
1622 1612 }
1623 1613 };
1624 1614
1625 1615 /**
1626 1616 * Dump this notebook into a JSON-friendly object.
1627 1617 *
1628 1618 * @method toJSON
1629 1619 * @return {Object} A JSON-friendly representation of this notebook.
1630 1620 */
1631 1621 Notebook.prototype.toJSON = function () {
1632 1622 var cells = this.get_cells();
1633 1623 var ncells = cells.length;
1634 1624 var cell_array = new Array(ncells);
1635 1625 for (var i=0; i<ncells; i++) {
1636 1626 cell_array[i] = cells[i].toJSON();
1637 1627 };
1638 1628 var data = {
1639 1629 // Only handle 1 worksheet for now.
1640 1630 worksheets : [{
1641 1631 cells: cell_array,
1642 1632 metadata: this.worksheet_metadata
1643 1633 }],
1644 1634 metadata : this.metadata
1645 1635 };
1646 1636 return data;
1647 1637 };
1648 1638
1649 1639 /**
1650 1640 * Start an autosave timer, for periodically saving the notebook.
1651 1641 *
1652 1642 * @method set_autosave_interval
1653 1643 * @param {Integer} interval the autosave interval in milliseconds
1654 1644 */
1655 1645 Notebook.prototype.set_autosave_interval = function (interval) {
1656 1646 var that = this;
1657 1647 // clear previous interval, so we don't get simultaneous timers
1658 1648 if (this.autosave_timer) {
1659 1649 clearInterval(this.autosave_timer);
1660 1650 }
1661 1651
1662 1652 this.autosave_interval = this.minimum_autosave_interval = interval;
1663 1653 if (interval) {
1664 1654 this.autosave_timer = setInterval(function() {
1665 1655 if (that.dirty) {
1666 1656 that.save_notebook();
1667 1657 }
1668 1658 }, interval);
1669 1659 $([IPython.events]).trigger("autosave_enabled.Notebook", interval);
1670 1660 } else {
1671 1661 this.autosave_timer = null;
1672 1662 $([IPython.events]).trigger("autosave_disabled.Notebook");
1673 1663 };
1674 1664 };
1675 1665
1676 1666 /**
1677 1667 * Save this notebook on the server.
1678 1668 *
1679 1669 * @method save_notebook
1680 1670 */
1681 1671 Notebook.prototype.save_notebook = function () {
1682 1672 // We may want to move the name/id/nbformat logic inside toJSON?
1683 1673 var data = this.toJSON();
1684 1674 data.metadata.name = this.notebook_name;
1685 1675 data.nbformat = this.nbformat;
1686 1676 data.nbformat_minor = this.nbformat_minor;
1687 1677
1688 1678 // time the ajax call for autosave tuning purposes.
1689 1679 var start = new Date().getTime();
1690 1680 // We do the call with settings so we can set cache to false.
1691 1681 var settings = {
1692 1682 processData : false,
1693 1683 cache : false,
1694 1684 type : "PUT",
1695 1685 data : JSON.stringify(data),
1696 1686 headers : {'Content-Type': 'application/json'},
1697 1687 success : $.proxy(this.save_notebook_success, this, start),
1698 1688 error : $.proxy(this.save_notebook_error, this)
1699 1689 };
1700 1690 $([IPython.events]).trigger('notebook_saving.Notebook');
1701 1691 var url = this.baseProjectUrl() + 'api/notebooks/' + this.notebookPath()+ this.notebook_name;
1702 1692 $.ajax(url, settings);
1703 1693 };
1704 1694
1705 1695 /**
1706 1696 * Success callback for saving a notebook.
1707 1697 *
1708 1698 * @method save_notebook_success
1709 1699 * @param {Integer} start the time when the save request started
1710 1700 * @param {Object} data JSON representation of a notebook
1711 1701 * @param {String} status Description of response status
1712 1702 * @param {jqXHR} xhr jQuery Ajax object
1713 1703 */
1714 1704 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1715 1705 this.set_dirty(false);
1716 1706 $([IPython.events]).trigger('notebook_saved.Notebook');
1717 1707 this._update_autosave_interval(start);
1718 1708 if (this._checkpoint_after_save) {
1719 1709 this.create_checkpoint();
1720 1710 this._checkpoint_after_save = false;
1721 1711 };
1722 1712 };
1723 1713
1724 1714 /**
1725 1715 * update the autosave interval based on how long the last save took
1726 1716 *
1727 1717 * @method _update_autosave_interval
1728 1718 * @param {Integer} timestamp when the save request started
1729 1719 */
1730 1720 Notebook.prototype._update_autosave_interval = function (start) {
1731 1721 var duration = (new Date().getTime() - start);
1732 1722 if (this.autosave_interval) {
1733 1723 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1734 1724 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1735 1725 // round to 10 seconds, otherwise we will be setting a new interval too often
1736 1726 interval = 10000 * Math.round(interval / 10000);
1737 1727 // set new interval, if it's changed
1738 1728 if (interval != this.autosave_interval) {
1739 1729 this.set_autosave_interval(interval);
1740 1730 }
1741 1731 }
1742 1732 };
1743 1733
1744 1734 /**
1745 1735 * Failure callback for saving a notebook.
1746 1736 *
1747 1737 * @method save_notebook_error
1748 1738 * @param {jqXHR} xhr jQuery Ajax object
1749 1739 * @param {String} status Description of response status
1750 1740 * @param {String} error_msg HTTP error message
1751 1741 */
1752 1742 Notebook.prototype.save_notebook_error = function (xhr, status, error_msg) {
1753 1743 $([IPython.events]).trigger('notebook_save_failed.Notebook');
1754 1744 };
1755 1745
1756 1746
1757 1747 Notebook.prototype.notebook_rename = function (nbname) {
1758 1748 var that = this;
1759 1749 var new_name = nbname + '.ipynb'
1760 var name = {'notebook_name': new_name};
1750 var name = {'name': new_name};
1761 1751 var settings = {
1762 1752 processData : false,
1763 1753 cache : false,
1764 1754 type : "PATCH",
1765 1755 data : JSON.stringify(name),
1766 1756 dataType: "json",
1767 1757 headers : {'Content-Type': 'application/json'},
1768 1758 success : $.proxy(that.rename_success, this)
1769 1759 };
1770 1760 $([IPython.events]).trigger('notebook_rename.Notebook');
1771 1761 var url = this.baseProjectUrl() + 'api/notebooks/' + this.notebookPath()+ this.notebook_name;
1772 1762 $.ajax(url, settings);
1773 1763 };
1774 1764
1775 1765
1776 1766 Notebook.prototype.rename_success = function (json, status, xhr) {
1777 this.notebook_name = json.notebook_name
1767 this.notebook_name = json.name
1778 1768 var notebook_path = this.notebookPath() + this.notebook_name;
1779 1769 this.session.notebook_rename(notebook_path);
1780 1770 $([IPython.events]).trigger('notebook_renamed.Notebook');
1781 1771 }
1782 1772
1783 1773 /**
1784 1774 * Request a notebook's data from the server.
1785 1775 *
1786 1776 * @method load_notebook
1787 1777 * @param {String} notebook_naem and path A notebook to load
1788 1778 */
1789 1779 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
1790 1780 var that = this;
1791 1781 this.notebook_name = notebook_name;
1792 1782 this.notebook_path = notebook_path;
1793 1783 // We do the call with settings so we can set cache to false.
1794 1784 var settings = {
1795 1785 processData : false,
1796 1786 cache : false,
1797 1787 type : "GET",
1798 1788 dataType : "json",
1799 1789 success : $.proxy(this.load_notebook_success,this),
1800 1790 error : $.proxy(this.load_notebook_error,this),
1801 1791 };
1802 1792 $([IPython.events]).trigger('notebook_loading.Notebook');
1803 1793 var url = this.baseProjectUrl() + 'api/notebooks/' + this.notebookPath() + this.notebook_name;
1804 1794 $.ajax(url, settings);
1805 1795 };
1806 1796
1807 1797 /**
1808 1798 * Success callback for loading a notebook from the server.
1809 1799 *
1810 1800 * Load notebook data from the JSON response.
1811 1801 *
1812 1802 * @method load_notebook_success
1813 1803 * @param {Object} data JSON representation of a notebook
1814 1804 * @param {String} status Description of response status
1815 1805 * @param {jqXHR} xhr jQuery Ajax object
1816 1806 */
1817 1807 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
1818 1808 this.fromJSON(data);
1819 1809 if (this.ncells() === 0) {
1820 1810 this.insert_cell_below('code');
1821 1811 };
1822 1812 this.set_dirty(false);
1823 1813 this.select(0);
1824 1814 this.scroll_to_top();
1825 1815 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
1826 1816 var msg = "This notebook has been converted from an older " +
1827 1817 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
1828 1818 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
1829 1819 "newer notebook format will be used and older versions of IPython " +
1830 1820 "may not be able to read it. To keep the older version, close the " +
1831 1821 "notebook without saving it.";
1832 1822 IPython.dialog.modal({
1833 1823 title : "Notebook converted",
1834 1824 body : msg,
1835 1825 buttons : {
1836 1826 OK : {
1837 1827 class : "btn-primary"
1838 1828 }
1839 1829 }
1840 1830 });
1841 1831 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
1842 1832 var that = this;
1843 1833 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
1844 1834 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
1845 1835 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
1846 1836 this_vs + ". You can still work with this notebook, but some features " +
1847 1837 "introduced in later notebook versions may not be available."
1848 1838
1849 1839 IPython.dialog.modal({
1850 1840 title : "Newer Notebook",
1851 1841 body : msg,
1852 1842 buttons : {
1853 1843 OK : {
1854 1844 class : "btn-danger"
1855 1845 }
1856 1846 }
1857 1847 });
1858 1848
1859 1849 }
1860 1850
1861 1851 // Create the session after the notebook is completely loaded to prevent
1862 1852 // code execution upon loading, which is a security risk.
1863 1853 if (this.session == null) {
1864 1854 this.start_session(this.notebook_path);
1865 1855 }
1866 1856 // load our checkpoint list
1867 1857 IPython.notebook.list_checkpoints();
1868 1858 $([IPython.events]).trigger('notebook_loaded.Notebook');
1869 1859 };
1870 1860
1871 1861 /**
1872 1862 * Failure callback for loading a notebook from the server.
1873 1863 *
1874 1864 * @method load_notebook_error
1875 1865 * @param {jqXHR} xhr jQuery Ajax object
1876 1866 * @param {String} textStatus Description of response status
1877 1867 * @param {String} errorThrow HTTP error message
1878 1868 */
1879 1869 Notebook.prototype.load_notebook_error = function (xhr, textStatus, errorThrow) {
1880 1870 if (xhr.status === 400) {
1881 1871 var msg = errorThrow;
1882 1872 } else if (xhr.status === 500) {
1883 1873 var msg = "An unknown error occurred while loading this notebook. " +
1884 1874 "This version can load notebook formats " +
1885 1875 "v" + this.nbformat + " or earlier.";
1886 1876 }
1887 1877 IPython.dialog.modal({
1888 1878 title: "Error loading notebook",
1889 1879 body : msg,
1890 1880 buttons : {
1891 1881 "OK": {}
1892 1882 }
1893 1883 });
1894 1884 }
1895 1885
1896 1886 /********************* checkpoint-related *********************/
1897 1887
1898 1888 /**
1899 1889 * Save the notebook then immediately create a checkpoint.
1900 1890 *
1901 1891 * @method save_checkpoint
1902 1892 */
1903 1893 Notebook.prototype.save_checkpoint = function () {
1904 1894 this._checkpoint_after_save = true;
1905 1895 this.save_notebook();
1906 1896 };
1907 1897
1908 1898 /**
1909 1899 * Add a checkpoint for this notebook.
1910 1900 * for use as a callback from checkpoint creation.
1911 1901 *
1912 1902 * @method add_checkpoint
1913 1903 */
1914 1904 Notebook.prototype.add_checkpoint = function (checkpoint) {
1915 1905 var found = false;
1916 1906 for (var i = 0; i < this.checkpoints.length; i++) {
1917 1907 var existing = this.checkpoints[i];
1918 1908 if (existing.checkpoint_id == checkpoint.checkpoint_id) {
1919 1909 found = true;
1920 1910 this.checkpoints[i] = checkpoint;
1921 1911 break;
1922 1912 }
1923 1913 }
1924 1914 if (!found) {
1925 1915 this.checkpoints.push(checkpoint);
1926 1916 }
1927 1917 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
1928 1918 };
1929 1919
1930 1920 /**
1931 1921 * List checkpoints for this notebook.
1932 1922 *
1933 1923 * @method list_checkpoints
1934 1924 */
1935 1925 Notebook.prototype.list_checkpoints = function () {
1936 1926 var url = this.baseProjectUrl() + 'api/notebooks/' + this.notebookPath() + this.notebook_name + '/checkpoints';
1937 1927 $.get(url).done(
1938 1928 $.proxy(this.list_checkpoints_success, this)
1939 1929 ).fail(
1940 1930 $.proxy(this.list_checkpoints_error, this)
1941 1931 );
1942 1932 };
1943 1933
1944 1934 /**
1945 1935 * Success callback for listing checkpoints.
1946 1936 *
1947 1937 * @method list_checkpoint_success
1948 1938 * @param {Object} data JSON representation of a checkpoint
1949 1939 * @param {String} status Description of response status
1950 1940 * @param {jqXHR} xhr jQuery Ajax object
1951 1941 */
1952 1942 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
1953 1943 var data = $.parseJSON(data);
1954 1944 this.checkpoints = data;
1955 1945 if (data.length) {
1956 1946 this.last_checkpoint = data[data.length - 1];
1957 1947 } else {
1958 1948 this.last_checkpoint = null;
1959 1949 }
1960 1950 $([IPython.events]).trigger('checkpoints_listed.Notebook', [data]);
1961 1951 };
1962 1952
1963 1953 /**
1964 1954 * Failure callback for listing a checkpoint.
1965 1955 *
1966 1956 * @method list_checkpoint_error
1967 1957 * @param {jqXHR} xhr jQuery Ajax object
1968 1958 * @param {String} status Description of response status
1969 1959 * @param {String} error_msg HTTP error message
1970 1960 */
1971 1961 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
1972 1962 $([IPython.events]).trigger('list_checkpoints_failed.Notebook');
1973 1963 };
1974 1964
1975 1965 /**
1976 1966 * Create a checkpoint of this notebook on the server from the most recent save.
1977 1967 *
1978 1968 * @method create_checkpoint
1979 1969 */
1980 1970 Notebook.prototype.create_checkpoint = function () {
1981 1971 var url = this.baseProjectUrl() + 'api/notebooks/' + this.notebookPath() + this.notebook_name + '/checkpoints';
1982 1972 $.post(url).done(
1983 1973 $.proxy(this.create_checkpoint_success, this)
1984 1974 ).fail(
1985 1975 $.proxy(this.create_checkpoint_error, this)
1986 1976 );
1987 1977 };
1988 1978
1989 1979 /**
1990 1980 * Success callback for creating a checkpoint.
1991 1981 *
1992 1982 * @method create_checkpoint_success
1993 1983 * @param {Object} data JSON representation of a checkpoint
1994 1984 * @param {String} status Description of response status
1995 1985 * @param {jqXHR} xhr jQuery Ajax object
1996 1986 */
1997 1987 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
1998 1988 var data = $.parseJSON(data);
1999 1989 this.add_checkpoint(data);
2000 1990 $([IPython.events]).trigger('checkpoint_created.Notebook', data);
2001 1991 };
2002 1992
2003 1993 /**
2004 1994 * Failure callback for creating a checkpoint.
2005 1995 *
2006 1996 * @method create_checkpoint_error
2007 1997 * @param {jqXHR} xhr jQuery Ajax object
2008 1998 * @param {String} status Description of response status
2009 1999 * @param {String} error_msg HTTP error message
2010 2000 */
2011 2001 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2012 2002 $([IPython.events]).trigger('checkpoint_failed.Notebook');
2013 2003 };
2014 2004
2015 2005 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2016 2006 var that = this;
2017 2007 var checkpoint = checkpoint || this.last_checkpoint;
2018 2008 if ( ! checkpoint ) {
2019 2009 console.log("restore dialog, but no checkpoint to restore to!");
2020 2010 return;
2021 2011 }
2022 2012 var body = $('<div/>').append(
2023 2013 $('<p/>').addClass("p-space").text(
2024 2014 "Are you sure you want to revert the notebook to " +
2025 2015 "the latest checkpoint?"
2026 2016 ).append(
2027 2017 $("<strong/>").text(
2028 2018 " This cannot be undone."
2029 2019 )
2030 2020 )
2031 2021 ).append(
2032 2022 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2033 2023 ).append(
2034 2024 $('<p/>').addClass("p-space").text(
2035 2025 Date(checkpoint.last_modified)
2036 2026 ).css("text-align", "center")
2037 2027 );
2038 2028
2039 2029 IPython.dialog.modal({
2040 2030 title : "Revert notebook to checkpoint",
2041 2031 body : body,
2042 2032 buttons : {
2043 2033 Revert : {
2044 2034 class : "btn-danger",
2045 2035 click : function () {
2046 2036 that.restore_checkpoint(checkpoint.checkpoint_id);
2047 2037 }
2048 2038 },
2049 2039 Cancel : {}
2050 2040 }
2051 2041 });
2052 2042 }
2053 2043
2054 2044 /**
2055 2045 * Restore the notebook to a checkpoint state.
2056 2046 *
2057 2047 * @method restore_checkpoint
2058 2048 * @param {String} checkpoint ID
2059 2049 */
2060 2050 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2061 2051 <<<<<<< HEAD
2062 2052 $([IPython.events]).trigger('checkpoint_restoring.Notebook', checkpoint);
2063 2053 if (this.notebook_path != "") {
2064 2054 var url = this.baseProjectUrl() + 'api/notebooks/' + this.notebook_path + this.notebook_name + '/checkpoints/' + checkpoint;
2065 2055 }
2066 2056 else {
2067 2057 var url = this.baseProjectUrl() + 'api/notebooks/' +this.notebook_name + '/checkpoints/' + checkpoint;
2068 2058 }
2069 2059 =======
2070 2060 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2071 2061 var url = this.baseProjectUrl() + 'api/notebooks/' + this.notebookPath() + this.notebook_name + '/checkpoints/' + checkpoint;
2072 2062 >>>>>>> fixing path redirects, cleaning path logic
2073 2063 $.post(url).done(
2074 2064 $.proxy(this.restore_checkpoint_success, this)
2075 2065 ).fail(
2076 2066 $.proxy(this.restore_checkpoint_error, this)
2077 2067 );
2078 2068 };
2079 2069
2080 2070 /**
2081 2071 * Success callback for restoring a notebook to a checkpoint.
2082 2072 *
2083 2073 * @method restore_checkpoint_success
2084 2074 * @param {Object} data (ignored, should be empty)
2085 2075 * @param {String} status Description of response status
2086 2076 * @param {jqXHR} xhr jQuery Ajax object
2087 2077 */
2088 2078 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2089 2079 $([IPython.events]).trigger('checkpoint_restored.Notebook');
2090 2080 this.load_notebook(this.notebook_name, this.notebook_path);
2091 2081 };
2092 2082
2093 2083 /**
2094 2084 * Failure callback for restoring a notebook to a checkpoint.
2095 2085 *
2096 2086 * @method restore_checkpoint_error
2097 2087 * @param {jqXHR} xhr jQuery Ajax object
2098 2088 * @param {String} status Description of response status
2099 2089 * @param {String} error_msg HTTP error message
2100 2090 */
2101 2091 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2102 2092 $([IPython.events]).trigger('checkpoint_restore_failed.Notebook');
2103 2093 };
2104 2094
2105 2095 /**
2106 2096 * Delete a notebook checkpoint.
2107 2097 *
2108 2098 * @method delete_checkpoint
2109 2099 * @param {String} checkpoint ID
2110 2100 */
2111 2101 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2112 2102 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2113 2103 var url = this.baseProjectUrl() + 'api/notebooks/' + this.notebookPath() + this.notebook_name + '/checkpoints/' + checkpoint;
2114 2104 $.ajax(url, {
2115 2105 type: 'DELETE',
2116 2106 success: $.proxy(this.delete_checkpoint_success, this),
2117 2107 error: $.proxy(this.delete_notebook_error,this)
2118 2108 });
2119 2109 };
2120 2110
2121 2111 /**
2122 2112 * Success callback for deleting a notebook checkpoint
2123 2113 *
2124 2114 * @method delete_checkpoint_success
2125 2115 * @param {Object} data (ignored, should be empty)
2126 2116 * @param {String} status Description of response status
2127 2117 * @param {jqXHR} xhr jQuery Ajax object
2128 2118 */
2129 2119 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2130 2120 $([IPython.events]).trigger('checkpoint_deleted.Notebook', data);
2131 2121 this.load_notebook(this.notebook_name, this.notebook_path);
2132 2122 };
2133 2123
2134 2124 /**
2135 2125 * Failure callback for deleting a notebook checkpoint.
2136 2126 *
2137 2127 * @method delete_checkpoint_error
2138 2128 * @param {jqXHR} xhr jQuery Ajax object
2139 2129 * @param {String} status Description of response status
2140 2130 * @param {String} error_msg HTTP error message
2141 2131 */
2142 2132 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2143 2133 $([IPython.events]).trigger('checkpoint_delete_failed.Notebook');
2144 2134 };
2145 2135
2146 2136
2147 2137 IPython.Notebook = Notebook;
2148 2138
2149 2139
2150 2140 return IPython;
2151 2141
2152 2142 }(IPython));
2153 2143
@@ -1,527 +1,527 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Kernel
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule Kernel
16 16 */
17 17
18 18 var IPython = (function (IPython) {
19 19
20 20 var utils = IPython.utils;
21 21
22 22 // Initialization and connection.
23 23 /**
24 24 * A Kernel Class to communicate with the Python kernel
25 25 * @Class Kernel
26 26 */
27 27 var Kernel = function (base_url, session_id) {
28 28 this.kernel_id = null;
29 29 this.session_id = session_id
30 30 this.shell_channel = null;
31 31 this.iopub_channel = null;
32 32 this.stdin_channel = null;
33 33 this.base_url = base_url;
34 34 this.running = false;
35 35 this.username = "username";
36 36 this.base_session_id = utils.uuid();
37 37 this._msg_callbacks = {};
38 38
39 39 if (typeof(WebSocket) !== 'undefined') {
40 40 this.WebSocket = WebSocket;
41 41 } else if (typeof(MozWebSocket) !== 'undefined') {
42 42 this.WebSocket = MozWebSocket;
43 43 } else {
44 44 alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox β‰₯ 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.');
45 45 };
46 46 this.bind_events();
47 47 };
48 48
49 49
50 50 Kernel.prototype._get_msg = function (msg_type, content) {
51 51 var msg = {
52 52 header : {
53 53 msg_id : utils.uuid(),
54 54 username : this.username,
55 55 session : this.base_session_id,
56 56 msg_type : msg_type
57 57 },
58 58 metadata : {},
59 59 content : content,
60 60 parent_header : {}
61 61 };
62 62 return msg;
63 63 };
64 64
65 65 Kernel.prototype.bind_events = function() {
66 66 var that = this;
67 67 $([IPython.events]).on('send_input_reply.Kernel', function(evt, data) {
68 68 that.send_input_reply(data);
69 69 });
70 70 }
71 71
72 72 /**
73 73 * Start the Python kernel
74 74 * @method start
75 75 */
76 76 Kernel.prototype.start = function (params) {
77 77 var that = this;
78 78 params = params || {};
79 79 params.session = this.session_id;
80 80 if (!this.running) {
81 81 var qs = $.param(params);
82 82 var url = this.base_url + '?' + qs;
83 83 $.post(url,
84 84 $.proxy(that._kernel_started,that),
85 85 'json'
86 86 );
87 87 };
88 88 };
89 89
90 90 /**
91 91 * Restart the python kernel.
92 92 *
93 93 * Emit a 'status_restarting.Kernel' event with
94 94 * the current object as parameter
95 95 *
96 96 * @method restart
97 97 */
98 98 Kernel.prototype.restart = function () {
99 99 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
100 100 var that = this;
101 101 if (this.running) {
102 102 this.stop_channels();
103 103 var url = this.kernel_url + "/restart";
104 104 $.post(url,
105 105 $.proxy(that._kernel_started, that),
106 106 'json'
107 107 );
108 108 };
109 109 };
110 110
111 111
112 112 Kernel.prototype._kernel_started = function (json) {
113 console.log("Kernel started: ", json.kernel_id);
113 console.log("Kernel started: ", json.id);
114 114 this.running = true;
115 this.kernel_id = json.kernel_id;
115 this.kernel_id = json.id;
116 116 var ws_url = json.ws_url;
117 117 if (ws_url.match(/wss?:\/\//) == null) {
118 118 // trailing 's' in https will become wss for secure web sockets
119 119 prot = location.protocol.replace('http', 'ws') + "//";
120 120 ws_url = prot + location.host + ws_url;
121 121 };
122 122 this.ws_url = ws_url;
123 123 this.kernel_url = this.base_url + "/" + this.kernel_id;
124 124 this.start_channels();
125 125 };
126 126
127 127
128 128 Kernel.prototype._websocket_closed = function(ws_url, early) {
129 129 this.stop_channels();
130 130 $([IPython.events]).trigger('websocket_closed.Kernel',
131 131 {ws_url: ws_url, kernel: this, early: early}
132 132 );
133 133 };
134 134
135 135 /**
136 136 * Start the `shell`and `iopub` channels.
137 137 * Will stop and restart them if they already exist.
138 138 *
139 139 * @method start_channels
140 140 */
141 141 Kernel.prototype.start_channels = function () {
142 142 var that = this;
143 143 this.stop_channels();
144 144 var ws_url = this.ws_url + this.kernel_url;
145 145 console.log("Starting WebSockets:", ws_url);
146 146 this.shell_channel = new this.WebSocket(ws_url + "/shell");
147 147 this.stdin_channel = new this.WebSocket(ws_url + "/stdin");
148 148 this.iopub_channel = new this.WebSocket(ws_url + "/iopub");
149 149
150 150 var already_called_onclose = false; // only alert once
151 151 var ws_closed_early = function(evt){
152 152 if (already_called_onclose){
153 153 return;
154 154 }
155 155 already_called_onclose = true;
156 156 if ( ! evt.wasClean ){
157 157 that._websocket_closed(ws_url, true);
158 158 }
159 159 };
160 160 var ws_closed_late = function(evt){
161 161 if (already_called_onclose){
162 162 return;
163 163 }
164 164 already_called_onclose = true;
165 165 if ( ! evt.wasClean ){
166 166 that._websocket_closed(ws_url, false);
167 167 }
168 168 };
169 169 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
170 170 for (var i=0; i < channels.length; i++) {
171 171 channels[i].onopen = $.proxy(this._ws_opened, this);
172 172 channels[i].onclose = ws_closed_early;
173 173 }
174 174 // switch from early-close to late-close message after 1s
175 175 setTimeout(function() {
176 176 for (var i=0; i < channels.length; i++) {
177 177 if (channels[i] !== null) {
178 178 channels[i].onclose = ws_closed_late;
179 179 }
180 180 }
181 181 }, 1000);
182 182 this.shell_channel.onmessage = $.proxy(this._handle_shell_reply, this);
183 183 this.iopub_channel.onmessage = $.proxy(this._handle_iopub_reply, this);
184 184 this.stdin_channel.onmessage = $.proxy(this._handle_input_request, this);
185 185 };
186 186
187 187 /**
188 188 * Handle a websocket entering the open state
189 189 * sends session and cookie authentication info as first message.
190 190 * Once all sockets are open, signal the Kernel.status_started event.
191 191 * @method _ws_opened
192 192 */
193 193 Kernel.prototype._ws_opened = function (evt) {
194 194 // send the session id so the Session object Python-side
195 195 // has the same identity
196 196 evt.target.send(this.session_id + ':' + document.cookie);
197 197
198 198 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
199 199 for (var i=0; i < channels.length; i++) {
200 200 // if any channel is not ready, don't trigger event.
201 201 if ( !channels[i].readyState ) return;
202 202 }
203 203 // all events ready, trigger started event.
204 204 $([IPython.events]).trigger('status_started.Kernel', {kernel: this});
205 205 };
206 206
207 207 /**
208 208 * Stop the websocket channels.
209 209 * @method stop_channels
210 210 */
211 211 Kernel.prototype.stop_channels = function () {
212 212 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
213 213 for (var i=0; i < channels.length; i++) {
214 214 if ( channels[i] !== null ) {
215 215 channels[i].onclose = function (evt) {};
216 216 channels[i].close();
217 217 }
218 218 };
219 219 this.shell_channel = this.iopub_channel = this.stdin_channel = null;
220 220 };
221 221
222 222 // Main public methods.
223 223
224 224 /**
225 225 * Get info on object asynchronoulsy
226 226 *
227 227 * @async
228 228 * @param objname {string}
229 229 * @param callback {dict}
230 230 * @method object_info_request
231 231 *
232 232 * @example
233 233 *
234 234 * When calling this method pass a callbacks structure of the form:
235 235 *
236 236 * callbacks = {
237 237 * 'object_info_reply': object_info_reply_callback
238 238 * }
239 239 *
240 240 * The `object_info_reply_callback` will be passed the content object of the
241 241 *
242 242 * `object_into_reply` message documented in
243 243 * [IPython dev documentation](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information)
244 244 */
245 245 Kernel.prototype.object_info_request = function (objname, callbacks) {
246 246 if(typeof(objname)!=null && objname!=null)
247 247 {
248 248 var content = {
249 249 oname : objname.toString(),
250 250 detail_level : 0,
251 251 };
252 252 var msg = this._get_msg("object_info_request", content);
253 253 this.shell_channel.send(JSON.stringify(msg));
254 254 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
255 255 return msg.header.msg_id;
256 256 }
257 257 return;
258 258 }
259 259
260 260 /**
261 261 * Execute given code into kernel, and pass result to callback.
262 262 *
263 263 * TODO: document input_request in callbacks
264 264 *
265 265 * @async
266 266 * @method execute
267 267 * @param {string} code
268 268 * @param [callbacks] {Object} With the optional following keys
269 269 * @param callbacks.'execute_reply' {function}
270 270 * @param callbacks.'output' {function}
271 271 * @param callbacks.'clear_output' {function}
272 272 * @param callbacks.'set_next_input' {function}
273 273 * @param {object} [options]
274 274 * @param [options.silent=false] {Boolean}
275 275 * @param [options.user_expressions=empty_dict] {Dict}
276 276 * @param [options.user_variables=empty_list] {List od Strings}
277 277 * @param [options.allow_stdin=false] {Boolean} true|false
278 278 *
279 279 * @example
280 280 *
281 281 * The options object should contain the options for the execute call. Its default
282 282 * values are:
283 283 *
284 284 * options = {
285 285 * silent : true,
286 286 * user_variables : [],
287 287 * user_expressions : {},
288 288 * allow_stdin : false
289 289 * }
290 290 *
291 291 * When calling this method pass a callbacks structure of the form:
292 292 *
293 293 * callbacks = {
294 294 * 'execute_reply': execute_reply_callback,
295 295 * 'output': output_callback,
296 296 * 'clear_output': clear_output_callback,
297 297 * 'set_next_input': set_next_input_callback
298 298 * }
299 299 *
300 300 * The `execute_reply_callback` will be passed the content and metadata
301 301 * objects of the `execute_reply` message documented
302 302 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#execute)
303 303 *
304 304 * The `output_callback` will be passed `msg_type` ('stream','display_data','pyout','pyerr')
305 305 * of the output and the content and metadata objects of the PUB/SUB channel that contains the
306 306 * output:
307 307 *
308 308 * http://ipython.org/ipython-doc/dev/development/messaging.html#messages-on-the-pub-sub-socket
309 309 *
310 310 * The `clear_output_callback` will be passed a content object that contains
311 311 * stdout, stderr and other fields that are booleans, as well as the metadata object.
312 312 *
313 313 * The `set_next_input_callback` will be passed the text that should become the next
314 314 * input cell.
315 315 */
316 316 Kernel.prototype.execute = function (code, callbacks, options) {
317 317
318 318 var content = {
319 319 code : code,
320 320 silent : true,
321 321 store_history : false,
322 322 user_variables : [],
323 323 user_expressions : {},
324 324 allow_stdin : false
325 325 };
326 326 callbacks = callbacks || {};
327 327 if (callbacks.input_request !== undefined) {
328 328 content.allow_stdin = true;
329 329 }
330 330 $.extend(true, content, options)
331 331 $([IPython.events]).trigger('execution_request.Kernel', {kernel: this, content:content});
332 332 var msg = this._get_msg("execute_request", content);
333 333 this.shell_channel.send(JSON.stringify(msg));
334 334 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
335 335 return msg.header.msg_id;
336 336 };
337 337
338 338 /**
339 339 * When calling this method pass a callbacks structure of the form:
340 340 *
341 341 * callbacks = {
342 342 * 'complete_reply': complete_reply_callback
343 343 * }
344 344 *
345 345 * The `complete_reply_callback` will be passed the content object of the
346 346 * `complete_reply` message documented
347 347 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#complete)
348 348 *
349 349 * @method complete
350 350 * @param line {integer}
351 351 * @param cursor_pos {integer}
352 352 * @param {dict} callbacks
353 353 * @param callbacks.complete_reply {function} `complete_reply_callback`
354 354 *
355 355 */
356 356 Kernel.prototype.complete = function (line, cursor_pos, callbacks) {
357 357 callbacks = callbacks || {};
358 358 var content = {
359 359 text : '',
360 360 line : line,
361 361 block : null,
362 362 cursor_pos : cursor_pos
363 363 };
364 364 var msg = this._get_msg("complete_request", content);
365 365 this.shell_channel.send(JSON.stringify(msg));
366 366 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
367 367 return msg.header.msg_id;
368 368 };
369 369
370 370
371 371 Kernel.prototype.interrupt = function () {
372 372 if (this.running) {
373 373 $([IPython.events]).trigger('status_interrupting.Kernel', {kernel: this});
374 374 $.post(this.kernel_url + "/interrupt");
375 375 };
376 376 };
377 377
378 378
379 379 Kernel.prototype.kill = function () {
380 380 if (this.running) {
381 381 this.running = false;
382 382 var settings = {
383 383 cache : false,
384 384 type : "DELETE"
385 385 };
386 386 $.ajax(this.kernel_url, settings);
387 387 };
388 388 };
389 389
390 390 Kernel.prototype.send_input_reply = function (input) {
391 391 var content = {
392 392 value : input,
393 393 };
394 394 $([IPython.events]).trigger('input_reply.Kernel', {kernel: this, content:content});
395 395 var msg = this._get_msg("input_reply", content);
396 396 this.stdin_channel.send(JSON.stringify(msg));
397 397 return msg.header.msg_id;
398 398 };
399 399
400 400
401 401 // Reply handlers
402 402
403 403 Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
404 404 var callbacks = this._msg_callbacks[msg_id];
405 405 return callbacks;
406 406 };
407 407
408 408
409 409 Kernel.prototype.clear_callbacks_for_msg = function (msg_id) {
410 410 if (this._msg_callbacks[msg_id] !== undefined ) {
411 411 delete this._msg_callbacks[msg_id];
412 412 }
413 413 };
414 414
415 415
416 416 Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
417 417 this._msg_callbacks[msg_id] = callbacks || {};
418 418 };
419 419
420 420
421 421 Kernel.prototype._handle_shell_reply = function (e) {
422 422 var reply = $.parseJSON(e.data);
423 423 $([IPython.events]).trigger('shell_reply.Kernel', {kernel: this, reply:reply});
424 424 var header = reply.header;
425 425 var content = reply.content;
426 426 var metadata = reply.metadata;
427 427 var msg_type = header.msg_type;
428 428 var callbacks = this.get_callbacks_for_msg(reply.parent_header.msg_id);
429 429 if (callbacks !== undefined) {
430 430 var cb = callbacks[msg_type];
431 431 if (cb !== undefined) {
432 432 cb(content, metadata);
433 433 }
434 434 };
435 435
436 436 if (content.payload !== undefined) {
437 437 var payload = content.payload || [];
438 438 this._handle_payload(callbacks, payload);
439 439 }
440 440 };
441 441
442 442
443 443 Kernel.prototype._handle_payload = function (callbacks, payload) {
444 444 var l = payload.length;
445 445 // Payloads are handled by triggering events because we don't want the Kernel
446 446 // to depend on the Notebook or Pager classes.
447 447 for (var i=0; i<l; i++) {
448 448 if (payload[i].source === 'page') {
449 449 var data = {'text':payload[i].text}
450 450 $([IPython.events]).trigger('open_with_text.Pager', data);
451 451 } else if (payload[i].source === 'set_next_input') {
452 452 if (callbacks.set_next_input !== undefined) {
453 453 callbacks.set_next_input(payload[i].text)
454 454 }
455 455 }
456 456 };
457 457 };
458 458
459 459
460 460 Kernel.prototype._handle_iopub_reply = function (e) {
461 461 var reply = $.parseJSON(e.data);
462 462 var content = reply.content;
463 463 var msg_type = reply.header.msg_type;
464 464 var metadata = reply.metadata;
465 465 var callbacks = this.get_callbacks_for_msg(reply.parent_header.msg_id);
466 466 if (msg_type !== 'status' && callbacks === undefined) {
467 467 // Message not from one of this notebook's cells and there are no
468 468 // callbacks to handle it.
469 469 return;
470 470 }
471 471 var output_types = ['stream','display_data','pyout','pyerr'];
472 472 if (output_types.indexOf(msg_type) >= 0) {
473 473 var cb = callbacks['output'];
474 474 if (cb !== undefined) {
475 475 cb(msg_type, content, metadata);
476 476 }
477 477 } else if (msg_type === 'status') {
478 478 if (content.execution_state === 'busy') {
479 479 $([IPython.events]).trigger('status_busy.Kernel', {kernel: this});
480 480 } else if (content.execution_state === 'idle') {
481 481 $([IPython.events]).trigger('status_idle.Kernel', {kernel: this});
482 482 } else if (content.execution_state === 'restarting') {
483 483 // autorestarting is distinct from restarting,
484 484 // in that it means the kernel died and the server is restarting it.
485 485 // status_restarting sets the notification widget,
486 486 // autorestart shows the more prominent dialog.
487 487 $([IPython.events]).trigger('status_autorestarting.Kernel', {kernel: this});
488 488 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
489 489 } else if (content.execution_state === 'dead') {
490 490 this.stop_channels();
491 491 $([IPython.events]).trigger('status_dead.Kernel', {kernel: this});
492 492 };
493 493 } else if (msg_type === 'clear_output') {
494 494 var cb = callbacks['clear_output'];
495 495 if (cb !== undefined) {
496 496 cb(content, metadata);
497 497 }
498 498 };
499 499 };
500 500
501 501
502 502 Kernel.prototype._handle_input_request = function (e) {
503 503 var request = $.parseJSON(e.data);
504 504 var header = request.header;
505 505 var content = request.content;
506 506 var metadata = request.metadata;
507 507 var msg_type = header.msg_type;
508 508 if (msg_type !== 'input_request') {
509 509 console.log("Invalid input request!", request);
510 510 return;
511 511 }
512 512 var callbacks = this.get_callbacks_for_msg(request.parent_header.msg_id);
513 513 if (callbacks !== undefined) {
514 514 var cb = callbacks[msg_type];
515 515 if (cb !== undefined) {
516 516 cb(content, metadata);
517 517 }
518 518 };
519 519 };
520 520
521 521
522 522 IPython.Kernel = Kernel;
523 523
524 524 return IPython;
525 525
526 526 }(IPython));
527 527
@@ -1,96 +1,96 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Notebook
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13
14 14 var Session = function(notebook_path, Notebook){
15 15 this.kernel = null;
16 16 this.kernel_id = null;
17 17 this.session_id = null;
18 18 this.notebook_path = notebook_path;
19 19 this.notebook = Notebook;
20 20 this._baseProjectUrl = Notebook.baseProjectUrl()
21 21 };
22 22
23 23 Session.prototype.start = function(){
24 24 var that = this
25 25 var qs = $.param({notebook_path:this.notebook_path});
26 26 var url = '/api/sessions' + '?' + qs;
27 27 $.post(url,
28 28 $.proxy(this.start_kernel, that),
29 29 'json'
30 30 );
31 31 };
32 32
33 33 Session.prototype.notebook_rename = function (notebook_path) {
34 34 this.notebook_path = notebook_path;
35 35 var settings = {
36 36 processData : false,
37 37 cache : false,
38 38 type : "PATCH",
39 39 data: notebook_path,
40 40 dataType : "json",
41 41 };
42 42 var url = this._baseProjectUrl + 'api/sessions/' + this.session_id;
43 43 $.ajax(url, settings);
44 44 }
45 45
46 46
47 47 Session.prototype.delete_session = function() {
48 48 var settings = {
49 49 processData : false,
50 50 cache : false,
51 51 type : "DELETE",
52 52 dataType : "json",
53 53 };
54 54 var url = this._baseProjectUrl + 'api/sessions/' + this.session_id;
55 55 $.ajax(url, settings);
56 56 };
57 57
58 58 // Kernel related things
59 59 /**
60 60 * Start a new kernel and set it on each code cell.
61 61 *
62 62 * @method start_kernel
63 63 */
64 64 Session.prototype.start_kernel = function (json) {
65 this.session_id = json.session_id;
65 this.session_id = json.id;
66 66 this.kernel_content = json.kernel;
67 67 var base_url = $('body').data('baseKernelUrl') + "api/kernels";
68 68 this.kernel = new IPython.Kernel(base_url, this.session_id);
69 69 // Now that the kernel has been created, tell the CodeCells about it.
70 70 this.kernel._kernel_started(this.kernel_content);
71 71 };
72 72
73 73 /**
74 74 * Prompt the user to restart the IPython kernel.
75 75 *
76 76 * @method restart_kernel
77 77 */
78 78 Session.prototype.restart_kernel = function () {
79 79 this.kernel.restart();
80 80 };
81 81
82 82 Session.prototype.interrupt_kernel = function() {
83 83 this.kernel.interrupt();
84 84 };
85 85
86 86
87 87 Session.prototype.kill_kernel = function() {
88 88 this.kernel.kill();
89 89 };
90 90
91 91 IPython.Session = Session;
92 92
93 93
94 94 return IPython;
95 95
96 96 }(IPython));
@@ -1,346 +1,346 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // NotebookList
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13
14 14 var NotebookList = function (selector) {
15 15 this.selector = selector;
16 16 if (this.selector !== undefined) {
17 17 this.element = $(selector);
18 18 this.style();
19 19 this.bind_events();
20 20 }
21 21 this.notebooks_list = new Array();
22 22 this.sessions = new Object();
23 23 };
24 24
25 25 NotebookList.prototype.baseProjectUrl = function () {
26 26 return $('body').data('baseProjectUrl');
27 27 };
28 28
29 29 NotebookList.prototype.notebookPath = function() {
30 30 var path = $('body').data('notebookPath');
31 31 path = decodeURIComponent(path);
32 32 if (path != "") {
33 33 if (path[path.length-1] != '/') {
34 34 path = path.substring(0,path.length);
35 35 };
36 36 return path;
37 37 } else {
38 38 return path;
39 39 };
40 40 };
41 41
42 42 NotebookList.prototype.url_name = function(name){
43 43 return encodeURIComponent(name);
44 44 };
45 45
46 46 NotebookList.prototype.style = function () {
47 47 $('#notebook_toolbar').addClass('list_toolbar');
48 48 $('#drag_info').addClass('toolbar_info');
49 49 $('#notebook_buttons').addClass('toolbar_buttons');
50 50 $('#notebook_list_header').addClass('list_header');
51 51 this.element.addClass("list_container");
52 52 };
53 53
54 54
55 55 NotebookList.prototype.bind_events = function () {
56 56 var that = this;
57 57 $('#refresh_notebook_list').click(function () {
58 58 that.load_list();
59 59 });
60 60 this.element.bind('dragover', function () {
61 61 return false;
62 62 });
63 63 this.element.bind('drop', function(event){
64 64 that.handelFilesUpload(event,'drop');
65 65 return false;
66 66 });
67 67 };
68 68
69 69 NotebookList.prototype.handelFilesUpload = function(event, dropOrForm) {
70 70 var that = this;
71 71 var files;
72 72 if(dropOrForm =='drop'){
73 73 files = event.originalEvent.dataTransfer.files;
74 74 } else
75 75 {
76 76 files = event.originalEvent.target.files
77 77 }
78 78 for (var i = 0, f; f = files[i]; i++) {
79 79 var reader = new FileReader();
80 80 reader.readAsText(f);
81 81 var fname = f.name.split('.');
82 82 var nbname = fname.slice(0,-1).join('.');
83 83 var nbformat = fname.slice(-1)[0];
84 84 if (nbformat === 'ipynb') {nbformat = 'json';};
85 85 if (nbformat === 'py' || nbformat === 'json') {
86 86 var item = that.new_notebook_item(0);
87 87 that.add_name_input(nbname, item);
88 88 item.data('nbformat', nbformat);
89 89 // Store the notebook item in the reader so we can use it later
90 90 // to know which item it belongs to.
91 91 $(reader).data('item', item);
92 92 reader.onload = function (event) {
93 93 var nbitem = $(event.target).data('item');
94 94 that.add_notebook_data(event.target.result, nbitem);
95 95 that.add_upload_button(nbitem);
96 96 };
97 97 };
98 98 }
99 99 return false;
100 100 };
101 101
102 102 NotebookList.prototype.clear_list = function () {
103 103 this.element.children('.list_item').remove();
104 104 };
105 105
106 106 NotebookList.prototype.load_sessions = function(){
107 107 var that = this;
108 108 var settings = {
109 109 processData : false,
110 110 cache : false,
111 111 type : "GET",
112 112 dataType : "json",
113 113 success : $.proxy(that.sessions_loaded, this)
114 114 };
115 115 var url = this.baseProjectUrl() + 'api/sessions';
116 116 $.ajax(url,settings);
117 117 };
118 118
119 119
120 120 NotebookList.prototype.sessions_loaded = function(data){
121 121 this.sessions = new Object();
122 122 var len = data.length;
123 123 if (len != 0) {
124 124 for (var i=0; i<len; i++) {
125 if (data[i]['notebook_path']==null) {
126 nb_path = data[i]['notebook_name'];
125 if (data[i]['path']==null) {
126 nb_path = data[i]['name'];
127 127 }
128 128 else {
129 nb_path = data[i]['notebook_path'] + data[i]['notebook_name'];
129 nb_path = data[i]['path'] + data[i]['name'];
130 130 }
131 this.sessions[nb_path]= data[i]['session_id'];
131 this.sessions[nb_path]= data[i]['id'];
132 132 }
133 133 };
134 134 this.load_list();
135 135 };
136 136
137 137 NotebookList.prototype.load_list = function () {
138 138 var that = this;
139 139 var settings = {
140 140 processData : false,
141 141 cache : false,
142 142 type : "GET",
143 143 dataType : "json",
144 144 success : $.proxy(this.list_loaded, this),
145 145 error : $.proxy( function(){
146 146 that.list_loaded([], null, null, {msg:"Error connecting to server."});
147 147 },this)
148 148 };
149 149
150 150 var url = this.baseProjectUrl() + 'api/notebooks/' + this.notebookPath();
151 151 $.ajax(url, settings);
152 152 };
153 153
154 154
155 155 NotebookList.prototype.list_loaded = function (data, status, xhr, param) {
156 156 var message = 'Notebook list empty.';
157 157 if (param !== undefined && param.msg) {
158 158 var message = param.msg;
159 159 }
160 160 var len = data.length;
161 161 this.clear_list();
162 162 if(len == 0)
163 163 {
164 164 $(this.new_notebook_item(0))
165 165 .append(
166 166 $('<div style="margin:auto;text-align:center;color:grey"/>')
167 167 .text(message)
168 168 )
169 169 }
170 170 for (var i=0; i<len; i++) {
171 var name = data[i].notebook_name;
171 var name = data[i].name;
172 172 var path = this.notebookPath();
173 173 var nbname = name.split(".")[0];
174 174 var item = this.new_notebook_item(i);
175 175 this.add_link(path, nbname, item);
176 176 name = this.notebookPath() + name;
177 177 if(this.sessions[name] == undefined){
178 178 this.add_delete_button(item);
179 179 } else {
180 180 this.add_shutdown_button(item,this.sessions[name]);
181 181 }
182 182 };
183 183 };
184 184
185 185
186 186 NotebookList.prototype.new_notebook_item = function (index) {
187 187 var item = $('<div/>').addClass("list_item").addClass("row-fluid");
188 188 // item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
189 189 // item.css('border-top-style','none');
190 190 item.append($("<div/>").addClass("span12").append(
191 191 $("<a/>").addClass("item_link").append(
192 192 $("<span/>").addClass("item_name")
193 193 )
194 194 ).append(
195 195 $('<div/>').addClass("item_buttons btn-group pull-right")
196 196 ));
197 197
198 198 if (index === -1) {
199 199 this.element.append(item);
200 200 } else {
201 201 this.element.children().eq(index).after(item);
202 202 }
203 203 return item;
204 204 };
205 205
206 206
207 207 NotebookList.prototype.add_link = function (path, nbname, item) {
208 208 item.data('nbname', nbname);
209 209 item.data('path', path);
210 210 item.find(".item_name").text(nbname);
211 211 item.find("a.item_link")
212 212 .attr('href', this.baseProjectUrl() + "notebooks/" + this.notebookPath() + nbname + ".ipynb")
213 213 .attr('target','_blank');
214 214 };
215 215
216 216
217 217 NotebookList.prototype.add_name_input = function (nbname, item) {
218 218 item.data('nbname', nbname);
219 219 item.find(".item_name").empty().append(
220 220 $('<input/>')
221 221 .addClass("nbname_input")
222 222 .attr('value', nbname)
223 223 .attr('size', '30')
224 224 .attr('type', 'text')
225 225 );
226 226 };
227 227
228 228
229 229 NotebookList.prototype.add_notebook_data = function (data, item) {
230 230 item.data('nbdata',data);
231 231 };
232 232
233 233
234 234 NotebookList.prototype.add_shutdown_button = function (item, session) {
235 235 var that = this;
236 236 var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-mini").
237 237 click(function (e) {
238 238 var settings = {
239 239 processData : false,
240 240 cache : false,
241 241 type : "DELETE",
242 242 dataType : "json",
243 243 success : function () {
244 244 that.load_sessions();
245 245 }
246 246 };
247 247 var url = that.baseProjectUrl() + 'api/sessions/' + session;
248 248 $.ajax(url, settings);
249 249 return false;
250 250 });
251 251 // var new_buttons = item.find('a'); // shutdown_button;
252 252 item.find(".item_buttons").html("").append(shutdown_button);
253 253 };
254 254
255 255 NotebookList.prototype.add_delete_button = function (item) {
256 256 var new_buttons = $('<span/>').addClass("btn-group pull-right");
257 257 var notebooklist = this;
258 258 var delete_button = $("<button/>").text("Delete").addClass("btn btn-mini").
259 259 click(function (e) {
260 260 // $(this) is the button that was clicked.
261 261 var that = $(this);
262 262 // We use the nbname and notebook_id from the parent notebook_item element's
263 263 // data because the outer scopes values change as we iterate through the loop.
264 264 var parent_item = that.parents('div.list_item');
265 265 var nbname = parent_item.data('nbname');
266 266 var message = 'Are you sure you want to permanently delete the notebook: ' + nbname + '?';
267 267 IPython.dialog.modal({
268 268 title : "Delete notebook",
269 269 body : message,
270 270 buttons : {
271 271 Delete : {
272 272 class: "btn-danger",
273 273 click: function() {
274 274 var settings = {
275 275 processData : false,
276 276 cache : false,
277 277 type : "DELETE",
278 278 dataType : "json",
279 279 success : function (data, status, xhr) {
280 280 parent_item.remove();
281 281 }
282 282 };
283 283 var url = notebooklist.baseProjectUrl() + 'api/notebooks/' + notebooklist.notebookPath() + nbname + '.ipynb';
284 284 $.ajax(url, settings);
285 285 }
286 286 },
287 287 Cancel : {}
288 288 }
289 289 });
290 290 return false;
291 291 });
292 292 item.find(".item_buttons").html("").append(delete_button);
293 293 };
294 294
295 295
296 296 NotebookList.prototype.add_upload_button = function (item) {
297 297 var that = this;
298 298 var upload_button = $('<button/>').text("Upload")
299 299 .addClass('btn btn-primary btn-mini upload_button')
300 300 .click(function (e) {
301 301 var nbname = item.find('.item_name > input').attr('value');
302 302 var nbformat = item.data('nbformat');
303 303 var nbdata = item.data('nbdata');
304 304 var content_type = 'text/plain';
305 305 if (nbformat === 'json') {
306 306 content_type = 'application/json';
307 307 } else if (nbformat === 'py') {
308 308 content_type = 'application/x-python';
309 309 };
310 310 var settings = {
311 311 processData : false,
312 312 cache : false,
313 313 type : 'POST',
314 314 dataType : 'json',
315 315 data : nbdata,
316 316 headers : {'Content-Type': content_type},
317 317 success : function (data, status, xhr) {
318 318 that.add_link(data, nbname, item);
319 319 that.add_delete_button(item);
320 320 }
321 321 };
322 322
323 323 var qs = $.param({name:nbname, format:nbformat});
324 324 var url = that.baseProjectUrl() + 'notebooks?' + qs;
325 325 $.ajax(url, settings);
326 326 return false;
327 327 });
328 328 var cancel_button = $('<button/>').text("Cancel")
329 329 .addClass("btn btn-mini")
330 330 .click(function (e) {
331 331 console.log('cancel click');
332 332 item.remove();
333 333 return false;
334 334 });
335 335 item.find(".item_buttons").empty()
336 336 .append(upload_button)
337 337 .append(cancel_button);
338 338 };
339 339
340 340
341 341 IPython.NotebookList = NotebookList;
342 342
343 343 return IPython;
344 344
345 345 }(IPython));
346 346
General Comments 0
You need to be logged in to leave comments. Login now