##// END OF EJS Templates
better message when notebook format is not supported...
MinRK -
Show More
@@ -1,358 +1,359 b''
1 """A notebook manager that uses the local file system for storage.
1 """A notebook manager that uses the local file system for storage.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import datetime
19 import datetime
20 import io
20 import io
21 import os
21 import os
22 import glob
22 import glob
23 import shutil
23 import shutil
24 from unicodedata import normalize
24 from unicodedata import normalize
25
25
26 from tornado import web
26 from tornado import web
27
27
28 from .nbmanager import NotebookManager
28 from .nbmanager import NotebookManager
29 from IPython.nbformat import current
29 from IPython.nbformat import current
30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
31 from IPython.utils import tz
31 from IPython.utils import tz
32
32
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34 # Classes
34 # Classes
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36
36
37 class FileNotebookManager(NotebookManager):
37 class FileNotebookManager(NotebookManager):
38
38
39 save_script = Bool(False, config=True,
39 save_script = Bool(False, config=True,
40 help="""Automatically create a Python script when saving the notebook.
40 help="""Automatically create a Python script when saving the notebook.
41
41
42 For easier use of import, %run and %load across notebooks, a
42 For easier use of import, %run and %load across notebooks, a
43 <notebook-name>.py script will be created next to any
43 <notebook-name>.py script will be created next to any
44 <notebook-name>.ipynb on each save. This can also be set with the
44 <notebook-name>.ipynb on each save. This can also be set with the
45 short `--script` flag.
45 short `--script` flag.
46 """
46 """
47 )
47 )
48
48
49 checkpoint_dir = Unicode(config=True,
49 checkpoint_dir = Unicode(config=True,
50 help="""The location in which to keep notebook checkpoints
50 help="""The location in which to keep notebook checkpoints
51
51
52 By default, it is notebook-dir/.ipynb_checkpoints
52 By default, it is notebook-dir/.ipynb_checkpoints
53 """
53 """
54 )
54 )
55 def _checkpoint_dir_default(self):
55 def _checkpoint_dir_default(self):
56 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
56 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
57
57
58 def _checkpoint_dir_changed(self, name, old, new):
58 def _checkpoint_dir_changed(self, name, old, new):
59 """do a bit of validation of the checkpoint dir"""
59 """do a bit of validation of the checkpoint dir"""
60 if not os.path.isabs(new):
60 if not os.path.isabs(new):
61 # If we receive a non-absolute path, make it absolute.
61 # If we receive a non-absolute path, make it absolute.
62 abs_new = os.path.abspath(new)
62 abs_new = os.path.abspath(new)
63 self.checkpoint_dir = abs_new
63 self.checkpoint_dir = abs_new
64 return
64 return
65 if os.path.exists(new) and not os.path.isdir(new):
65 if os.path.exists(new) and not os.path.isdir(new):
66 raise TraitError("checkpoint dir %r is not a directory" % new)
66 raise TraitError("checkpoint dir %r is not a directory" % new)
67 if not os.path.exists(new):
67 if not os.path.exists(new):
68 self.log.info("Creating checkpoint dir %s", new)
68 self.log.info("Creating checkpoint dir %s", new)
69 try:
69 try:
70 os.mkdir(new)
70 os.mkdir(new)
71 except:
71 except:
72 raise TraitError("Couldn't create checkpoint dir %r" % new)
72 raise TraitError("Couldn't create checkpoint dir %r" % new)
73
73
74 filename_ext = Unicode(u'.ipynb')
74 filename_ext = Unicode(u'.ipynb')
75
75
76 # Map notebook names to notebook_ids
76 # Map notebook names to notebook_ids
77 rev_mapping = Dict()
77 rev_mapping = Dict()
78
78
79 def get_notebook_names(self):
79 def get_notebook_names(self):
80 """List all notebook names in the notebook dir."""
80 """List all notebook names in the notebook dir."""
81 names = glob.glob(os.path.join(self.notebook_dir,
81 names = glob.glob(os.path.join(self.notebook_dir,
82 '*' + self.filename_ext))
82 '*' + self.filename_ext))
83 names = [normalize('NFC', os.path.splitext(os.path.basename(name))[0])
83 names = [normalize('NFC', os.path.splitext(os.path.basename(name))[0])
84 for name in names]
84 for name in names]
85 return names
85 return names
86
86
87 def list_notebooks(self):
87 def list_notebooks(self):
88 """List all notebooks in the notebook dir."""
88 """List all notebooks in the notebook dir."""
89 names = self.get_notebook_names()
89 names = self.get_notebook_names()
90
90
91 data = []
91 data = []
92 for name in names:
92 for name in names:
93 if name not in self.rev_mapping:
93 if name not in self.rev_mapping:
94 notebook_id = self.new_notebook_id(name)
94 notebook_id = self.new_notebook_id(name)
95 else:
95 else:
96 notebook_id = self.rev_mapping[name]
96 notebook_id = self.rev_mapping[name]
97 data.append(dict(notebook_id=notebook_id,name=name))
97 data.append(dict(notebook_id=notebook_id,name=name))
98 data = sorted(data, key=lambda item: item['name'])
98 data = sorted(data, key=lambda item: item['name'])
99 return data
99 return data
100
100
101 def new_notebook_id(self, name):
101 def new_notebook_id(self, name):
102 """Generate a new notebook_id for a name and store its mappings."""
102 """Generate a new notebook_id for a name and store its mappings."""
103 notebook_id = super(FileNotebookManager, self).new_notebook_id(name)
103 notebook_id = super(FileNotebookManager, self).new_notebook_id(name)
104 self.rev_mapping[name] = notebook_id
104 self.rev_mapping[name] = notebook_id
105 return notebook_id
105 return notebook_id
106
106
107 def delete_notebook_id(self, notebook_id):
107 def delete_notebook_id(self, notebook_id):
108 """Delete a notebook's id in the mapping."""
108 """Delete a notebook's id in the mapping."""
109 name = self.mapping[notebook_id]
109 name = self.mapping[notebook_id]
110 super(FileNotebookManager, self).delete_notebook_id(notebook_id)
110 super(FileNotebookManager, self).delete_notebook_id(notebook_id)
111 del self.rev_mapping[name]
111 del self.rev_mapping[name]
112
112
113 def notebook_exists(self, notebook_id):
113 def notebook_exists(self, notebook_id):
114 """Does a notebook exist?"""
114 """Does a notebook exist?"""
115 exists = super(FileNotebookManager, self).notebook_exists(notebook_id)
115 exists = super(FileNotebookManager, self).notebook_exists(notebook_id)
116 if not exists:
116 if not exists:
117 return False
117 return False
118 path = self.get_path_by_name(self.mapping[notebook_id])
118 path = self.get_path_by_name(self.mapping[notebook_id])
119 return os.path.isfile(path)
119 return os.path.isfile(path)
120
120
121 def get_name(self, notebook_id):
121 def get_name(self, notebook_id):
122 """get a notebook name, raising 404 if not found"""
122 """get a notebook name, raising 404 if not found"""
123 try:
123 try:
124 name = self.mapping[notebook_id]
124 name = self.mapping[notebook_id]
125 except KeyError:
125 except KeyError:
126 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
126 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
127 return name
127 return name
128
128
129 def get_path(self, notebook_id):
129 def get_path(self, notebook_id):
130 """Return a full path to a notebook given its notebook_id."""
130 """Return a full path to a notebook given its notebook_id."""
131 name = self.get_name(notebook_id)
131 name = self.get_name(notebook_id)
132 return self.get_path_by_name(name)
132 return self.get_path_by_name(name)
133
133
134 def get_path_by_name(self, name):
134 def get_path_by_name(self, name):
135 """Return a full path to a notebook given its name."""
135 """Return a full path to a notebook given its name."""
136 filename = name + self.filename_ext
136 filename = name + self.filename_ext
137 path = os.path.join(self.notebook_dir, filename)
137 path = os.path.join(self.notebook_dir, filename)
138 return path
138 return path
139
139
140 def read_notebook_object_from_path(self, path):
140 def read_notebook_object_from_path(self, path):
141 """read a notebook object from a path"""
141 """read a notebook object from a path"""
142 info = os.stat(path)
142 info = os.stat(path)
143 last_modified = tz.utcfromtimestamp(info.st_mtime)
143 last_modified = tz.utcfromtimestamp(info.st_mtime)
144 with open(path,'r') as f:
144 with open(path,'r') as f:
145 s = f.read()
145 s = f.read()
146 try:
146 try:
147 # v1 and v2 and json in the .ipynb files.
147 # v1 and v2 and json in the .ipynb files.
148 nb = current.reads(s, u'json')
148 nb = current.reads(s, u'json')
149 except Exception as e:
149 except ValueError as e:
150 raise web.HTTPError(500, u'Unreadable JSON notebook: %s' % e)
150 msg = u"Unreadable Notebook: %s" % e
151 raise web.HTTPError(400, msg, reason=msg)
151 return last_modified, nb
152 return last_modified, nb
152
153
153 def read_notebook_object(self, notebook_id):
154 def read_notebook_object(self, notebook_id):
154 """Get the Notebook representation of a notebook by notebook_id."""
155 """Get the Notebook representation of a notebook by notebook_id."""
155 path = self.get_path(notebook_id)
156 path = self.get_path(notebook_id)
156 if not os.path.isfile(path):
157 if not os.path.isfile(path):
157 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
158 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
158 last_modified, nb = self.read_notebook_object_from_path(path)
159 last_modified, nb = self.read_notebook_object_from_path(path)
159 # Always use the filename as the notebook name.
160 # Always use the filename as the notebook name.
160 # Eventually we will get rid of the notebook name in the metadata
161 # Eventually we will get rid of the notebook name in the metadata
161 # but for now, that name is just an empty string. Until the notebooks
162 # but for now, that name is just an empty string. Until the notebooks
162 # web service knows about names in URLs we still pass the name
163 # web service knows about names in URLs we still pass the name
163 # back to the web app using the metadata though.
164 # back to the web app using the metadata though.
164 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
165 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
165 return last_modified, nb
166 return last_modified, nb
166
167
167 def write_notebook_object(self, nb, notebook_id=None):
168 def write_notebook_object(self, nb, notebook_id=None):
168 """Save an existing notebook object by notebook_id."""
169 """Save an existing notebook object by notebook_id."""
169 try:
170 try:
170 new_name = normalize('NFC', nb.metadata.name)
171 new_name = normalize('NFC', nb.metadata.name)
171 except AttributeError:
172 except AttributeError:
172 raise web.HTTPError(400, u'Missing notebook name')
173 raise web.HTTPError(400, u'Missing notebook name')
173
174
174 if notebook_id is None:
175 if notebook_id is None:
175 notebook_id = self.new_notebook_id(new_name)
176 notebook_id = self.new_notebook_id(new_name)
176
177
177 if notebook_id not in self.mapping:
178 if notebook_id not in self.mapping:
178 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
179 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
179
180
180 old_name = self.mapping[notebook_id]
181 old_name = self.mapping[notebook_id]
181 old_checkpoints = self.list_checkpoints(notebook_id)
182 old_checkpoints = self.list_checkpoints(notebook_id)
182 path = self.get_path_by_name(new_name)
183 path = self.get_path_by_name(new_name)
183
184
184 # Right before we save the notebook, we write an empty string as the
185 # Right before we save the notebook, we write an empty string as the
185 # notebook name in the metadata. This is to prepare for removing
186 # notebook name in the metadata. This is to prepare for removing
186 # this attribute entirely post 1.0. The web app still uses the metadata
187 # this attribute entirely post 1.0. The web app still uses the metadata
187 # name for now.
188 # name for now.
188 nb.metadata.name = u''
189 nb.metadata.name = u''
189
190
190 try:
191 try:
191 self.log.debug("Autosaving notebook %s", path)
192 self.log.debug("Autosaving notebook %s", path)
192 with open(path,'w') as f:
193 with open(path,'w') as f:
193 current.write(nb, f, u'json')
194 current.write(nb, f, u'json')
194 except Exception as e:
195 except Exception as e:
195 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % e)
196 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % e)
196
197
197 # save .py script as well
198 # save .py script as well
198 if self.save_script:
199 if self.save_script:
199 pypath = os.path.splitext(path)[0] + '.py'
200 pypath = os.path.splitext(path)[0] + '.py'
200 self.log.debug("Writing script %s", pypath)
201 self.log.debug("Writing script %s", pypath)
201 try:
202 try:
202 with io.open(pypath,'w', encoding='utf-8') as f:
203 with io.open(pypath,'w', encoding='utf-8') as f:
203 current.write(nb, f, u'py')
204 current.write(nb, f, u'py')
204 except Exception as e:
205 except Exception as e:
205 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
206 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
206
207
207 # remove old files if the name changed
208 # remove old files if the name changed
208 if old_name != new_name:
209 if old_name != new_name:
209 # update mapping
210 # update mapping
210 self.mapping[notebook_id] = new_name
211 self.mapping[notebook_id] = new_name
211 self.rev_mapping[new_name] = notebook_id
212 self.rev_mapping[new_name] = notebook_id
212 del self.rev_mapping[old_name]
213 del self.rev_mapping[old_name]
213
214
214 # remove renamed original, if it exists
215 # remove renamed original, if it exists
215 old_path = self.get_path_by_name(old_name)
216 old_path = self.get_path_by_name(old_name)
216 if os.path.isfile(old_path):
217 if os.path.isfile(old_path):
217 self.log.debug("unlinking notebook %s", old_path)
218 self.log.debug("unlinking notebook %s", old_path)
218 os.unlink(old_path)
219 os.unlink(old_path)
219
220
220 # cleanup old script, if it exists
221 # cleanup old script, if it exists
221 if self.save_script:
222 if self.save_script:
222 old_pypath = os.path.splitext(old_path)[0] + '.py'
223 old_pypath = os.path.splitext(old_path)[0] + '.py'
223 if os.path.isfile(old_pypath):
224 if os.path.isfile(old_pypath):
224 self.log.debug("unlinking script %s", old_pypath)
225 self.log.debug("unlinking script %s", old_pypath)
225 os.unlink(old_pypath)
226 os.unlink(old_pypath)
226
227
227 # rename checkpoints to follow file
228 # rename checkpoints to follow file
228 for cp in old_checkpoints:
229 for cp in old_checkpoints:
229 checkpoint_id = cp['checkpoint_id']
230 checkpoint_id = cp['checkpoint_id']
230 old_cp_path = self.get_checkpoint_path_by_name(old_name, checkpoint_id)
231 old_cp_path = self.get_checkpoint_path_by_name(old_name, checkpoint_id)
231 new_cp_path = self.get_checkpoint_path_by_name(new_name, checkpoint_id)
232 new_cp_path = self.get_checkpoint_path_by_name(new_name, checkpoint_id)
232 if os.path.isfile(old_cp_path):
233 if os.path.isfile(old_cp_path):
233 self.log.debug("renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
234 self.log.debug("renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
234 os.rename(old_cp_path, new_cp_path)
235 os.rename(old_cp_path, new_cp_path)
235
236
236 return notebook_id
237 return notebook_id
237
238
238 def delete_notebook(self, notebook_id):
239 def delete_notebook(self, notebook_id):
239 """Delete notebook by notebook_id."""
240 """Delete notebook by notebook_id."""
240 nb_path = self.get_path(notebook_id)
241 nb_path = self.get_path(notebook_id)
241 if not os.path.isfile(nb_path):
242 if not os.path.isfile(nb_path):
242 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
243 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
243
244
244 # clear checkpoints
245 # clear checkpoints
245 for checkpoint in self.list_checkpoints(notebook_id):
246 for checkpoint in self.list_checkpoints(notebook_id):
246 checkpoint_id = checkpoint['checkpoint_id']
247 checkpoint_id = checkpoint['checkpoint_id']
247 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
248 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
248 self.log.debug(path)
249 self.log.debug(path)
249 if os.path.isfile(path):
250 if os.path.isfile(path):
250 self.log.debug("unlinking checkpoint %s", path)
251 self.log.debug("unlinking checkpoint %s", path)
251 os.unlink(path)
252 os.unlink(path)
252
253
253 self.log.debug("unlinking notebook %s", nb_path)
254 self.log.debug("unlinking notebook %s", nb_path)
254 os.unlink(nb_path)
255 os.unlink(nb_path)
255 self.delete_notebook_id(notebook_id)
256 self.delete_notebook_id(notebook_id)
256
257
257 def increment_filename(self, basename):
258 def increment_filename(self, basename):
258 """Return a non-used filename of the form basename<int>.
259 """Return a non-used filename of the form basename<int>.
259
260
260 This searches through the filenames (basename0, basename1, ...)
261 This searches through the filenames (basename0, basename1, ...)
261 until is find one that is not already being used. It is used to
262 until is find one that is not already being used. It is used to
262 create Untitled and Copy names that are unique.
263 create Untitled and Copy names that are unique.
263 """
264 """
264 i = 0
265 i = 0
265 while True:
266 while True:
266 name = u'%s%i' % (basename,i)
267 name = u'%s%i' % (basename,i)
267 path = self.get_path_by_name(name)
268 path = self.get_path_by_name(name)
268 if not os.path.isfile(path):
269 if not os.path.isfile(path):
269 break
270 break
270 else:
271 else:
271 i = i+1
272 i = i+1
272 return name
273 return name
273
274
274 # Checkpoint-related utilities
275 # Checkpoint-related utilities
275
276
276 def get_checkpoint_path_by_name(self, name, checkpoint_id):
277 def get_checkpoint_path_by_name(self, name, checkpoint_id):
277 """Return a full path to a notebook checkpoint, given its name and checkpoint id."""
278 """Return a full path to a notebook checkpoint, given its name and checkpoint id."""
278 filename = u"{name}-{checkpoint_id}{ext}".format(
279 filename = u"{name}-{checkpoint_id}{ext}".format(
279 name=name,
280 name=name,
280 checkpoint_id=checkpoint_id,
281 checkpoint_id=checkpoint_id,
281 ext=self.filename_ext,
282 ext=self.filename_ext,
282 )
283 )
283 path = os.path.join(self.checkpoint_dir, filename)
284 path = os.path.join(self.checkpoint_dir, filename)
284 return path
285 return path
285
286
286 def get_checkpoint_path(self, notebook_id, checkpoint_id):
287 def get_checkpoint_path(self, notebook_id, checkpoint_id):
287 """find the path to a checkpoint"""
288 """find the path to a checkpoint"""
288 name = self.get_name(notebook_id)
289 name = self.get_name(notebook_id)
289 return self.get_checkpoint_path_by_name(name, checkpoint_id)
290 return self.get_checkpoint_path_by_name(name, checkpoint_id)
290
291
291 def get_checkpoint_info(self, notebook_id, checkpoint_id):
292 def get_checkpoint_info(self, notebook_id, checkpoint_id):
292 """construct the info dict for a given checkpoint"""
293 """construct the info dict for a given checkpoint"""
293 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
294 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
294 stats = os.stat(path)
295 stats = os.stat(path)
295 last_modified = tz.utcfromtimestamp(stats.st_mtime)
296 last_modified = tz.utcfromtimestamp(stats.st_mtime)
296 info = dict(
297 info = dict(
297 checkpoint_id = checkpoint_id,
298 checkpoint_id = checkpoint_id,
298 last_modified = last_modified,
299 last_modified = last_modified,
299 )
300 )
300
301
301 return info
302 return info
302
303
303 # public checkpoint API
304 # public checkpoint API
304
305
305 def create_checkpoint(self, notebook_id):
306 def create_checkpoint(self, notebook_id):
306 """Create a checkpoint from the current state of a notebook"""
307 """Create a checkpoint from the current state of a notebook"""
307 nb_path = self.get_path(notebook_id)
308 nb_path = self.get_path(notebook_id)
308 # only the one checkpoint ID:
309 # only the one checkpoint ID:
309 checkpoint_id = u"checkpoint"
310 checkpoint_id = u"checkpoint"
310 cp_path = self.get_checkpoint_path(notebook_id, checkpoint_id)
311 cp_path = self.get_checkpoint_path(notebook_id, checkpoint_id)
311 self.log.debug("creating checkpoint for notebook %s", notebook_id)
312 self.log.debug("creating checkpoint for notebook %s", notebook_id)
312 if not os.path.exists(self.checkpoint_dir):
313 if not os.path.exists(self.checkpoint_dir):
313 os.mkdir(self.checkpoint_dir)
314 os.mkdir(self.checkpoint_dir)
314 shutil.copy2(nb_path, cp_path)
315 shutil.copy2(nb_path, cp_path)
315
316
316 # return the checkpoint info
317 # return the checkpoint info
317 return self.get_checkpoint_info(notebook_id, checkpoint_id)
318 return self.get_checkpoint_info(notebook_id, checkpoint_id)
318
319
319 def list_checkpoints(self, notebook_id):
320 def list_checkpoints(self, notebook_id):
320 """list the checkpoints for a given notebook
321 """list the checkpoints for a given notebook
321
322
322 This notebook manager currently only supports one checkpoint per notebook.
323 This notebook manager currently only supports one checkpoint per notebook.
323 """
324 """
324 checkpoint_id = u"checkpoint"
325 checkpoint_id = u"checkpoint"
325 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
326 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
326 if not os.path.exists(path):
327 if not os.path.exists(path):
327 return []
328 return []
328 else:
329 else:
329 return [self.get_checkpoint_info(notebook_id, checkpoint_id)]
330 return [self.get_checkpoint_info(notebook_id, checkpoint_id)]
330
331
331
332
332 def restore_checkpoint(self, notebook_id, checkpoint_id):
333 def restore_checkpoint(self, notebook_id, checkpoint_id):
333 """restore a notebook to a checkpointed state"""
334 """restore a notebook to a checkpointed state"""
334 self.log.info("restoring Notebook %s from checkpoint %s", notebook_id, checkpoint_id)
335 self.log.info("restoring Notebook %s from checkpoint %s", notebook_id, checkpoint_id)
335 nb_path = self.get_path(notebook_id)
336 nb_path = self.get_path(notebook_id)
336 cp_path = self.get_checkpoint_path(notebook_id, checkpoint_id)
337 cp_path = self.get_checkpoint_path(notebook_id, checkpoint_id)
337 if not os.path.isfile(cp_path):
338 if not os.path.isfile(cp_path):
338 self.log.debug("checkpoint file does not exist: %s", cp_path)
339 self.log.debug("checkpoint file does not exist: %s", cp_path)
339 raise web.HTTPError(404,
340 raise web.HTTPError(404,
340 u'Notebook checkpoint does not exist: %s-%s' % (notebook_id, checkpoint_id)
341 u'Notebook checkpoint does not exist: %s-%s' % (notebook_id, checkpoint_id)
341 )
342 )
342 # ensure notebook is readable (never restore from an unreadable notebook)
343 # ensure notebook is readable (never restore from an unreadable notebook)
343 last_modified, nb = self.read_notebook_object_from_path(cp_path)
344 last_modified, nb = self.read_notebook_object_from_path(cp_path)
344 shutil.copy2(cp_path, nb_path)
345 shutil.copy2(cp_path, nb_path)
345 self.log.debug("copying %s -> %s", cp_path, nb_path)
346 self.log.debug("copying %s -> %s", cp_path, nb_path)
346
347
347 def delete_checkpoint(self, notebook_id, checkpoint_id):
348 def delete_checkpoint(self, notebook_id, checkpoint_id):
348 """delete a notebook's checkpoint"""
349 """delete a notebook's checkpoint"""
349 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
350 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
350 if not os.path.isfile(path):
351 if not os.path.isfile(path):
351 raise web.HTTPError(404,
352 raise web.HTTPError(404,
352 u'Notebook checkpoint does not exist: %s-%s' % (notebook_id, checkpoint_id)
353 u'Notebook checkpoint does not exist: %s-%s' % (notebook_id, checkpoint_id)
353 )
354 )
354 self.log.debug("unlinking %s", path)
355 self.log.debug("unlinking %s", path)
355 os.unlink(path)
356 os.unlink(path)
356
357
357 def info_string(self):
358 def info_string(self):
358 return "Serving notebooks from local directory: %s" % self.notebook_dir
359 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,2037 +1,2037 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // Notebook
9 // Notebook
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13
13
14 var utils = IPython.utils;
14 var utils = IPython.utils;
15 var key = IPython.utils.keycodes;
15 var key = IPython.utils.keycodes;
16
16
17 /**
17 /**
18 * A notebook contains and manages cells.
18 * A notebook contains and manages cells.
19 *
19 *
20 * @class Notebook
20 * @class Notebook
21 * @constructor
21 * @constructor
22 * @param {String} selector A jQuery selector for the notebook's DOM element
22 * @param {String} selector A jQuery selector for the notebook's DOM element
23 * @param {Object} [options] A config object
23 * @param {Object} [options] A config object
24 */
24 */
25 var Notebook = function (selector, options) {
25 var Notebook = function (selector, options) {
26 var options = options || {};
26 var options = options || {};
27 this._baseProjectUrl = options.baseProjectUrl;
27 this._baseProjectUrl = options.baseProjectUrl;
28 this.read_only = options.read_only || IPython.read_only;
28 this.read_only = options.read_only || IPython.read_only;
29
29
30 this.element = $(selector);
30 this.element = $(selector);
31 this.element.scroll();
31 this.element.scroll();
32 this.element.data("notebook", this);
32 this.element.data("notebook", this);
33 this.next_prompt_number = 1;
33 this.next_prompt_number = 1;
34 this.kernel = null;
34 this.kernel = null;
35 this.clipboard = null;
35 this.clipboard = null;
36 this.undelete_backup = null;
36 this.undelete_backup = null;
37 this.undelete_index = null;
37 this.undelete_index = null;
38 this.undelete_below = false;
38 this.undelete_below = false;
39 this.paste_enabled = false;
39 this.paste_enabled = false;
40 this.set_dirty(false);
40 this.set_dirty(false);
41 this.metadata = {};
41 this.metadata = {};
42 this._checkpoint_after_save = false;
42 this._checkpoint_after_save = false;
43 this.last_checkpoint = null;
43 this.last_checkpoint = null;
44 this.autosave_interval = 0;
44 this.autosave_interval = 0;
45 this.autosave_timer = null;
45 this.autosave_timer = null;
46 // autosave *at most* every two minutes
46 // autosave *at most* every two minutes
47 this.minimum_autosave_interval = 120000;
47 this.minimum_autosave_interval = 120000;
48 // single worksheet for now
48 // single worksheet for now
49 this.worksheet_metadata = {};
49 this.worksheet_metadata = {};
50 this.control_key_active = false;
50 this.control_key_active = false;
51 this.notebook_id = null;
51 this.notebook_id = null;
52 this.notebook_name = null;
52 this.notebook_name = null;
53 this.notebook_name_blacklist_re = /[\/\\:]/;
53 this.notebook_name_blacklist_re = /[\/\\:]/;
54 this.nbformat = 3 // Increment this when changing the nbformat
54 this.nbformat = 3 // Increment this when changing the nbformat
55 this.nbformat_minor = 0 // Increment this when changing the nbformat
55 this.nbformat_minor = 0 // Increment this when changing the nbformat
56 this.style();
56 this.style();
57 this.create_elements();
57 this.create_elements();
58 this.bind_events();
58 this.bind_events();
59 };
59 };
60
60
61 /**
61 /**
62 * Tweak the notebook's CSS style.
62 * Tweak the notebook's CSS style.
63 *
63 *
64 * @method style
64 * @method style
65 */
65 */
66 Notebook.prototype.style = function () {
66 Notebook.prototype.style = function () {
67 $('div#notebook').addClass('border-box-sizing');
67 $('div#notebook').addClass('border-box-sizing');
68 };
68 };
69
69
70 /**
70 /**
71 * Get the root URL of the notebook server.
71 * Get the root URL of the notebook server.
72 *
72 *
73 * @method baseProjectUrl
73 * @method baseProjectUrl
74 * @return {String} The base project URL
74 * @return {String} The base project URL
75 */
75 */
76 Notebook.prototype.baseProjectUrl = function(){
76 Notebook.prototype.baseProjectUrl = function(){
77 return this._baseProjectUrl || $('body').data('baseProjectUrl');
77 return this._baseProjectUrl || $('body').data('baseProjectUrl');
78 };
78 };
79
79
80 /**
80 /**
81 * Create an HTML and CSS representation of the notebook.
81 * Create an HTML and CSS representation of the notebook.
82 *
82 *
83 * @method create_elements
83 * @method create_elements
84 */
84 */
85 Notebook.prototype.create_elements = function () {
85 Notebook.prototype.create_elements = function () {
86 // We add this end_space div to the end of the notebook div to:
86 // We add this end_space div to the end of the notebook div to:
87 // i) provide a margin between the last cell and the end of the notebook
87 // i) provide a margin between the last cell and the end of the notebook
88 // ii) to prevent the div from scrolling up when the last cell is being
88 // ii) to prevent the div from scrolling up when the last cell is being
89 // edited, but is too low on the page, which browsers will do automatically.
89 // edited, but is too low on the page, which browsers will do automatically.
90 var that = this;
90 var that = this;
91 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
91 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
92 var end_space = $('<div/>').addClass('end_space');
92 var end_space = $('<div/>').addClass('end_space');
93 end_space.dblclick(function (e) {
93 end_space.dblclick(function (e) {
94 if (that.read_only) return;
94 if (that.read_only) return;
95 var ncells = that.ncells();
95 var ncells = that.ncells();
96 that.insert_cell_below('code',ncells-1);
96 that.insert_cell_below('code',ncells-1);
97 });
97 });
98 this.element.append(this.container);
98 this.element.append(this.container);
99 this.container.append(end_space);
99 this.container.append(end_space);
100 $('div#notebook').addClass('border-box-sizing');
100 $('div#notebook').addClass('border-box-sizing');
101 };
101 };
102
102
103 /**
103 /**
104 * Bind JavaScript events: key presses and custom IPython events.
104 * Bind JavaScript events: key presses and custom IPython events.
105 *
105 *
106 * @method bind_events
106 * @method bind_events
107 */
107 */
108 Notebook.prototype.bind_events = function () {
108 Notebook.prototype.bind_events = function () {
109 var that = this;
109 var that = this;
110
110
111 $([IPython.events]).on('set_next_input.Notebook', function (event, data) {
111 $([IPython.events]).on('set_next_input.Notebook', function (event, data) {
112 var index = that.find_cell_index(data.cell);
112 var index = that.find_cell_index(data.cell);
113 var new_cell = that.insert_cell_below('code',index);
113 var new_cell = that.insert_cell_below('code',index);
114 new_cell.set_text(data.text);
114 new_cell.set_text(data.text);
115 that.dirty = true;
115 that.dirty = true;
116 });
116 });
117
117
118 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
118 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
119 that.dirty = data.value;
119 that.dirty = data.value;
120 });
120 });
121
121
122 $([IPython.events]).on('select.Cell', function (event, data) {
122 $([IPython.events]).on('select.Cell', function (event, data) {
123 var index = that.find_cell_index(data.cell);
123 var index = that.find_cell_index(data.cell);
124 that.select(index);
124 that.select(index);
125 });
125 });
126
126
127 $([IPython.events]).on('status_autorestarting.Kernel', function () {
127 $([IPython.events]).on('status_autorestarting.Kernel', function () {
128 IPython.dialog.modal({
128 IPython.dialog.modal({
129 title: "Kernel Restarting",
129 title: "Kernel Restarting",
130 body: "The kernel appears to have died. It will restart automatically.",
130 body: "The kernel appears to have died. It will restart automatically.",
131 buttons: {
131 buttons: {
132 OK : {
132 OK : {
133 class : "btn-primary"
133 class : "btn-primary"
134 }
134 }
135 }
135 }
136 });
136 });
137 });
137 });
138
138
139
139
140 $(document).keydown(function (event) {
140 $(document).keydown(function (event) {
141 // console.log(event);
141 // console.log(event);
142 if (that.read_only) return true;
142 if (that.read_only) return true;
143
143
144 // Save (CTRL+S) or (AppleKey+S)
144 // Save (CTRL+S) or (AppleKey+S)
145 //metaKey = applekey on mac
145 //metaKey = applekey on mac
146 if ((event.ctrlKey || event.metaKey) && event.keyCode==83) {
146 if ((event.ctrlKey || event.metaKey) && event.keyCode==83) {
147 that.save_checkpoint();
147 that.save_checkpoint();
148 event.preventDefault();
148 event.preventDefault();
149 return false;
149 return false;
150 } else if (event.which === key.ESC) {
150 } else if (event.which === key.ESC) {
151 // Intercept escape at highest level to avoid closing
151 // Intercept escape at highest level to avoid closing
152 // websocket connection with firefox
152 // websocket connection with firefox
153 IPython.pager.collapse();
153 IPython.pager.collapse();
154 event.preventDefault();
154 event.preventDefault();
155 } else if (event.which === key.SHIFT) {
155 } else if (event.which === key.SHIFT) {
156 // ignore shift keydown
156 // ignore shift keydown
157 return true;
157 return true;
158 }
158 }
159 if (event.which === key.UPARROW && !event.shiftKey) {
159 if (event.which === key.UPARROW && !event.shiftKey) {
160 var cell = that.get_selected_cell();
160 var cell = that.get_selected_cell();
161 if (cell && cell.at_top()) {
161 if (cell && cell.at_top()) {
162 event.preventDefault();
162 event.preventDefault();
163 that.select_prev();
163 that.select_prev();
164 };
164 };
165 } else if (event.which === key.DOWNARROW && !event.shiftKey) {
165 } else if (event.which === key.DOWNARROW && !event.shiftKey) {
166 var cell = that.get_selected_cell();
166 var cell = that.get_selected_cell();
167 if (cell && cell.at_bottom()) {
167 if (cell && cell.at_bottom()) {
168 event.preventDefault();
168 event.preventDefault();
169 that.select_next();
169 that.select_next();
170 };
170 };
171 } else if (event.which === key.ENTER && event.shiftKey) {
171 } else if (event.which === key.ENTER && event.shiftKey) {
172 that.execute_selected_cell();
172 that.execute_selected_cell();
173 return false;
173 return false;
174 } else if (event.which === key.ENTER && event.altKey) {
174 } else if (event.which === key.ENTER && event.altKey) {
175 // Execute code cell, and insert new in place
175 // Execute code cell, and insert new in place
176 that.execute_selected_cell();
176 that.execute_selected_cell();
177 // Only insert a new cell, if we ended up in an already populated cell
177 // Only insert a new cell, if we ended up in an already populated cell
178 if (/\S/.test(that.get_selected_cell().get_text()) == true) {
178 if (/\S/.test(that.get_selected_cell().get_text()) == true) {
179 that.insert_cell_above('code');
179 that.insert_cell_above('code');
180 }
180 }
181 return false;
181 return false;
182 } else if (event.which === key.ENTER && event.ctrlKey) {
182 } else if (event.which === key.ENTER && event.ctrlKey) {
183 that.execute_selected_cell({terminal:true});
183 that.execute_selected_cell({terminal:true});
184 return false;
184 return false;
185 } else if (event.which === 77 && event.ctrlKey && that.control_key_active == false) {
185 } else if (event.which === 77 && event.ctrlKey && that.control_key_active == false) {
186 that.control_key_active = true;
186 that.control_key_active = true;
187 return false;
187 return false;
188 } else if (event.which === 88 && that.control_key_active) {
188 } else if (event.which === 88 && that.control_key_active) {
189 // Cut selected cell = x
189 // Cut selected cell = x
190 that.cut_cell();
190 that.cut_cell();
191 that.control_key_active = false;
191 that.control_key_active = false;
192 return false;
192 return false;
193 } else if (event.which === 67 && that.control_key_active) {
193 } else if (event.which === 67 && that.control_key_active) {
194 // Copy selected cell = c
194 // Copy selected cell = c
195 that.copy_cell();
195 that.copy_cell();
196 that.control_key_active = false;
196 that.control_key_active = false;
197 return false;
197 return false;
198 } else if (event.which === 86 && that.control_key_active) {
198 } else if (event.which === 86 && that.control_key_active) {
199 // Paste below selected cell = v
199 // Paste below selected cell = v
200 that.paste_cell_below();
200 that.paste_cell_below();
201 that.control_key_active = false;
201 that.control_key_active = false;
202 return false;
202 return false;
203 } else if (event.which === 68 && that.control_key_active) {
203 } else if (event.which === 68 && that.control_key_active) {
204 // Delete selected cell = d
204 // Delete selected cell = d
205 that.delete_cell();
205 that.delete_cell();
206 that.control_key_active = false;
206 that.control_key_active = false;
207 return false;
207 return false;
208 } else if (event.which === 65 && that.control_key_active) {
208 } else if (event.which === 65 && that.control_key_active) {
209 // Insert code cell above selected = a
209 // Insert code cell above selected = a
210 that.insert_cell_above('code');
210 that.insert_cell_above('code');
211 that.control_key_active = false;
211 that.control_key_active = false;
212 return false;
212 return false;
213 } else if (event.which === 66 && that.control_key_active) {
213 } else if (event.which === 66 && that.control_key_active) {
214 // Insert code cell below selected = b
214 // Insert code cell below selected = b
215 that.insert_cell_below('code');
215 that.insert_cell_below('code');
216 that.control_key_active = false;
216 that.control_key_active = false;
217 return false;
217 return false;
218 } else if (event.which === 89 && that.control_key_active) {
218 } else if (event.which === 89 && that.control_key_active) {
219 // To code = y
219 // To code = y
220 that.to_code();
220 that.to_code();
221 that.control_key_active = false;
221 that.control_key_active = false;
222 return false;
222 return false;
223 } else if (event.which === 77 && that.control_key_active) {
223 } else if (event.which === 77 && that.control_key_active) {
224 // To markdown = m
224 // To markdown = m
225 that.to_markdown();
225 that.to_markdown();
226 that.control_key_active = false;
226 that.control_key_active = false;
227 return false;
227 return false;
228 } else if (event.which === 84 && that.control_key_active) {
228 } else if (event.which === 84 && that.control_key_active) {
229 // To Raw = t
229 // To Raw = t
230 that.to_raw();
230 that.to_raw();
231 that.control_key_active = false;
231 that.control_key_active = false;
232 return false;
232 return false;
233 } else if (event.which === 49 && that.control_key_active) {
233 } else if (event.which === 49 && that.control_key_active) {
234 // To Heading 1 = 1
234 // To Heading 1 = 1
235 that.to_heading(undefined, 1);
235 that.to_heading(undefined, 1);
236 that.control_key_active = false;
236 that.control_key_active = false;
237 return false;
237 return false;
238 } else if (event.which === 50 && that.control_key_active) {
238 } else if (event.which === 50 && that.control_key_active) {
239 // To Heading 2 = 2
239 // To Heading 2 = 2
240 that.to_heading(undefined, 2);
240 that.to_heading(undefined, 2);
241 that.control_key_active = false;
241 that.control_key_active = false;
242 return false;
242 return false;
243 } else if (event.which === 51 && that.control_key_active) {
243 } else if (event.which === 51 && that.control_key_active) {
244 // To Heading 3 = 3
244 // To Heading 3 = 3
245 that.to_heading(undefined, 3);
245 that.to_heading(undefined, 3);
246 that.control_key_active = false;
246 that.control_key_active = false;
247 return false;
247 return false;
248 } else if (event.which === 52 && that.control_key_active) {
248 } else if (event.which === 52 && that.control_key_active) {
249 // To Heading 4 = 4
249 // To Heading 4 = 4
250 that.to_heading(undefined, 4);
250 that.to_heading(undefined, 4);
251 that.control_key_active = false;
251 that.control_key_active = false;
252 return false;
252 return false;
253 } else if (event.which === 53 && that.control_key_active) {
253 } else if (event.which === 53 && that.control_key_active) {
254 // To Heading 5 = 5
254 // To Heading 5 = 5
255 that.to_heading(undefined, 5);
255 that.to_heading(undefined, 5);
256 that.control_key_active = false;
256 that.control_key_active = false;
257 return false;
257 return false;
258 } else if (event.which === 54 && that.control_key_active) {
258 } else if (event.which === 54 && that.control_key_active) {
259 // To Heading 6 = 6
259 // To Heading 6 = 6
260 that.to_heading(undefined, 6);
260 that.to_heading(undefined, 6);
261 that.control_key_active = false;
261 that.control_key_active = false;
262 return false;
262 return false;
263 } else if (event.which === 79 && that.control_key_active) {
263 } else if (event.which === 79 && that.control_key_active) {
264 // Toggle output = o
264 // Toggle output = o
265 if (event.shiftKey){
265 if (event.shiftKey){
266 that.toggle_output_scroll();
266 that.toggle_output_scroll();
267 } else {
267 } else {
268 that.toggle_output();
268 that.toggle_output();
269 }
269 }
270 that.control_key_active = false;
270 that.control_key_active = false;
271 return false;
271 return false;
272 } else if (event.which === 83 && that.control_key_active) {
272 } else if (event.which === 83 && that.control_key_active) {
273 // Save notebook = s
273 // Save notebook = s
274 that.save_checkpoint();
274 that.save_checkpoint();
275 that.control_key_active = false;
275 that.control_key_active = false;
276 return false;
276 return false;
277 } else if (event.which === 74 && that.control_key_active) {
277 } else if (event.which === 74 && that.control_key_active) {
278 // Move cell down = j
278 // Move cell down = j
279 that.move_cell_down();
279 that.move_cell_down();
280 that.control_key_active = false;
280 that.control_key_active = false;
281 return false;
281 return false;
282 } else if (event.which === 75 && that.control_key_active) {
282 } else if (event.which === 75 && that.control_key_active) {
283 // Move cell up = k
283 // Move cell up = k
284 that.move_cell_up();
284 that.move_cell_up();
285 that.control_key_active = false;
285 that.control_key_active = false;
286 return false;
286 return false;
287 } else if (event.which === 80 && that.control_key_active) {
287 } else if (event.which === 80 && that.control_key_active) {
288 // Select previous = p
288 // Select previous = p
289 that.select_prev();
289 that.select_prev();
290 that.control_key_active = false;
290 that.control_key_active = false;
291 return false;
291 return false;
292 } else if (event.which === 78 && that.control_key_active) {
292 } else if (event.which === 78 && that.control_key_active) {
293 // Select next = n
293 // Select next = n
294 that.select_next();
294 that.select_next();
295 that.control_key_active = false;
295 that.control_key_active = false;
296 return false;
296 return false;
297 } else if (event.which === 76 && that.control_key_active) {
297 } else if (event.which === 76 && that.control_key_active) {
298 // Toggle line numbers = l
298 // Toggle line numbers = l
299 that.cell_toggle_line_numbers();
299 that.cell_toggle_line_numbers();
300 that.control_key_active = false;
300 that.control_key_active = false;
301 return false;
301 return false;
302 } else if (event.which === 73 && that.control_key_active) {
302 } else if (event.which === 73 && that.control_key_active) {
303 // Interrupt kernel = i
303 // Interrupt kernel = i
304 that.kernel.interrupt();
304 that.kernel.interrupt();
305 that.control_key_active = false;
305 that.control_key_active = false;
306 return false;
306 return false;
307 } else if (event.which === 190 && that.control_key_active) {
307 } else if (event.which === 190 && that.control_key_active) {
308 // Restart kernel = . # matches qt console
308 // Restart kernel = . # matches qt console
309 that.restart_kernel();
309 that.restart_kernel();
310 that.control_key_active = false;
310 that.control_key_active = false;
311 return false;
311 return false;
312 } else if (event.which === 72 && that.control_key_active) {
312 } else if (event.which === 72 && that.control_key_active) {
313 // Show keyboard shortcuts = h
313 // Show keyboard shortcuts = h
314 IPython.quick_help.show_keyboard_shortcuts();
314 IPython.quick_help.show_keyboard_shortcuts();
315 that.control_key_active = false;
315 that.control_key_active = false;
316 return false;
316 return false;
317 } else if (event.which === 90 && that.control_key_active) {
317 } else if (event.which === 90 && that.control_key_active) {
318 // Undo last cell delete = z
318 // Undo last cell delete = z
319 that.undelete();
319 that.undelete();
320 that.control_key_active = false;
320 that.control_key_active = false;
321 return false;
321 return false;
322 } else if (event.which === 189 && that.control_key_active) {
322 } else if (event.which === 189 && that.control_key_active) {
323 // Split cell = -
323 // Split cell = -
324 that.split_cell();
324 that.split_cell();
325 that.control_key_active = false;
325 that.control_key_active = false;
326 return false;
326 return false;
327 } else if (that.control_key_active) {
327 } else if (that.control_key_active) {
328 that.control_key_active = false;
328 that.control_key_active = false;
329 return true;
329 return true;
330 }
330 }
331 return true;
331 return true;
332 });
332 });
333
333
334 var collapse_time = function(time){
334 var collapse_time = function(time){
335 var app_height = $('#ipython-main-app').height(); // content height
335 var app_height = $('#ipython-main-app').height(); // content height
336 var splitter_height = $('div#pager_splitter').outerHeight(true);
336 var splitter_height = $('div#pager_splitter').outerHeight(true);
337 var new_height = app_height - splitter_height;
337 var new_height = app_height - splitter_height;
338 that.element.animate({height : new_height + 'px'}, time);
338 that.element.animate({height : new_height + 'px'}, time);
339 }
339 }
340
340
341 this.element.bind('collapse_pager', function (event,extrap) {
341 this.element.bind('collapse_pager', function (event,extrap) {
342 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
342 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
343 collapse_time(time);
343 collapse_time(time);
344 });
344 });
345
345
346 var expand_time = function(time) {
346 var expand_time = function(time) {
347 var app_height = $('#ipython-main-app').height(); // content height
347 var app_height = $('#ipython-main-app').height(); // content height
348 var splitter_height = $('div#pager_splitter').outerHeight(true);
348 var splitter_height = $('div#pager_splitter').outerHeight(true);
349 var pager_height = $('div#pager').outerHeight(true);
349 var pager_height = $('div#pager').outerHeight(true);
350 var new_height = app_height - pager_height - splitter_height;
350 var new_height = app_height - pager_height - splitter_height;
351 that.element.animate({height : new_height + 'px'}, time);
351 that.element.animate({height : new_height + 'px'}, time);
352 }
352 }
353
353
354 this.element.bind('expand_pager', function (event, extrap) {
354 this.element.bind('expand_pager', function (event, extrap) {
355 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
355 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
356 expand_time(time);
356 expand_time(time);
357 });
357 });
358
358
359 // Firefox 22 broke $(window).on("beforeunload")
359 // Firefox 22 broke $(window).on("beforeunload")
360 // I'm not sure why or how.
360 // I'm not sure why or how.
361 window.onbeforeunload = function (e) {
361 window.onbeforeunload = function (e) {
362 // TODO: Make killing the kernel configurable.
362 // TODO: Make killing the kernel configurable.
363 var kill_kernel = false;
363 var kill_kernel = false;
364 if (kill_kernel) {
364 if (kill_kernel) {
365 that.kernel.kill();
365 that.kernel.kill();
366 }
366 }
367 // if we are autosaving, trigger an autosave on nav-away.
367 // if we are autosaving, trigger an autosave on nav-away.
368 // still warn, because if we don't the autosave may fail.
368 // still warn, because if we don't the autosave may fail.
369 if (that.dirty && ! that.read_only) {
369 if (that.dirty && ! that.read_only) {
370 if ( that.autosave_interval ) {
370 if ( that.autosave_interval ) {
371 that.save_notebook();
371 that.save_notebook();
372 return "Autosave in progress, latest changes may be lost.";
372 return "Autosave in progress, latest changes may be lost.";
373 } else {
373 } else {
374 return "Unsaved changes will be lost.";
374 return "Unsaved changes will be lost.";
375 }
375 }
376 };
376 };
377 // Null is the *only* return value that will make the browser not
377 // Null is the *only* return value that will make the browser not
378 // pop up the "don't leave" dialog.
378 // pop up the "don't leave" dialog.
379 return null;
379 return null;
380 };
380 };
381 };
381 };
382
382
383 /**
383 /**
384 * Set the dirty flag, and trigger the set_dirty.Notebook event
384 * Set the dirty flag, and trigger the set_dirty.Notebook event
385 *
385 *
386 * @method set_dirty
386 * @method set_dirty
387 */
387 */
388 Notebook.prototype.set_dirty = function (value) {
388 Notebook.prototype.set_dirty = function (value) {
389 if (value === undefined) {
389 if (value === undefined) {
390 value = true;
390 value = true;
391 }
391 }
392 if (this.dirty == value) {
392 if (this.dirty == value) {
393 return;
393 return;
394 }
394 }
395 $([IPython.events]).trigger('set_dirty.Notebook', {value: value});
395 $([IPython.events]).trigger('set_dirty.Notebook', {value: value});
396 };
396 };
397
397
398 /**
398 /**
399 * Scroll the top of the page to a given cell.
399 * Scroll the top of the page to a given cell.
400 *
400 *
401 * @method scroll_to_cell
401 * @method scroll_to_cell
402 * @param {Number} cell_number An index of the cell to view
402 * @param {Number} cell_number An index of the cell to view
403 * @param {Number} time Animation time in milliseconds
403 * @param {Number} time Animation time in milliseconds
404 * @return {Number} Pixel offset from the top of the container
404 * @return {Number} Pixel offset from the top of the container
405 */
405 */
406 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
406 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
407 var cells = this.get_cells();
407 var cells = this.get_cells();
408 var time = time || 0;
408 var time = time || 0;
409 cell_number = Math.min(cells.length-1,cell_number);
409 cell_number = Math.min(cells.length-1,cell_number);
410 cell_number = Math.max(0 ,cell_number);
410 cell_number = Math.max(0 ,cell_number);
411 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
411 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
412 this.element.animate({scrollTop:scroll_value}, time);
412 this.element.animate({scrollTop:scroll_value}, time);
413 return scroll_value;
413 return scroll_value;
414 };
414 };
415
415
416 /**
416 /**
417 * Scroll to the bottom of the page.
417 * Scroll to the bottom of the page.
418 *
418 *
419 * @method scroll_to_bottom
419 * @method scroll_to_bottom
420 */
420 */
421 Notebook.prototype.scroll_to_bottom = function () {
421 Notebook.prototype.scroll_to_bottom = function () {
422 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
422 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
423 };
423 };
424
424
425 /**
425 /**
426 * Scroll to the top of the page.
426 * Scroll to the top of the page.
427 *
427 *
428 * @method scroll_to_top
428 * @method scroll_to_top
429 */
429 */
430 Notebook.prototype.scroll_to_top = function () {
430 Notebook.prototype.scroll_to_top = function () {
431 this.element.animate({scrollTop:0}, 0);
431 this.element.animate({scrollTop:0}, 0);
432 };
432 };
433
433
434
434
435 // Cell indexing, retrieval, etc.
435 // Cell indexing, retrieval, etc.
436
436
437 /**
437 /**
438 * Get all cell elements in the notebook.
438 * Get all cell elements in the notebook.
439 *
439 *
440 * @method get_cell_elements
440 * @method get_cell_elements
441 * @return {jQuery} A selector of all cell elements
441 * @return {jQuery} A selector of all cell elements
442 */
442 */
443 Notebook.prototype.get_cell_elements = function () {
443 Notebook.prototype.get_cell_elements = function () {
444 return this.container.children("div.cell");
444 return this.container.children("div.cell");
445 };
445 };
446
446
447 /**
447 /**
448 * Get a particular cell element.
448 * Get a particular cell element.
449 *
449 *
450 * @method get_cell_element
450 * @method get_cell_element
451 * @param {Number} index An index of a cell to select
451 * @param {Number} index An index of a cell to select
452 * @return {jQuery} A selector of the given cell.
452 * @return {jQuery} A selector of the given cell.
453 */
453 */
454 Notebook.prototype.get_cell_element = function (index) {
454 Notebook.prototype.get_cell_element = function (index) {
455 var result = null;
455 var result = null;
456 var e = this.get_cell_elements().eq(index);
456 var e = this.get_cell_elements().eq(index);
457 if (e.length !== 0) {
457 if (e.length !== 0) {
458 result = e;
458 result = e;
459 }
459 }
460 return result;
460 return result;
461 };
461 };
462
462
463 /**
463 /**
464 * Count the cells in this notebook.
464 * Count the cells in this notebook.
465 *
465 *
466 * @method ncells
466 * @method ncells
467 * @return {Number} The number of cells in this notebook
467 * @return {Number} The number of cells in this notebook
468 */
468 */
469 Notebook.prototype.ncells = function () {
469 Notebook.prototype.ncells = function () {
470 return this.get_cell_elements().length;
470 return this.get_cell_elements().length;
471 };
471 };
472
472
473 /**
473 /**
474 * Get all Cell objects in this notebook.
474 * Get all Cell objects in this notebook.
475 *
475 *
476 * @method get_cells
476 * @method get_cells
477 * @return {Array} This notebook's Cell objects
477 * @return {Array} This notebook's Cell objects
478 */
478 */
479 // TODO: we are often calling cells as cells()[i], which we should optimize
479 // TODO: we are often calling cells as cells()[i], which we should optimize
480 // to cells(i) or a new method.
480 // to cells(i) or a new method.
481 Notebook.prototype.get_cells = function () {
481 Notebook.prototype.get_cells = function () {
482 return this.get_cell_elements().toArray().map(function (e) {
482 return this.get_cell_elements().toArray().map(function (e) {
483 return $(e).data("cell");
483 return $(e).data("cell");
484 });
484 });
485 };
485 };
486
486
487 /**
487 /**
488 * Get a Cell object from this notebook.
488 * Get a Cell object from this notebook.
489 *
489 *
490 * @method get_cell
490 * @method get_cell
491 * @param {Number} index An index of a cell to retrieve
491 * @param {Number} index An index of a cell to retrieve
492 * @return {Cell} A particular cell
492 * @return {Cell} A particular cell
493 */
493 */
494 Notebook.prototype.get_cell = function (index) {
494 Notebook.prototype.get_cell = function (index) {
495 var result = null;
495 var result = null;
496 var ce = this.get_cell_element(index);
496 var ce = this.get_cell_element(index);
497 if (ce !== null) {
497 if (ce !== null) {
498 result = ce.data('cell');
498 result = ce.data('cell');
499 }
499 }
500 return result;
500 return result;
501 }
501 }
502
502
503 /**
503 /**
504 * Get the cell below a given cell.
504 * Get the cell below a given cell.
505 *
505 *
506 * @method get_next_cell
506 * @method get_next_cell
507 * @param {Cell} cell The provided cell
507 * @param {Cell} cell The provided cell
508 * @return {Cell} The next cell
508 * @return {Cell} The next cell
509 */
509 */
510 Notebook.prototype.get_next_cell = function (cell) {
510 Notebook.prototype.get_next_cell = function (cell) {
511 var result = null;
511 var result = null;
512 var index = this.find_cell_index(cell);
512 var index = this.find_cell_index(cell);
513 if (this.is_valid_cell_index(index+1)) {
513 if (this.is_valid_cell_index(index+1)) {
514 result = this.get_cell(index+1);
514 result = this.get_cell(index+1);
515 }
515 }
516 return result;
516 return result;
517 }
517 }
518
518
519 /**
519 /**
520 * Get the cell above a given cell.
520 * Get the cell above a given cell.
521 *
521 *
522 * @method get_prev_cell
522 * @method get_prev_cell
523 * @param {Cell} cell The provided cell
523 * @param {Cell} cell The provided cell
524 * @return {Cell} The previous cell
524 * @return {Cell} The previous cell
525 */
525 */
526 Notebook.prototype.get_prev_cell = function (cell) {
526 Notebook.prototype.get_prev_cell = function (cell) {
527 // TODO: off-by-one
527 // TODO: off-by-one
528 // nb.get_prev_cell(nb.get_cell(1)) is null
528 // nb.get_prev_cell(nb.get_cell(1)) is null
529 var result = null;
529 var result = null;
530 var index = this.find_cell_index(cell);
530 var index = this.find_cell_index(cell);
531 if (index !== null && index > 1) {
531 if (index !== null && index > 1) {
532 result = this.get_cell(index-1);
532 result = this.get_cell(index-1);
533 }
533 }
534 return result;
534 return result;
535 }
535 }
536
536
537 /**
537 /**
538 * Get the numeric index of a given cell.
538 * Get the numeric index of a given cell.
539 *
539 *
540 * @method find_cell_index
540 * @method find_cell_index
541 * @param {Cell} cell The provided cell
541 * @param {Cell} cell The provided cell
542 * @return {Number} The cell's numeric index
542 * @return {Number} The cell's numeric index
543 */
543 */
544 Notebook.prototype.find_cell_index = function (cell) {
544 Notebook.prototype.find_cell_index = function (cell) {
545 var result = null;
545 var result = null;
546 this.get_cell_elements().filter(function (index) {
546 this.get_cell_elements().filter(function (index) {
547 if ($(this).data("cell") === cell) {
547 if ($(this).data("cell") === cell) {
548 result = index;
548 result = index;
549 };
549 };
550 });
550 });
551 return result;
551 return result;
552 };
552 };
553
553
554 /**
554 /**
555 * Get a given index , or the selected index if none is provided.
555 * Get a given index , or the selected index if none is provided.
556 *
556 *
557 * @method index_or_selected
557 * @method index_or_selected
558 * @param {Number} index A cell's index
558 * @param {Number} index A cell's index
559 * @return {Number} The given index, or selected index if none is provided.
559 * @return {Number} The given index, or selected index if none is provided.
560 */
560 */
561 Notebook.prototype.index_or_selected = function (index) {
561 Notebook.prototype.index_or_selected = function (index) {
562 var i;
562 var i;
563 if (index === undefined || index === null) {
563 if (index === undefined || index === null) {
564 i = this.get_selected_index();
564 i = this.get_selected_index();
565 if (i === null) {
565 if (i === null) {
566 i = 0;
566 i = 0;
567 }
567 }
568 } else {
568 } else {
569 i = index;
569 i = index;
570 }
570 }
571 return i;
571 return i;
572 };
572 };
573
573
574 /**
574 /**
575 * Get the currently selected cell.
575 * Get the currently selected cell.
576 * @method get_selected_cell
576 * @method get_selected_cell
577 * @return {Cell} The selected cell
577 * @return {Cell} The selected cell
578 */
578 */
579 Notebook.prototype.get_selected_cell = function () {
579 Notebook.prototype.get_selected_cell = function () {
580 var index = this.get_selected_index();
580 var index = this.get_selected_index();
581 return this.get_cell(index);
581 return this.get_cell(index);
582 };
582 };
583
583
584 /**
584 /**
585 * Check whether a cell index is valid.
585 * Check whether a cell index is valid.
586 *
586 *
587 * @method is_valid_cell_index
587 * @method is_valid_cell_index
588 * @param {Number} index A cell index
588 * @param {Number} index A cell index
589 * @return True if the index is valid, false otherwise
589 * @return True if the index is valid, false otherwise
590 */
590 */
591 Notebook.prototype.is_valid_cell_index = function (index) {
591 Notebook.prototype.is_valid_cell_index = function (index) {
592 if (index !== null && index >= 0 && index < this.ncells()) {
592 if (index !== null && index >= 0 && index < this.ncells()) {
593 return true;
593 return true;
594 } else {
594 } else {
595 return false;
595 return false;
596 };
596 };
597 }
597 }
598
598
599 /**
599 /**
600 * Get the index of the currently selected cell.
600 * Get the index of the currently selected cell.
601
601
602 * @method get_selected_index
602 * @method get_selected_index
603 * @return {Number} The selected cell's numeric index
603 * @return {Number} The selected cell's numeric index
604 */
604 */
605 Notebook.prototype.get_selected_index = function () {
605 Notebook.prototype.get_selected_index = function () {
606 var result = null;
606 var result = null;
607 this.get_cell_elements().filter(function (index) {
607 this.get_cell_elements().filter(function (index) {
608 if ($(this).data("cell").selected === true) {
608 if ($(this).data("cell").selected === true) {
609 result = index;
609 result = index;
610 };
610 };
611 });
611 });
612 return result;
612 return result;
613 };
613 };
614
614
615
615
616 // Cell selection.
616 // Cell selection.
617
617
618 /**
618 /**
619 * Programmatically select a cell.
619 * Programmatically select a cell.
620 *
620 *
621 * @method select
621 * @method select
622 * @param {Number} index A cell's index
622 * @param {Number} index A cell's index
623 * @return {Notebook} This notebook
623 * @return {Notebook} This notebook
624 */
624 */
625 Notebook.prototype.select = function (index) {
625 Notebook.prototype.select = function (index) {
626 if (this.is_valid_cell_index(index)) {
626 if (this.is_valid_cell_index(index)) {
627 var sindex = this.get_selected_index()
627 var sindex = this.get_selected_index()
628 if (sindex !== null && index !== sindex) {
628 if (sindex !== null && index !== sindex) {
629 this.get_cell(sindex).unselect();
629 this.get_cell(sindex).unselect();
630 };
630 };
631 var cell = this.get_cell(index);
631 var cell = this.get_cell(index);
632 cell.select();
632 cell.select();
633 if (cell.cell_type === 'heading') {
633 if (cell.cell_type === 'heading') {
634 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
634 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
635 {'cell_type':cell.cell_type,level:cell.level}
635 {'cell_type':cell.cell_type,level:cell.level}
636 );
636 );
637 } else {
637 } else {
638 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
638 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
639 {'cell_type':cell.cell_type}
639 {'cell_type':cell.cell_type}
640 );
640 );
641 };
641 };
642 };
642 };
643 return this;
643 return this;
644 };
644 };
645
645
646 /**
646 /**
647 * Programmatically select the next cell.
647 * Programmatically select the next cell.
648 *
648 *
649 * @method select_next
649 * @method select_next
650 * @return {Notebook} This notebook
650 * @return {Notebook} This notebook
651 */
651 */
652 Notebook.prototype.select_next = function () {
652 Notebook.prototype.select_next = function () {
653 var index = this.get_selected_index();
653 var index = this.get_selected_index();
654 this.select(index+1);
654 this.select(index+1);
655 return this;
655 return this;
656 };
656 };
657
657
658 /**
658 /**
659 * Programmatically select the previous cell.
659 * Programmatically select the previous cell.
660 *
660 *
661 * @method select_prev
661 * @method select_prev
662 * @return {Notebook} This notebook
662 * @return {Notebook} This notebook
663 */
663 */
664 Notebook.prototype.select_prev = function () {
664 Notebook.prototype.select_prev = function () {
665 var index = this.get_selected_index();
665 var index = this.get_selected_index();
666 this.select(index-1);
666 this.select(index-1);
667 return this;
667 return this;
668 };
668 };
669
669
670
670
671 // Cell movement
671 // Cell movement
672
672
673 /**
673 /**
674 * Move given (or selected) cell up and select it.
674 * Move given (or selected) cell up and select it.
675 *
675 *
676 * @method move_cell_up
676 * @method move_cell_up
677 * @param [index] {integer} cell index
677 * @param [index] {integer} cell index
678 * @return {Notebook} This notebook
678 * @return {Notebook} This notebook
679 **/
679 **/
680 Notebook.prototype.move_cell_up = function (index) {
680 Notebook.prototype.move_cell_up = function (index) {
681 var i = this.index_or_selected(index);
681 var i = this.index_or_selected(index);
682 if (this.is_valid_cell_index(i) && i > 0) {
682 if (this.is_valid_cell_index(i) && i > 0) {
683 var pivot = this.get_cell_element(i-1);
683 var pivot = this.get_cell_element(i-1);
684 var tomove = this.get_cell_element(i);
684 var tomove = this.get_cell_element(i);
685 if (pivot !== null && tomove !== null) {
685 if (pivot !== null && tomove !== null) {
686 tomove.detach();
686 tomove.detach();
687 pivot.before(tomove);
687 pivot.before(tomove);
688 this.select(i-1);
688 this.select(i-1);
689 };
689 };
690 this.set_dirty(true);
690 this.set_dirty(true);
691 };
691 };
692 return this;
692 return this;
693 };
693 };
694
694
695
695
696 /**
696 /**
697 * Move given (or selected) cell down and select it
697 * Move given (or selected) cell down and select it
698 *
698 *
699 * @method move_cell_down
699 * @method move_cell_down
700 * @param [index] {integer} cell index
700 * @param [index] {integer} cell index
701 * @return {Notebook} This notebook
701 * @return {Notebook} This notebook
702 **/
702 **/
703 Notebook.prototype.move_cell_down = function (index) {
703 Notebook.prototype.move_cell_down = function (index) {
704 var i = this.index_or_selected(index);
704 var i = this.index_or_selected(index);
705 if ( this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
705 if ( this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
706 var pivot = this.get_cell_element(i+1);
706 var pivot = this.get_cell_element(i+1);
707 var tomove = this.get_cell_element(i);
707 var tomove = this.get_cell_element(i);
708 if (pivot !== null && tomove !== null) {
708 if (pivot !== null && tomove !== null) {
709 tomove.detach();
709 tomove.detach();
710 pivot.after(tomove);
710 pivot.after(tomove);
711 this.select(i+1);
711 this.select(i+1);
712 };
712 };
713 };
713 };
714 this.set_dirty();
714 this.set_dirty();
715 return this;
715 return this;
716 };
716 };
717
717
718
718
719 // Insertion, deletion.
719 // Insertion, deletion.
720
720
721 /**
721 /**
722 * Delete a cell from the notebook.
722 * Delete a cell from the notebook.
723 *
723 *
724 * @method delete_cell
724 * @method delete_cell
725 * @param [index] A cell's numeric index
725 * @param [index] A cell's numeric index
726 * @return {Notebook} This notebook
726 * @return {Notebook} This notebook
727 */
727 */
728 Notebook.prototype.delete_cell = function (index) {
728 Notebook.prototype.delete_cell = function (index) {
729 var i = this.index_or_selected(index);
729 var i = this.index_or_selected(index);
730 var cell = this.get_selected_cell();
730 var cell = this.get_selected_cell();
731 this.undelete_backup = cell.toJSON();
731 this.undelete_backup = cell.toJSON();
732 $('#undelete_cell').removeClass('disabled');
732 $('#undelete_cell').removeClass('disabled');
733 if (this.is_valid_cell_index(i)) {
733 if (this.is_valid_cell_index(i)) {
734 var ce = this.get_cell_element(i);
734 var ce = this.get_cell_element(i);
735 ce.remove();
735 ce.remove();
736 if (i === (this.ncells())) {
736 if (i === (this.ncells())) {
737 this.select(i-1);
737 this.select(i-1);
738 this.undelete_index = i - 1;
738 this.undelete_index = i - 1;
739 this.undelete_below = true;
739 this.undelete_below = true;
740 } else {
740 } else {
741 this.select(i);
741 this.select(i);
742 this.undelete_index = i;
742 this.undelete_index = i;
743 this.undelete_below = false;
743 this.undelete_below = false;
744 };
744 };
745 $([IPython.events]).trigger('delete.Cell', {'cell': cell, 'index': i});
745 $([IPython.events]).trigger('delete.Cell', {'cell': cell, 'index': i});
746 this.set_dirty(true);
746 this.set_dirty(true);
747 };
747 };
748 return this;
748 return this;
749 };
749 };
750
750
751 /**
751 /**
752 * Insert a cell so that after insertion the cell is at given index.
752 * Insert a cell so that after insertion the cell is at given index.
753 *
753 *
754 * Similar to insert_above, but index parameter is mandatory
754 * Similar to insert_above, but index parameter is mandatory
755 *
755 *
756 * Index will be brought back into the accissible range [0,n]
756 * Index will be brought back into the accissible range [0,n]
757 *
757 *
758 * @method insert_cell_at_index
758 * @method insert_cell_at_index
759 * @param type {string} in ['code','markdown','heading']
759 * @param type {string} in ['code','markdown','heading']
760 * @param [index] {int} a valid index where to inser cell
760 * @param [index] {int} a valid index where to inser cell
761 *
761 *
762 * @return cell {cell|null} created cell or null
762 * @return cell {cell|null} created cell or null
763 **/
763 **/
764 Notebook.prototype.insert_cell_at_index = function(type, index){
764 Notebook.prototype.insert_cell_at_index = function(type, index){
765
765
766 var ncells = this.ncells();
766 var ncells = this.ncells();
767 var index = Math.min(index,ncells);
767 var index = Math.min(index,ncells);
768 index = Math.max(index,0);
768 index = Math.max(index,0);
769 var cell = null;
769 var cell = null;
770
770
771 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
771 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
772 if (type === 'code') {
772 if (type === 'code') {
773 cell = new IPython.CodeCell(this.kernel);
773 cell = new IPython.CodeCell(this.kernel);
774 cell.set_input_prompt();
774 cell.set_input_prompt();
775 } else if (type === 'markdown') {
775 } else if (type === 'markdown') {
776 cell = new IPython.MarkdownCell();
776 cell = new IPython.MarkdownCell();
777 } else if (type === 'raw') {
777 } else if (type === 'raw') {
778 cell = new IPython.RawCell();
778 cell = new IPython.RawCell();
779 } else if (type === 'heading') {
779 } else if (type === 'heading') {
780 cell = new IPython.HeadingCell();
780 cell = new IPython.HeadingCell();
781 }
781 }
782
782
783 if(this._insert_element_at_index(cell.element,index)){
783 if(this._insert_element_at_index(cell.element,index)){
784 cell.render();
784 cell.render();
785 this.select(this.find_cell_index(cell));
785 this.select(this.find_cell_index(cell));
786 $([IPython.events]).trigger('create.Cell', {'cell': cell, 'index': index});
786 $([IPython.events]).trigger('create.Cell', {'cell': cell, 'index': index});
787 this.set_dirty(true);
787 this.set_dirty(true);
788 }
788 }
789 }
789 }
790 return cell;
790 return cell;
791
791
792 };
792 };
793
793
794 /**
794 /**
795 * Insert an element at given cell index.
795 * Insert an element at given cell index.
796 *
796 *
797 * @method _insert_element_at_index
797 * @method _insert_element_at_index
798 * @param element {dom element} a cell element
798 * @param element {dom element} a cell element
799 * @param [index] {int} a valid index where to inser cell
799 * @param [index] {int} a valid index where to inser cell
800 * @private
800 * @private
801 *
801 *
802 * return true if everything whent fine.
802 * return true if everything whent fine.
803 **/
803 **/
804 Notebook.prototype._insert_element_at_index = function(element, index){
804 Notebook.prototype._insert_element_at_index = function(element, index){
805 if (element === undefined){
805 if (element === undefined){
806 return false;
806 return false;
807 }
807 }
808
808
809 var ncells = this.ncells();
809 var ncells = this.ncells();
810
810
811 if (ncells === 0) {
811 if (ncells === 0) {
812 // special case append if empty
812 // special case append if empty
813 this.element.find('div.end_space').before(element);
813 this.element.find('div.end_space').before(element);
814 } else if ( ncells === index ) {
814 } else if ( ncells === index ) {
815 // special case append it the end, but not empty
815 // special case append it the end, but not empty
816 this.get_cell_element(index-1).after(element);
816 this.get_cell_element(index-1).after(element);
817 } else if (this.is_valid_cell_index(index)) {
817 } else if (this.is_valid_cell_index(index)) {
818 // otherwise always somewhere to append to
818 // otherwise always somewhere to append to
819 this.get_cell_element(index).before(element);
819 this.get_cell_element(index).before(element);
820 } else {
820 } else {
821 return false;
821 return false;
822 }
822 }
823
823
824 if (this.undelete_index !== null && index <= this.undelete_index) {
824 if (this.undelete_index !== null && index <= this.undelete_index) {
825 this.undelete_index = this.undelete_index + 1;
825 this.undelete_index = this.undelete_index + 1;
826 this.set_dirty(true);
826 this.set_dirty(true);
827 }
827 }
828 return true;
828 return true;
829 };
829 };
830
830
831 /**
831 /**
832 * Insert a cell of given type above given index, or at top
832 * Insert a cell of given type above given index, or at top
833 * of notebook if index smaller than 0.
833 * of notebook if index smaller than 0.
834 *
834 *
835 * default index value is the one of currently selected cell
835 * default index value is the one of currently selected cell
836 *
836 *
837 * @method insert_cell_above
837 * @method insert_cell_above
838 * @param type {string} cell type
838 * @param type {string} cell type
839 * @param [index] {integer}
839 * @param [index] {integer}
840 *
840 *
841 * @return handle to created cell or null
841 * @return handle to created cell or null
842 **/
842 **/
843 Notebook.prototype.insert_cell_above = function (type, index) {
843 Notebook.prototype.insert_cell_above = function (type, index) {
844 index = this.index_or_selected(index);
844 index = this.index_or_selected(index);
845 return this.insert_cell_at_index(type, index);
845 return this.insert_cell_at_index(type, index);
846 };
846 };
847
847
848 /**
848 /**
849 * Insert a cell of given type below given index, or at bottom
849 * Insert a cell of given type below given index, or at bottom
850 * of notebook if index greater thatn number of cell
850 * of notebook if index greater thatn number of cell
851 *
851 *
852 * default index value is the one of currently selected cell
852 * default index value is the one of currently selected cell
853 *
853 *
854 * @method insert_cell_below
854 * @method insert_cell_below
855 * @param type {string} cell type
855 * @param type {string} cell type
856 * @param [index] {integer}
856 * @param [index] {integer}
857 *
857 *
858 * @return handle to created cell or null
858 * @return handle to created cell or null
859 *
859 *
860 **/
860 **/
861 Notebook.prototype.insert_cell_below = function (type, index) {
861 Notebook.prototype.insert_cell_below = function (type, index) {
862 index = this.index_or_selected(index);
862 index = this.index_or_selected(index);
863 return this.insert_cell_at_index(type, index+1);
863 return this.insert_cell_at_index(type, index+1);
864 };
864 };
865
865
866
866
867 /**
867 /**
868 * Insert cell at end of notebook
868 * Insert cell at end of notebook
869 *
869 *
870 * @method insert_cell_at_bottom
870 * @method insert_cell_at_bottom
871 * @param {String} type cell type
871 * @param {String} type cell type
872 *
872 *
873 * @return the added cell; or null
873 * @return the added cell; or null
874 **/
874 **/
875 Notebook.prototype.insert_cell_at_bottom = function (type){
875 Notebook.prototype.insert_cell_at_bottom = function (type){
876 var len = this.ncells();
876 var len = this.ncells();
877 return this.insert_cell_below(type,len-1);
877 return this.insert_cell_below(type,len-1);
878 };
878 };
879
879
880 /**
880 /**
881 * Turn a cell into a code cell.
881 * Turn a cell into a code cell.
882 *
882 *
883 * @method to_code
883 * @method to_code
884 * @param {Number} [index] A cell's index
884 * @param {Number} [index] A cell's index
885 */
885 */
886 Notebook.prototype.to_code = function (index) {
886 Notebook.prototype.to_code = function (index) {
887 var i = this.index_or_selected(index);
887 var i = this.index_or_selected(index);
888 if (this.is_valid_cell_index(i)) {
888 if (this.is_valid_cell_index(i)) {
889 var source_element = this.get_cell_element(i);
889 var source_element = this.get_cell_element(i);
890 var source_cell = source_element.data("cell");
890 var source_cell = source_element.data("cell");
891 if (!(source_cell instanceof IPython.CodeCell)) {
891 if (!(source_cell instanceof IPython.CodeCell)) {
892 var target_cell = this.insert_cell_below('code',i);
892 var target_cell = this.insert_cell_below('code',i);
893 var text = source_cell.get_text();
893 var text = source_cell.get_text();
894 if (text === source_cell.placeholder) {
894 if (text === source_cell.placeholder) {
895 text = '';
895 text = '';
896 }
896 }
897 target_cell.set_text(text);
897 target_cell.set_text(text);
898 // make this value the starting point, so that we can only undo
898 // make this value the starting point, so that we can only undo
899 // to this state, instead of a blank cell
899 // to this state, instead of a blank cell
900 target_cell.code_mirror.clearHistory();
900 target_cell.code_mirror.clearHistory();
901 source_element.remove();
901 source_element.remove();
902 this.set_dirty(true);
902 this.set_dirty(true);
903 };
903 };
904 };
904 };
905 };
905 };
906
906
907 /**
907 /**
908 * Turn a cell into a Markdown cell.
908 * Turn a cell into a Markdown cell.
909 *
909 *
910 * @method to_markdown
910 * @method to_markdown
911 * @param {Number} [index] A cell's index
911 * @param {Number} [index] A cell's index
912 */
912 */
913 Notebook.prototype.to_markdown = function (index) {
913 Notebook.prototype.to_markdown = function (index) {
914 var i = this.index_or_selected(index);
914 var i = this.index_or_selected(index);
915 if (this.is_valid_cell_index(i)) {
915 if (this.is_valid_cell_index(i)) {
916 var source_element = this.get_cell_element(i);
916 var source_element = this.get_cell_element(i);
917 var source_cell = source_element.data("cell");
917 var source_cell = source_element.data("cell");
918 if (!(source_cell instanceof IPython.MarkdownCell)) {
918 if (!(source_cell instanceof IPython.MarkdownCell)) {
919 var target_cell = this.insert_cell_below('markdown',i);
919 var target_cell = this.insert_cell_below('markdown',i);
920 var text = source_cell.get_text();
920 var text = source_cell.get_text();
921 if (text === source_cell.placeholder) {
921 if (text === source_cell.placeholder) {
922 text = '';
922 text = '';
923 };
923 };
924 // The edit must come before the set_text.
924 // The edit must come before the set_text.
925 target_cell.edit();
925 target_cell.edit();
926 target_cell.set_text(text);
926 target_cell.set_text(text);
927 // make this value the starting point, so that we can only undo
927 // make this value the starting point, so that we can only undo
928 // to this state, instead of a blank cell
928 // to this state, instead of a blank cell
929 target_cell.code_mirror.clearHistory();
929 target_cell.code_mirror.clearHistory();
930 source_element.remove();
930 source_element.remove();
931 this.set_dirty(true);
931 this.set_dirty(true);
932 };
932 };
933 };
933 };
934 };
934 };
935
935
936 /**
936 /**
937 * Turn a cell into a raw text cell.
937 * Turn a cell into a raw text cell.
938 *
938 *
939 * @method to_raw
939 * @method to_raw
940 * @param {Number} [index] A cell's index
940 * @param {Number} [index] A cell's index
941 */
941 */
942 Notebook.prototype.to_raw = function (index) {
942 Notebook.prototype.to_raw = function (index) {
943 var i = this.index_or_selected(index);
943 var i = this.index_or_selected(index);
944 if (this.is_valid_cell_index(i)) {
944 if (this.is_valid_cell_index(i)) {
945 var source_element = this.get_cell_element(i);
945 var source_element = this.get_cell_element(i);
946 var source_cell = source_element.data("cell");
946 var source_cell = source_element.data("cell");
947 var target_cell = null;
947 var target_cell = null;
948 if (!(source_cell instanceof IPython.RawCell)) {
948 if (!(source_cell instanceof IPython.RawCell)) {
949 target_cell = this.insert_cell_below('raw',i);
949 target_cell = this.insert_cell_below('raw',i);
950 var text = source_cell.get_text();
950 var text = source_cell.get_text();
951 if (text === source_cell.placeholder) {
951 if (text === source_cell.placeholder) {
952 text = '';
952 text = '';
953 };
953 };
954 // The edit must come before the set_text.
954 // The edit must come before the set_text.
955 target_cell.edit();
955 target_cell.edit();
956 target_cell.set_text(text);
956 target_cell.set_text(text);
957 // make this value the starting point, so that we can only undo
957 // make this value the starting point, so that we can only undo
958 // to this state, instead of a blank cell
958 // to this state, instead of a blank cell
959 target_cell.code_mirror.clearHistory();
959 target_cell.code_mirror.clearHistory();
960 source_element.remove();
960 source_element.remove();
961 this.set_dirty(true);
961 this.set_dirty(true);
962 };
962 };
963 };
963 };
964 };
964 };
965
965
966 /**
966 /**
967 * Turn a cell into a heading cell.
967 * Turn a cell into a heading cell.
968 *
968 *
969 * @method to_heading
969 * @method to_heading
970 * @param {Number} [index] A cell's index
970 * @param {Number} [index] A cell's index
971 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
971 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
972 */
972 */
973 Notebook.prototype.to_heading = function (index, level) {
973 Notebook.prototype.to_heading = function (index, level) {
974 level = level || 1;
974 level = level || 1;
975 var i = this.index_or_selected(index);
975 var i = this.index_or_selected(index);
976 if (this.is_valid_cell_index(i)) {
976 if (this.is_valid_cell_index(i)) {
977 var source_element = this.get_cell_element(i);
977 var source_element = this.get_cell_element(i);
978 var source_cell = source_element.data("cell");
978 var source_cell = source_element.data("cell");
979 var target_cell = null;
979 var target_cell = null;
980 if (source_cell instanceof IPython.HeadingCell) {
980 if (source_cell instanceof IPython.HeadingCell) {
981 source_cell.set_level(level);
981 source_cell.set_level(level);
982 } else {
982 } else {
983 target_cell = this.insert_cell_below('heading',i);
983 target_cell = this.insert_cell_below('heading',i);
984 var text = source_cell.get_text();
984 var text = source_cell.get_text();
985 if (text === source_cell.placeholder) {
985 if (text === source_cell.placeholder) {
986 text = '';
986 text = '';
987 };
987 };
988 // The edit must come before the set_text.
988 // The edit must come before the set_text.
989 target_cell.set_level(level);
989 target_cell.set_level(level);
990 target_cell.edit();
990 target_cell.edit();
991 target_cell.set_text(text);
991 target_cell.set_text(text);
992 // make this value the starting point, so that we can only undo
992 // make this value the starting point, so that we can only undo
993 // to this state, instead of a blank cell
993 // to this state, instead of a blank cell
994 target_cell.code_mirror.clearHistory();
994 target_cell.code_mirror.clearHistory();
995 source_element.remove();
995 source_element.remove();
996 this.set_dirty(true);
996 this.set_dirty(true);
997 };
997 };
998 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
998 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
999 {'cell_type':'heading',level:level}
999 {'cell_type':'heading',level:level}
1000 );
1000 );
1001 };
1001 };
1002 };
1002 };
1003
1003
1004
1004
1005 // Cut/Copy/Paste
1005 // Cut/Copy/Paste
1006
1006
1007 /**
1007 /**
1008 * Enable UI elements for pasting cells.
1008 * Enable UI elements for pasting cells.
1009 *
1009 *
1010 * @method enable_paste
1010 * @method enable_paste
1011 */
1011 */
1012 Notebook.prototype.enable_paste = function () {
1012 Notebook.prototype.enable_paste = function () {
1013 var that = this;
1013 var that = this;
1014 if (!this.paste_enabled) {
1014 if (!this.paste_enabled) {
1015 $('#paste_cell_replace').removeClass('disabled')
1015 $('#paste_cell_replace').removeClass('disabled')
1016 .on('click', function () {that.paste_cell_replace();});
1016 .on('click', function () {that.paste_cell_replace();});
1017 $('#paste_cell_above').removeClass('disabled')
1017 $('#paste_cell_above').removeClass('disabled')
1018 .on('click', function () {that.paste_cell_above();});
1018 .on('click', function () {that.paste_cell_above();});
1019 $('#paste_cell_below').removeClass('disabled')
1019 $('#paste_cell_below').removeClass('disabled')
1020 .on('click', function () {that.paste_cell_below();});
1020 .on('click', function () {that.paste_cell_below();});
1021 this.paste_enabled = true;
1021 this.paste_enabled = true;
1022 };
1022 };
1023 };
1023 };
1024
1024
1025 /**
1025 /**
1026 * Disable UI elements for pasting cells.
1026 * Disable UI elements for pasting cells.
1027 *
1027 *
1028 * @method disable_paste
1028 * @method disable_paste
1029 */
1029 */
1030 Notebook.prototype.disable_paste = function () {
1030 Notebook.prototype.disable_paste = function () {
1031 if (this.paste_enabled) {
1031 if (this.paste_enabled) {
1032 $('#paste_cell_replace').addClass('disabled').off('click');
1032 $('#paste_cell_replace').addClass('disabled').off('click');
1033 $('#paste_cell_above').addClass('disabled').off('click');
1033 $('#paste_cell_above').addClass('disabled').off('click');
1034 $('#paste_cell_below').addClass('disabled').off('click');
1034 $('#paste_cell_below').addClass('disabled').off('click');
1035 this.paste_enabled = false;
1035 this.paste_enabled = false;
1036 };
1036 };
1037 };
1037 };
1038
1038
1039 /**
1039 /**
1040 * Cut a cell.
1040 * Cut a cell.
1041 *
1041 *
1042 * @method cut_cell
1042 * @method cut_cell
1043 */
1043 */
1044 Notebook.prototype.cut_cell = function () {
1044 Notebook.prototype.cut_cell = function () {
1045 this.copy_cell();
1045 this.copy_cell();
1046 this.delete_cell();
1046 this.delete_cell();
1047 }
1047 }
1048
1048
1049 /**
1049 /**
1050 * Copy a cell.
1050 * Copy a cell.
1051 *
1051 *
1052 * @method copy_cell
1052 * @method copy_cell
1053 */
1053 */
1054 Notebook.prototype.copy_cell = function () {
1054 Notebook.prototype.copy_cell = function () {
1055 var cell = this.get_selected_cell();
1055 var cell = this.get_selected_cell();
1056 this.clipboard = cell.toJSON();
1056 this.clipboard = cell.toJSON();
1057 this.enable_paste();
1057 this.enable_paste();
1058 };
1058 };
1059
1059
1060 /**
1060 /**
1061 * Replace the selected cell with a cell in the clipboard.
1061 * Replace the selected cell with a cell in the clipboard.
1062 *
1062 *
1063 * @method paste_cell_replace
1063 * @method paste_cell_replace
1064 */
1064 */
1065 Notebook.prototype.paste_cell_replace = function () {
1065 Notebook.prototype.paste_cell_replace = function () {
1066 if (this.clipboard !== null && this.paste_enabled) {
1066 if (this.clipboard !== null && this.paste_enabled) {
1067 var cell_data = this.clipboard;
1067 var cell_data = this.clipboard;
1068 var new_cell = this.insert_cell_above(cell_data.cell_type);
1068 var new_cell = this.insert_cell_above(cell_data.cell_type);
1069 new_cell.fromJSON(cell_data);
1069 new_cell.fromJSON(cell_data);
1070 var old_cell = this.get_next_cell(new_cell);
1070 var old_cell = this.get_next_cell(new_cell);
1071 this.delete_cell(this.find_cell_index(old_cell));
1071 this.delete_cell(this.find_cell_index(old_cell));
1072 this.select(this.find_cell_index(new_cell));
1072 this.select(this.find_cell_index(new_cell));
1073 };
1073 };
1074 };
1074 };
1075
1075
1076 /**
1076 /**
1077 * Paste a cell from the clipboard above the selected cell.
1077 * Paste a cell from the clipboard above the selected cell.
1078 *
1078 *
1079 * @method paste_cell_above
1079 * @method paste_cell_above
1080 */
1080 */
1081 Notebook.prototype.paste_cell_above = function () {
1081 Notebook.prototype.paste_cell_above = function () {
1082 if (this.clipboard !== null && this.paste_enabled) {
1082 if (this.clipboard !== null && this.paste_enabled) {
1083 var cell_data = this.clipboard;
1083 var cell_data = this.clipboard;
1084 var new_cell = this.insert_cell_above(cell_data.cell_type);
1084 var new_cell = this.insert_cell_above(cell_data.cell_type);
1085 new_cell.fromJSON(cell_data);
1085 new_cell.fromJSON(cell_data);
1086 };
1086 };
1087 };
1087 };
1088
1088
1089 /**
1089 /**
1090 * Paste a cell from the clipboard below the selected cell.
1090 * Paste a cell from the clipboard below the selected cell.
1091 *
1091 *
1092 * @method paste_cell_below
1092 * @method paste_cell_below
1093 */
1093 */
1094 Notebook.prototype.paste_cell_below = function () {
1094 Notebook.prototype.paste_cell_below = function () {
1095 if (this.clipboard !== null && this.paste_enabled) {
1095 if (this.clipboard !== null && this.paste_enabled) {
1096 var cell_data = this.clipboard;
1096 var cell_data = this.clipboard;
1097 var new_cell = this.insert_cell_below(cell_data.cell_type);
1097 var new_cell = this.insert_cell_below(cell_data.cell_type);
1098 new_cell.fromJSON(cell_data);
1098 new_cell.fromJSON(cell_data);
1099 };
1099 };
1100 };
1100 };
1101
1101
1102 // Cell undelete
1102 // Cell undelete
1103
1103
1104 /**
1104 /**
1105 * Restore the most recently deleted cell.
1105 * Restore the most recently deleted cell.
1106 *
1106 *
1107 * @method undelete
1107 * @method undelete
1108 */
1108 */
1109 Notebook.prototype.undelete = function() {
1109 Notebook.prototype.undelete = function() {
1110 if (this.undelete_backup !== null && this.undelete_index !== null) {
1110 if (this.undelete_backup !== null && this.undelete_index !== null) {
1111 var current_index = this.get_selected_index();
1111 var current_index = this.get_selected_index();
1112 if (this.undelete_index < current_index) {
1112 if (this.undelete_index < current_index) {
1113 current_index = current_index + 1;
1113 current_index = current_index + 1;
1114 }
1114 }
1115 if (this.undelete_index >= this.ncells()) {
1115 if (this.undelete_index >= this.ncells()) {
1116 this.select(this.ncells() - 1);
1116 this.select(this.ncells() - 1);
1117 }
1117 }
1118 else {
1118 else {
1119 this.select(this.undelete_index);
1119 this.select(this.undelete_index);
1120 }
1120 }
1121 var cell_data = this.undelete_backup;
1121 var cell_data = this.undelete_backup;
1122 var new_cell = null;
1122 var new_cell = null;
1123 if (this.undelete_below) {
1123 if (this.undelete_below) {
1124 new_cell = this.insert_cell_below(cell_data.cell_type);
1124 new_cell = this.insert_cell_below(cell_data.cell_type);
1125 } else {
1125 } else {
1126 new_cell = this.insert_cell_above(cell_data.cell_type);
1126 new_cell = this.insert_cell_above(cell_data.cell_type);
1127 }
1127 }
1128 new_cell.fromJSON(cell_data);
1128 new_cell.fromJSON(cell_data);
1129 this.select(current_index);
1129 this.select(current_index);
1130 this.undelete_backup = null;
1130 this.undelete_backup = null;
1131 this.undelete_index = null;
1131 this.undelete_index = null;
1132 }
1132 }
1133 $('#undelete_cell').addClass('disabled');
1133 $('#undelete_cell').addClass('disabled');
1134 }
1134 }
1135
1135
1136 // Split/merge
1136 // Split/merge
1137
1137
1138 /**
1138 /**
1139 * Split the selected cell into two, at the cursor.
1139 * Split the selected cell into two, at the cursor.
1140 *
1140 *
1141 * @method split_cell
1141 * @method split_cell
1142 */
1142 */
1143 Notebook.prototype.split_cell = function () {
1143 Notebook.prototype.split_cell = function () {
1144 // Todo: implement spliting for other cell types.
1144 // Todo: implement spliting for other cell types.
1145 var cell = this.get_selected_cell();
1145 var cell = this.get_selected_cell();
1146 if (cell.is_splittable()) {
1146 if (cell.is_splittable()) {
1147 var texta = cell.get_pre_cursor();
1147 var texta = cell.get_pre_cursor();
1148 var textb = cell.get_post_cursor();
1148 var textb = cell.get_post_cursor();
1149 if (cell instanceof IPython.CodeCell) {
1149 if (cell instanceof IPython.CodeCell) {
1150 cell.set_text(texta);
1150 cell.set_text(texta);
1151 var new_cell = this.insert_cell_below('code');
1151 var new_cell = this.insert_cell_below('code');
1152 new_cell.set_text(textb);
1152 new_cell.set_text(textb);
1153 } else if (cell instanceof IPython.MarkdownCell) {
1153 } else if (cell instanceof IPython.MarkdownCell) {
1154 cell.set_text(texta);
1154 cell.set_text(texta);
1155 cell.render();
1155 cell.render();
1156 var new_cell = this.insert_cell_below('markdown');
1156 var new_cell = this.insert_cell_below('markdown');
1157 new_cell.edit(); // editor must be visible to call set_text
1157 new_cell.edit(); // editor must be visible to call set_text
1158 new_cell.set_text(textb);
1158 new_cell.set_text(textb);
1159 new_cell.render();
1159 new_cell.render();
1160 }
1160 }
1161 };
1161 };
1162 };
1162 };
1163
1163
1164 /**
1164 /**
1165 * Combine the selected cell into the cell above it.
1165 * Combine the selected cell into the cell above it.
1166 *
1166 *
1167 * @method merge_cell_above
1167 * @method merge_cell_above
1168 */
1168 */
1169 Notebook.prototype.merge_cell_above = function () {
1169 Notebook.prototype.merge_cell_above = function () {
1170 var index = this.get_selected_index();
1170 var index = this.get_selected_index();
1171 var cell = this.get_cell(index);
1171 var cell = this.get_cell(index);
1172 if (index > 0) {
1172 if (index > 0) {
1173 var upper_cell = this.get_cell(index-1);
1173 var upper_cell = this.get_cell(index-1);
1174 var upper_text = upper_cell.get_text();
1174 var upper_text = upper_cell.get_text();
1175 var text = cell.get_text();
1175 var text = cell.get_text();
1176 if (cell instanceof IPython.CodeCell) {
1176 if (cell instanceof IPython.CodeCell) {
1177 cell.set_text(upper_text+'\n'+text);
1177 cell.set_text(upper_text+'\n'+text);
1178 } else if (cell instanceof IPython.MarkdownCell) {
1178 } else if (cell instanceof IPython.MarkdownCell) {
1179 cell.edit();
1179 cell.edit();
1180 cell.set_text(upper_text+'\n'+text);
1180 cell.set_text(upper_text+'\n'+text);
1181 cell.render();
1181 cell.render();
1182 };
1182 };
1183 this.delete_cell(index-1);
1183 this.delete_cell(index-1);
1184 this.select(this.find_cell_index(cell));
1184 this.select(this.find_cell_index(cell));
1185 };
1185 };
1186 };
1186 };
1187
1187
1188 /**
1188 /**
1189 * Combine the selected cell into the cell below it.
1189 * Combine the selected cell into the cell below it.
1190 *
1190 *
1191 * @method merge_cell_below
1191 * @method merge_cell_below
1192 */
1192 */
1193 Notebook.prototype.merge_cell_below = function () {
1193 Notebook.prototype.merge_cell_below = function () {
1194 var index = this.get_selected_index();
1194 var index = this.get_selected_index();
1195 var cell = this.get_cell(index);
1195 var cell = this.get_cell(index);
1196 if (index < this.ncells()-1) {
1196 if (index < this.ncells()-1) {
1197 var lower_cell = this.get_cell(index+1);
1197 var lower_cell = this.get_cell(index+1);
1198 var lower_text = lower_cell.get_text();
1198 var lower_text = lower_cell.get_text();
1199 var text = cell.get_text();
1199 var text = cell.get_text();
1200 if (cell instanceof IPython.CodeCell) {
1200 if (cell instanceof IPython.CodeCell) {
1201 cell.set_text(text+'\n'+lower_text);
1201 cell.set_text(text+'\n'+lower_text);
1202 } else if (cell instanceof IPython.MarkdownCell) {
1202 } else if (cell instanceof IPython.MarkdownCell) {
1203 cell.edit();
1203 cell.edit();
1204 cell.set_text(text+'\n'+lower_text);
1204 cell.set_text(text+'\n'+lower_text);
1205 cell.render();
1205 cell.render();
1206 };
1206 };
1207 this.delete_cell(index+1);
1207 this.delete_cell(index+1);
1208 this.select(this.find_cell_index(cell));
1208 this.select(this.find_cell_index(cell));
1209 };
1209 };
1210 };
1210 };
1211
1211
1212
1212
1213 // Cell collapsing and output clearing
1213 // Cell collapsing and output clearing
1214
1214
1215 /**
1215 /**
1216 * Hide a cell's output.
1216 * Hide a cell's output.
1217 *
1217 *
1218 * @method collapse
1218 * @method collapse
1219 * @param {Number} index A cell's numeric index
1219 * @param {Number} index A cell's numeric index
1220 */
1220 */
1221 Notebook.prototype.collapse = function (index) {
1221 Notebook.prototype.collapse = function (index) {
1222 var i = this.index_or_selected(index);
1222 var i = this.index_or_selected(index);
1223 this.get_cell(i).collapse();
1223 this.get_cell(i).collapse();
1224 this.set_dirty(true);
1224 this.set_dirty(true);
1225 };
1225 };
1226
1226
1227 /**
1227 /**
1228 * Show a cell's output.
1228 * Show a cell's output.
1229 *
1229 *
1230 * @method expand
1230 * @method expand
1231 * @param {Number} index A cell's numeric index
1231 * @param {Number} index A cell's numeric index
1232 */
1232 */
1233 Notebook.prototype.expand = function (index) {
1233 Notebook.prototype.expand = function (index) {
1234 var i = this.index_or_selected(index);
1234 var i = this.index_or_selected(index);
1235 this.get_cell(i).expand();
1235 this.get_cell(i).expand();
1236 this.set_dirty(true);
1236 this.set_dirty(true);
1237 };
1237 };
1238
1238
1239 /** Toggle whether a cell's output is collapsed or expanded.
1239 /** Toggle whether a cell's output is collapsed or expanded.
1240 *
1240 *
1241 * @method toggle_output
1241 * @method toggle_output
1242 * @param {Number} index A cell's numeric index
1242 * @param {Number} index A cell's numeric index
1243 */
1243 */
1244 Notebook.prototype.toggle_output = function (index) {
1244 Notebook.prototype.toggle_output = function (index) {
1245 var i = this.index_or_selected(index);
1245 var i = this.index_or_selected(index);
1246 this.get_cell(i).toggle_output();
1246 this.get_cell(i).toggle_output();
1247 this.set_dirty(true);
1247 this.set_dirty(true);
1248 };
1248 };
1249
1249
1250 /**
1250 /**
1251 * Toggle a scrollbar for long cell outputs.
1251 * Toggle a scrollbar for long cell outputs.
1252 *
1252 *
1253 * @method toggle_output_scroll
1253 * @method toggle_output_scroll
1254 * @param {Number} index A cell's numeric index
1254 * @param {Number} index A cell's numeric index
1255 */
1255 */
1256 Notebook.prototype.toggle_output_scroll = function (index) {
1256 Notebook.prototype.toggle_output_scroll = function (index) {
1257 var i = this.index_or_selected(index);
1257 var i = this.index_or_selected(index);
1258 this.get_cell(i).toggle_output_scroll();
1258 this.get_cell(i).toggle_output_scroll();
1259 };
1259 };
1260
1260
1261 /**
1261 /**
1262 * Hide each code cell's output area.
1262 * Hide each code cell's output area.
1263 *
1263 *
1264 * @method collapse_all_output
1264 * @method collapse_all_output
1265 */
1265 */
1266 Notebook.prototype.collapse_all_output = function () {
1266 Notebook.prototype.collapse_all_output = function () {
1267 var ncells = this.ncells();
1267 var ncells = this.ncells();
1268 var cells = this.get_cells();
1268 var cells = this.get_cells();
1269 for (var i=0; i<ncells; i++) {
1269 for (var i=0; i<ncells; i++) {
1270 if (cells[i] instanceof IPython.CodeCell) {
1270 if (cells[i] instanceof IPython.CodeCell) {
1271 cells[i].output_area.collapse();
1271 cells[i].output_area.collapse();
1272 }
1272 }
1273 };
1273 };
1274 // this should not be set if the `collapse` key is removed from nbformat
1274 // this should not be set if the `collapse` key is removed from nbformat
1275 this.set_dirty(true);
1275 this.set_dirty(true);
1276 };
1276 };
1277
1277
1278 /**
1278 /**
1279 * Expand each code cell's output area, and add a scrollbar for long output.
1279 * Expand each code cell's output area, and add a scrollbar for long output.
1280 *
1280 *
1281 * @method scroll_all_output
1281 * @method scroll_all_output
1282 */
1282 */
1283 Notebook.prototype.scroll_all_output = function () {
1283 Notebook.prototype.scroll_all_output = function () {
1284 var ncells = this.ncells();
1284 var ncells = this.ncells();
1285 var cells = this.get_cells();
1285 var cells = this.get_cells();
1286 for (var i=0; i<ncells; i++) {
1286 for (var i=0; i<ncells; i++) {
1287 if (cells[i] instanceof IPython.CodeCell) {
1287 if (cells[i] instanceof IPython.CodeCell) {
1288 cells[i].output_area.expand();
1288 cells[i].output_area.expand();
1289 cells[i].output_area.scroll_if_long();
1289 cells[i].output_area.scroll_if_long();
1290 }
1290 }
1291 };
1291 };
1292 // this should not be set if the `collapse` key is removed from nbformat
1292 // this should not be set if the `collapse` key is removed from nbformat
1293 this.set_dirty(true);
1293 this.set_dirty(true);
1294 };
1294 };
1295
1295
1296 /**
1296 /**
1297 * Expand each code cell's output area, and remove scrollbars.
1297 * Expand each code cell's output area, and remove scrollbars.
1298 *
1298 *
1299 * @method expand_all_output
1299 * @method expand_all_output
1300 */
1300 */
1301 Notebook.prototype.expand_all_output = function () {
1301 Notebook.prototype.expand_all_output = function () {
1302 var ncells = this.ncells();
1302 var ncells = this.ncells();
1303 var cells = this.get_cells();
1303 var cells = this.get_cells();
1304 for (var i=0; i<ncells; i++) {
1304 for (var i=0; i<ncells; i++) {
1305 if (cells[i] instanceof IPython.CodeCell) {
1305 if (cells[i] instanceof IPython.CodeCell) {
1306 cells[i].output_area.expand();
1306 cells[i].output_area.expand();
1307 cells[i].output_area.unscroll_area();
1307 cells[i].output_area.unscroll_area();
1308 }
1308 }
1309 };
1309 };
1310 // this should not be set if the `collapse` key is removed from nbformat
1310 // this should not be set if the `collapse` key is removed from nbformat
1311 this.set_dirty(true);
1311 this.set_dirty(true);
1312 };
1312 };
1313
1313
1314 /**
1314 /**
1315 * Clear each code cell's output area.
1315 * Clear each code cell's output area.
1316 *
1316 *
1317 * @method clear_all_output
1317 * @method clear_all_output
1318 */
1318 */
1319 Notebook.prototype.clear_all_output = function () {
1319 Notebook.prototype.clear_all_output = function () {
1320 var ncells = this.ncells();
1320 var ncells = this.ncells();
1321 var cells = this.get_cells();
1321 var cells = this.get_cells();
1322 for (var i=0; i<ncells; i++) {
1322 for (var i=0; i<ncells; i++) {
1323 if (cells[i] instanceof IPython.CodeCell) {
1323 if (cells[i] instanceof IPython.CodeCell) {
1324 cells[i].clear_output(true,true,true);
1324 cells[i].clear_output(true,true,true);
1325 // Make all In[] prompts blank, as well
1325 // Make all In[] prompts blank, as well
1326 // TODO: make this configurable (via checkbox?)
1326 // TODO: make this configurable (via checkbox?)
1327 cells[i].set_input_prompt();
1327 cells[i].set_input_prompt();
1328 }
1328 }
1329 };
1329 };
1330 this.set_dirty(true);
1330 this.set_dirty(true);
1331 };
1331 };
1332
1332
1333
1333
1334 // Other cell functions: line numbers, ...
1334 // Other cell functions: line numbers, ...
1335
1335
1336 /**
1336 /**
1337 * Toggle line numbers in the selected cell's input area.
1337 * Toggle line numbers in the selected cell's input area.
1338 *
1338 *
1339 * @method cell_toggle_line_numbers
1339 * @method cell_toggle_line_numbers
1340 */
1340 */
1341 Notebook.prototype.cell_toggle_line_numbers = function() {
1341 Notebook.prototype.cell_toggle_line_numbers = function() {
1342 this.get_selected_cell().toggle_line_numbers();
1342 this.get_selected_cell().toggle_line_numbers();
1343 };
1343 };
1344
1344
1345 // Kernel related things
1345 // Kernel related things
1346
1346
1347 /**
1347 /**
1348 * Start a new kernel and set it on each code cell.
1348 * Start a new kernel and set it on each code cell.
1349 *
1349 *
1350 * @method start_kernel
1350 * @method start_kernel
1351 */
1351 */
1352 Notebook.prototype.start_kernel = function () {
1352 Notebook.prototype.start_kernel = function () {
1353 var base_url = $('body').data('baseKernelUrl') + "kernels";
1353 var base_url = $('body').data('baseKernelUrl') + "kernels";
1354 this.kernel = new IPython.Kernel(base_url);
1354 this.kernel = new IPython.Kernel(base_url);
1355 this.kernel.start(this.notebook_id);
1355 this.kernel.start(this.notebook_id);
1356 // Now that the kernel has been created, tell the CodeCells about it.
1356 // Now that the kernel has been created, tell the CodeCells about it.
1357 var ncells = this.ncells();
1357 var ncells = this.ncells();
1358 for (var i=0; i<ncells; i++) {
1358 for (var i=0; i<ncells; i++) {
1359 var cell = this.get_cell(i);
1359 var cell = this.get_cell(i);
1360 if (cell instanceof IPython.CodeCell) {
1360 if (cell instanceof IPython.CodeCell) {
1361 cell.set_kernel(this.kernel)
1361 cell.set_kernel(this.kernel)
1362 };
1362 };
1363 };
1363 };
1364 };
1364 };
1365
1365
1366 /**
1366 /**
1367 * Prompt the user to restart the IPython kernel.
1367 * Prompt the user to restart the IPython kernel.
1368 *
1368 *
1369 * @method restart_kernel
1369 * @method restart_kernel
1370 */
1370 */
1371 Notebook.prototype.restart_kernel = function () {
1371 Notebook.prototype.restart_kernel = function () {
1372 var that = this;
1372 var that = this;
1373 IPython.dialog.modal({
1373 IPython.dialog.modal({
1374 title : "Restart kernel or continue running?",
1374 title : "Restart kernel or continue running?",
1375 body : $("<p/>").html(
1375 body : $("<p/>").html(
1376 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1376 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1377 ),
1377 ),
1378 buttons : {
1378 buttons : {
1379 "Continue running" : {},
1379 "Continue running" : {},
1380 "Restart" : {
1380 "Restart" : {
1381 "class" : "btn-danger",
1381 "class" : "btn-danger",
1382 "click" : function() {
1382 "click" : function() {
1383 that.kernel.restart();
1383 that.kernel.restart();
1384 }
1384 }
1385 }
1385 }
1386 }
1386 }
1387 });
1387 });
1388 };
1388 };
1389
1389
1390 /**
1390 /**
1391 * Run the selected cell.
1391 * Run the selected cell.
1392 *
1392 *
1393 * Execute or render cell outputs.
1393 * Execute or render cell outputs.
1394 *
1394 *
1395 * @method execute_selected_cell
1395 * @method execute_selected_cell
1396 * @param {Object} options Customize post-execution behavior
1396 * @param {Object} options Customize post-execution behavior
1397 */
1397 */
1398 Notebook.prototype.execute_selected_cell = function (options) {
1398 Notebook.prototype.execute_selected_cell = function (options) {
1399 // add_new: should a new cell be added if we are at the end of the nb
1399 // add_new: should a new cell be added if we are at the end of the nb
1400 // terminal: execute in terminal mode, which stays in the current cell
1400 // terminal: execute in terminal mode, which stays in the current cell
1401 var default_options = {terminal: false, add_new: true};
1401 var default_options = {terminal: false, add_new: true};
1402 $.extend(default_options, options);
1402 $.extend(default_options, options);
1403 var that = this;
1403 var that = this;
1404 var cell = that.get_selected_cell();
1404 var cell = that.get_selected_cell();
1405 var cell_index = that.find_cell_index(cell);
1405 var cell_index = that.find_cell_index(cell);
1406 if (cell instanceof IPython.CodeCell) {
1406 if (cell instanceof IPython.CodeCell) {
1407 cell.execute();
1407 cell.execute();
1408 }
1408 }
1409 if (default_options.terminal) {
1409 if (default_options.terminal) {
1410 cell.select_all();
1410 cell.select_all();
1411 } else {
1411 } else {
1412 if ((cell_index === (that.ncells()-1)) && default_options.add_new) {
1412 if ((cell_index === (that.ncells()-1)) && default_options.add_new) {
1413 that.insert_cell_below('code');
1413 that.insert_cell_below('code');
1414 // If we are adding a new cell at the end, scroll down to show it.
1414 // If we are adding a new cell at the end, scroll down to show it.
1415 that.scroll_to_bottom();
1415 that.scroll_to_bottom();
1416 } else {
1416 } else {
1417 that.select(cell_index+1);
1417 that.select(cell_index+1);
1418 };
1418 };
1419 };
1419 };
1420 this.set_dirty(true);
1420 this.set_dirty(true);
1421 };
1421 };
1422
1422
1423 /**
1423 /**
1424 * Execute all cells below the selected cell.
1424 * Execute all cells below the selected cell.
1425 *
1425 *
1426 * @method execute_cells_below
1426 * @method execute_cells_below
1427 */
1427 */
1428 Notebook.prototype.execute_cells_below = function () {
1428 Notebook.prototype.execute_cells_below = function () {
1429 this.execute_cell_range(this.get_selected_index(), this.ncells());
1429 this.execute_cell_range(this.get_selected_index(), this.ncells());
1430 this.scroll_to_bottom();
1430 this.scroll_to_bottom();
1431 };
1431 };
1432
1432
1433 /**
1433 /**
1434 * Execute all cells above the selected cell.
1434 * Execute all cells above the selected cell.
1435 *
1435 *
1436 * @method execute_cells_above
1436 * @method execute_cells_above
1437 */
1437 */
1438 Notebook.prototype.execute_cells_above = function () {
1438 Notebook.prototype.execute_cells_above = function () {
1439 this.execute_cell_range(0, this.get_selected_index());
1439 this.execute_cell_range(0, this.get_selected_index());
1440 };
1440 };
1441
1441
1442 /**
1442 /**
1443 * Execute all cells.
1443 * Execute all cells.
1444 *
1444 *
1445 * @method execute_all_cells
1445 * @method execute_all_cells
1446 */
1446 */
1447 Notebook.prototype.execute_all_cells = function () {
1447 Notebook.prototype.execute_all_cells = function () {
1448 this.execute_cell_range(0, this.ncells());
1448 this.execute_cell_range(0, this.ncells());
1449 this.scroll_to_bottom();
1449 this.scroll_to_bottom();
1450 };
1450 };
1451
1451
1452 /**
1452 /**
1453 * Execute a contiguous range of cells.
1453 * Execute a contiguous range of cells.
1454 *
1454 *
1455 * @method execute_cell_range
1455 * @method execute_cell_range
1456 * @param {Number} start Index of the first cell to execute (inclusive)
1456 * @param {Number} start Index of the first cell to execute (inclusive)
1457 * @param {Number} end Index of the last cell to execute (exclusive)
1457 * @param {Number} end Index of the last cell to execute (exclusive)
1458 */
1458 */
1459 Notebook.prototype.execute_cell_range = function (start, end) {
1459 Notebook.prototype.execute_cell_range = function (start, end) {
1460 for (var i=start; i<end; i++) {
1460 for (var i=start; i<end; i++) {
1461 this.select(i);
1461 this.select(i);
1462 this.execute_selected_cell({add_new:false});
1462 this.execute_selected_cell({add_new:false});
1463 };
1463 };
1464 };
1464 };
1465
1465
1466 // Persistance and loading
1466 // Persistance and loading
1467
1467
1468 /**
1468 /**
1469 * Getter method for this notebook's ID.
1469 * Getter method for this notebook's ID.
1470 *
1470 *
1471 * @method get_notebook_id
1471 * @method get_notebook_id
1472 * @return {String} This notebook's ID
1472 * @return {String} This notebook's ID
1473 */
1473 */
1474 Notebook.prototype.get_notebook_id = function () {
1474 Notebook.prototype.get_notebook_id = function () {
1475 return this.notebook_id;
1475 return this.notebook_id;
1476 };
1476 };
1477
1477
1478 /**
1478 /**
1479 * Getter method for this notebook's name.
1479 * Getter method for this notebook's name.
1480 *
1480 *
1481 * @method get_notebook_name
1481 * @method get_notebook_name
1482 * @return {String} This notebook's name
1482 * @return {String} This notebook's name
1483 */
1483 */
1484 Notebook.prototype.get_notebook_name = function () {
1484 Notebook.prototype.get_notebook_name = function () {
1485 return this.notebook_name;
1485 return this.notebook_name;
1486 };
1486 };
1487
1487
1488 /**
1488 /**
1489 * Setter method for this notebook's name.
1489 * Setter method for this notebook's name.
1490 *
1490 *
1491 * @method set_notebook_name
1491 * @method set_notebook_name
1492 * @param {String} name A new name for this notebook
1492 * @param {String} name A new name for this notebook
1493 */
1493 */
1494 Notebook.prototype.set_notebook_name = function (name) {
1494 Notebook.prototype.set_notebook_name = function (name) {
1495 this.notebook_name = name;
1495 this.notebook_name = name;
1496 };
1496 };
1497
1497
1498 /**
1498 /**
1499 * Check that a notebook's name is valid.
1499 * Check that a notebook's name is valid.
1500 *
1500 *
1501 * @method test_notebook_name
1501 * @method test_notebook_name
1502 * @param {String} nbname A name for this notebook
1502 * @param {String} nbname A name for this notebook
1503 * @return {Boolean} True if the name is valid, false if invalid
1503 * @return {Boolean} True if the name is valid, false if invalid
1504 */
1504 */
1505 Notebook.prototype.test_notebook_name = function (nbname) {
1505 Notebook.prototype.test_notebook_name = function (nbname) {
1506 nbname = nbname || '';
1506 nbname = nbname || '';
1507 if (this.notebook_name_blacklist_re.test(nbname) == false && nbname.length>0) {
1507 if (this.notebook_name_blacklist_re.test(nbname) == false && nbname.length>0) {
1508 return true;
1508 return true;
1509 } else {
1509 } else {
1510 return false;
1510 return false;
1511 };
1511 };
1512 };
1512 };
1513
1513
1514 /**
1514 /**
1515 * Load a notebook from JSON (.ipynb).
1515 * Load a notebook from JSON (.ipynb).
1516 *
1516 *
1517 * This currently handles one worksheet: others are deleted.
1517 * This currently handles one worksheet: others are deleted.
1518 *
1518 *
1519 * @method fromJSON
1519 * @method fromJSON
1520 * @param {Object} data JSON representation of a notebook
1520 * @param {Object} data JSON representation of a notebook
1521 */
1521 */
1522 Notebook.prototype.fromJSON = function (data) {
1522 Notebook.prototype.fromJSON = function (data) {
1523 var ncells = this.ncells();
1523 var ncells = this.ncells();
1524 var i;
1524 var i;
1525 for (i=0; i<ncells; i++) {
1525 for (i=0; i<ncells; i++) {
1526 // Always delete cell 0 as they get renumbered as they are deleted.
1526 // Always delete cell 0 as they get renumbered as they are deleted.
1527 this.delete_cell(0);
1527 this.delete_cell(0);
1528 };
1528 };
1529 // Save the metadata and name.
1529 // Save the metadata and name.
1530 this.metadata = data.metadata;
1530 this.metadata = data.metadata;
1531 this.notebook_name = data.metadata.name;
1531 this.notebook_name = data.metadata.name;
1532 // Only handle 1 worksheet for now.
1532 // Only handle 1 worksheet for now.
1533 var worksheet = data.worksheets[0];
1533 var worksheet = data.worksheets[0];
1534 if (worksheet !== undefined) {
1534 if (worksheet !== undefined) {
1535 if (worksheet.metadata) {
1535 if (worksheet.metadata) {
1536 this.worksheet_metadata = worksheet.metadata;
1536 this.worksheet_metadata = worksheet.metadata;
1537 }
1537 }
1538 var new_cells = worksheet.cells;
1538 var new_cells = worksheet.cells;
1539 ncells = new_cells.length;
1539 ncells = new_cells.length;
1540 var cell_data = null;
1540 var cell_data = null;
1541 var new_cell = null;
1541 var new_cell = null;
1542 for (i=0; i<ncells; i++) {
1542 for (i=0; i<ncells; i++) {
1543 cell_data = new_cells[i];
1543 cell_data = new_cells[i];
1544 // VERSIONHACK: plaintext -> raw
1544 // VERSIONHACK: plaintext -> raw
1545 // handle never-released plaintext name for raw cells
1545 // handle never-released plaintext name for raw cells
1546 if (cell_data.cell_type === 'plaintext'){
1546 if (cell_data.cell_type === 'plaintext'){
1547 cell_data.cell_type = 'raw';
1547 cell_data.cell_type = 'raw';
1548 }
1548 }
1549
1549
1550 new_cell = this.insert_cell_below(cell_data.cell_type);
1550 new_cell = this.insert_cell_below(cell_data.cell_type);
1551 new_cell.fromJSON(cell_data);
1551 new_cell.fromJSON(cell_data);
1552 };
1552 };
1553 };
1553 };
1554 if (data.worksheets.length > 1) {
1554 if (data.worksheets.length > 1) {
1555 IPython.dialog.modal({
1555 IPython.dialog.modal({
1556 title : "Multiple worksheets",
1556 title : "Multiple worksheets",
1557 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1557 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1558 "but this version of IPython can only handle the first. " +
1558 "but this version of IPython can only handle the first. " +
1559 "If you save this notebook, worksheets after the first will be lost.",
1559 "If you save this notebook, worksheets after the first will be lost.",
1560 buttons : {
1560 buttons : {
1561 OK : {
1561 OK : {
1562 class : "btn-danger"
1562 class : "btn-danger"
1563 }
1563 }
1564 }
1564 }
1565 });
1565 });
1566 }
1566 }
1567 };
1567 };
1568
1568
1569 /**
1569 /**
1570 * Dump this notebook into a JSON-friendly object.
1570 * Dump this notebook into a JSON-friendly object.
1571 *
1571 *
1572 * @method toJSON
1572 * @method toJSON
1573 * @return {Object} A JSON-friendly representation of this notebook.
1573 * @return {Object} A JSON-friendly representation of this notebook.
1574 */
1574 */
1575 Notebook.prototype.toJSON = function () {
1575 Notebook.prototype.toJSON = function () {
1576 var cells = this.get_cells();
1576 var cells = this.get_cells();
1577 var ncells = cells.length;
1577 var ncells = cells.length;
1578 var cell_array = new Array(ncells);
1578 var cell_array = new Array(ncells);
1579 for (var i=0; i<ncells; i++) {
1579 for (var i=0; i<ncells; i++) {
1580 cell_array[i] = cells[i].toJSON();
1580 cell_array[i] = cells[i].toJSON();
1581 };
1581 };
1582 var data = {
1582 var data = {
1583 // Only handle 1 worksheet for now.
1583 // Only handle 1 worksheet for now.
1584 worksheets : [{
1584 worksheets : [{
1585 cells: cell_array,
1585 cells: cell_array,
1586 metadata: this.worksheet_metadata
1586 metadata: this.worksheet_metadata
1587 }],
1587 }],
1588 metadata : this.metadata
1588 metadata : this.metadata
1589 };
1589 };
1590 return data;
1590 return data;
1591 };
1591 };
1592
1592
1593 /**
1593 /**
1594 * Start an autosave timer, for periodically saving the notebook.
1594 * Start an autosave timer, for periodically saving the notebook.
1595 *
1595 *
1596 * @method set_autosave_interval
1596 * @method set_autosave_interval
1597 * @param {Integer} interval the autosave interval in milliseconds
1597 * @param {Integer} interval the autosave interval in milliseconds
1598 */
1598 */
1599 Notebook.prototype.set_autosave_interval = function (interval) {
1599 Notebook.prototype.set_autosave_interval = function (interval) {
1600 var that = this;
1600 var that = this;
1601 // clear previous interval, so we don't get simultaneous timers
1601 // clear previous interval, so we don't get simultaneous timers
1602 if (this.autosave_timer) {
1602 if (this.autosave_timer) {
1603 clearInterval(this.autosave_timer);
1603 clearInterval(this.autosave_timer);
1604 }
1604 }
1605
1605
1606 this.autosave_interval = this.minimum_autosave_interval = interval;
1606 this.autosave_interval = this.minimum_autosave_interval = interval;
1607 if (interval) {
1607 if (interval) {
1608 this.autosave_timer = setInterval(function() {
1608 this.autosave_timer = setInterval(function() {
1609 if (that.dirty) {
1609 if (that.dirty) {
1610 that.save_notebook();
1610 that.save_notebook();
1611 }
1611 }
1612 }, interval);
1612 }, interval);
1613 $([IPython.events]).trigger("autosave_enabled.Notebook", interval);
1613 $([IPython.events]).trigger("autosave_enabled.Notebook", interval);
1614 } else {
1614 } else {
1615 this.autosave_timer = null;
1615 this.autosave_timer = null;
1616 $([IPython.events]).trigger("autosave_disabled.Notebook");
1616 $([IPython.events]).trigger("autosave_disabled.Notebook");
1617 };
1617 };
1618 };
1618 };
1619
1619
1620 /**
1620 /**
1621 * Save this notebook on the server.
1621 * Save this notebook on the server.
1622 *
1622 *
1623 * @method save_notebook
1623 * @method save_notebook
1624 */
1624 */
1625 Notebook.prototype.save_notebook = function () {
1625 Notebook.prototype.save_notebook = function () {
1626 // We may want to move the name/id/nbformat logic inside toJSON?
1626 // We may want to move the name/id/nbformat logic inside toJSON?
1627 var data = this.toJSON();
1627 var data = this.toJSON();
1628 data.metadata.name = this.notebook_name;
1628 data.metadata.name = this.notebook_name;
1629 data.nbformat = this.nbformat;
1629 data.nbformat = this.nbformat;
1630 data.nbformat_minor = this.nbformat_minor;
1630 data.nbformat_minor = this.nbformat_minor;
1631
1631
1632 // time the ajax call for autosave tuning purposes.
1632 // time the ajax call for autosave tuning purposes.
1633 var start = new Date().getTime();
1633 var start = new Date().getTime();
1634
1634
1635 // We do the call with settings so we can set cache to false.
1635 // We do the call with settings so we can set cache to false.
1636 var settings = {
1636 var settings = {
1637 processData : false,
1637 processData : false,
1638 cache : false,
1638 cache : false,
1639 type : "PUT",
1639 type : "PUT",
1640 data : JSON.stringify(data),
1640 data : JSON.stringify(data),
1641 headers : {'Content-Type': 'application/json'},
1641 headers : {'Content-Type': 'application/json'},
1642 success : $.proxy(this.save_notebook_success, this, start),
1642 success : $.proxy(this.save_notebook_success, this, start),
1643 error : $.proxy(this.save_notebook_error, this)
1643 error : $.proxy(this.save_notebook_error, this)
1644 };
1644 };
1645 $([IPython.events]).trigger('notebook_saving.Notebook');
1645 $([IPython.events]).trigger('notebook_saving.Notebook');
1646 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id;
1646 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id;
1647 $.ajax(url, settings);
1647 $.ajax(url, settings);
1648 };
1648 };
1649
1649
1650 /**
1650 /**
1651 * Success callback for saving a notebook.
1651 * Success callback for saving a notebook.
1652 *
1652 *
1653 * @method save_notebook_success
1653 * @method save_notebook_success
1654 * @param {Integer} start the time when the save request started
1654 * @param {Integer} start the time when the save request started
1655 * @param {Object} data JSON representation of a notebook
1655 * @param {Object} data JSON representation of a notebook
1656 * @param {String} status Description of response status
1656 * @param {String} status Description of response status
1657 * @param {jqXHR} xhr jQuery Ajax object
1657 * @param {jqXHR} xhr jQuery Ajax object
1658 */
1658 */
1659 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1659 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1660 this.set_dirty(false);
1660 this.set_dirty(false);
1661 $([IPython.events]).trigger('notebook_saved.Notebook');
1661 $([IPython.events]).trigger('notebook_saved.Notebook');
1662 this._update_autosave_interval(start);
1662 this._update_autosave_interval(start);
1663 if (this._checkpoint_after_save) {
1663 if (this._checkpoint_after_save) {
1664 this.create_checkpoint();
1664 this.create_checkpoint();
1665 this._checkpoint_after_save = false;
1665 this._checkpoint_after_save = false;
1666 };
1666 };
1667 };
1667 };
1668
1668
1669 /**
1669 /**
1670 * update the autosave interval based on how long the last save took
1670 * update the autosave interval based on how long the last save took
1671 *
1671 *
1672 * @method _update_autosave_interval
1672 * @method _update_autosave_interval
1673 * @param {Integer} timestamp when the save request started
1673 * @param {Integer} timestamp when the save request started
1674 */
1674 */
1675 Notebook.prototype._update_autosave_interval = function (start) {
1675 Notebook.prototype._update_autosave_interval = function (start) {
1676 var duration = (new Date().getTime() - start);
1676 var duration = (new Date().getTime() - start);
1677 if (this.autosave_interval) {
1677 if (this.autosave_interval) {
1678 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1678 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1679 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1679 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1680 // round to 10 seconds, otherwise we will be setting a new interval too often
1680 // round to 10 seconds, otherwise we will be setting a new interval too often
1681 interval = 10000 * Math.round(interval / 10000);
1681 interval = 10000 * Math.round(interval / 10000);
1682 // set new interval, if it's changed
1682 // set new interval, if it's changed
1683 if (interval != this.autosave_interval) {
1683 if (interval != this.autosave_interval) {
1684 this.set_autosave_interval(interval);
1684 this.set_autosave_interval(interval);
1685 }
1685 }
1686 }
1686 }
1687 };
1687 };
1688
1688
1689 /**
1689 /**
1690 * Failure callback for saving a notebook.
1690 * Failure callback for saving a notebook.
1691 *
1691 *
1692 * @method save_notebook_error
1692 * @method save_notebook_error
1693 * @param {jqXHR} xhr jQuery Ajax object
1693 * @param {jqXHR} xhr jQuery Ajax object
1694 * @param {String} status Description of response status
1694 * @param {String} status Description of response status
1695 * @param {String} error_msg HTTP error message
1695 * @param {String} error_msg HTTP error message
1696 */
1696 */
1697 Notebook.prototype.save_notebook_error = function (xhr, status, error_msg) {
1697 Notebook.prototype.save_notebook_error = function (xhr, status, error_msg) {
1698 $([IPython.events]).trigger('notebook_save_failed.Notebook');
1698 $([IPython.events]).trigger('notebook_save_failed.Notebook');
1699 };
1699 };
1700
1700
1701 /**
1701 /**
1702 * Request a notebook's data from the server.
1702 * Request a notebook's data from the server.
1703 *
1703 *
1704 * @method load_notebook
1704 * @method load_notebook
1705 * @param {String} notebook_id A notebook to load
1705 * @param {String} notebook_id A notebook to load
1706 */
1706 */
1707 Notebook.prototype.load_notebook = function (notebook_id) {
1707 Notebook.prototype.load_notebook = function (notebook_id) {
1708 var that = this;
1708 var that = this;
1709 this.notebook_id = notebook_id;
1709 this.notebook_id = notebook_id;
1710 // We do the call with settings so we can set cache to false.
1710 // We do the call with settings so we can set cache to false.
1711 var settings = {
1711 var settings = {
1712 processData : false,
1712 processData : false,
1713 cache : false,
1713 cache : false,
1714 type : "GET",
1714 type : "GET",
1715 dataType : "json",
1715 dataType : "json",
1716 success : $.proxy(this.load_notebook_success,this),
1716 success : $.proxy(this.load_notebook_success,this),
1717 error : $.proxy(this.load_notebook_error,this),
1717 error : $.proxy(this.load_notebook_error,this),
1718 };
1718 };
1719 $([IPython.events]).trigger('notebook_loading.Notebook');
1719 $([IPython.events]).trigger('notebook_loading.Notebook');
1720 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id;
1720 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id;
1721 $.ajax(url, settings);
1721 $.ajax(url, settings);
1722 };
1722 };
1723
1723
1724 /**
1724 /**
1725 * Success callback for loading a notebook from the server.
1725 * Success callback for loading a notebook from the server.
1726 *
1726 *
1727 * Load notebook data from the JSON response.
1727 * Load notebook data from the JSON response.
1728 *
1728 *
1729 * @method load_notebook_success
1729 * @method load_notebook_success
1730 * @param {Object} data JSON representation of a notebook
1730 * @param {Object} data JSON representation of a notebook
1731 * @param {String} status Description of response status
1731 * @param {String} status Description of response status
1732 * @param {jqXHR} xhr jQuery Ajax object
1732 * @param {jqXHR} xhr jQuery Ajax object
1733 */
1733 */
1734 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
1734 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
1735 this.fromJSON(data);
1735 this.fromJSON(data);
1736 if (this.ncells() === 0) {
1736 if (this.ncells() === 0) {
1737 this.insert_cell_below('code');
1737 this.insert_cell_below('code');
1738 };
1738 };
1739 this.set_dirty(false);
1739 this.set_dirty(false);
1740 this.select(0);
1740 this.select(0);
1741 this.scroll_to_top();
1741 this.scroll_to_top();
1742 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
1742 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
1743 var msg = "This notebook has been converted from an older " +
1743 var msg = "This notebook has been converted from an older " +
1744 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
1744 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
1745 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
1745 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
1746 "newer notebook format will be used and older versions of IPython " +
1746 "newer notebook format will be used and older versions of IPython " +
1747 "may not be able to read it. To keep the older version, close the " +
1747 "may not be able to read it. To keep the older version, close the " +
1748 "notebook without saving it.";
1748 "notebook without saving it.";
1749 IPython.dialog.modal({
1749 IPython.dialog.modal({
1750 title : "Notebook converted",
1750 title : "Notebook converted",
1751 body : msg,
1751 body : msg,
1752 buttons : {
1752 buttons : {
1753 OK : {
1753 OK : {
1754 class : "btn-primary"
1754 class : "btn-primary"
1755 }
1755 }
1756 }
1756 }
1757 });
1757 });
1758 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
1758 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
1759 var that = this;
1759 var that = this;
1760 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
1760 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
1761 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
1761 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
1762 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
1762 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
1763 this_vs + ". You can still work with this notebook, but some features " +
1763 this_vs + ". You can still work with this notebook, but some features " +
1764 "introduced in later notebook versions may not be available."
1764 "introduced in later notebook versions may not be available."
1765
1765
1766 IPython.dialog.modal({
1766 IPython.dialog.modal({
1767 title : "Newer Notebook",
1767 title : "Newer Notebook",
1768 body : msg,
1768 body : msg,
1769 buttons : {
1769 buttons : {
1770 OK : {
1770 OK : {
1771 class : "btn-danger"
1771 class : "btn-danger"
1772 }
1772 }
1773 }
1773 }
1774 });
1774 });
1775
1775
1776 }
1776 }
1777
1777
1778 // Create the kernel after the notebook is completely loaded to prevent
1778 // Create the kernel after the notebook is completely loaded to prevent
1779 // code execution upon loading, which is a security risk.
1779 // code execution upon loading, which is a security risk.
1780 if (! this.read_only) {
1780 if (! this.read_only) {
1781 this.start_kernel();
1781 this.start_kernel();
1782 // load our checkpoint list
1782 // load our checkpoint list
1783 IPython.notebook.list_checkpoints();
1783 IPython.notebook.list_checkpoints();
1784 }
1784 }
1785 $([IPython.events]).trigger('notebook_loaded.Notebook');
1785 $([IPython.events]).trigger('notebook_loaded.Notebook');
1786 };
1786 };
1787
1787
1788 /**
1788 /**
1789 * Failure callback for loading a notebook from the server.
1789 * Failure callback for loading a notebook from the server.
1790 *
1790 *
1791 * @method load_notebook_error
1791 * @method load_notebook_error
1792 * @param {jqXHR} xhr jQuery Ajax object
1792 * @param {jqXHR} xhr jQuery Ajax object
1793 * @param {String} textStatus Description of response status
1793 * @param {String} textStatus Description of response status
1794 * @param {String} errorThrow HTTP error message
1794 * @param {String} errorThrow HTTP error message
1795 */
1795 */
1796 Notebook.prototype.load_notebook_error = function (xhr, textStatus, errorThrow) {
1796 Notebook.prototype.load_notebook_error = function (xhr, textStatus, errorThrow) {
1797 if (xhr.status === 500) {
1797 if (xhr.status === 400) {
1798 var msg = "An error occurred while loading this notebook. Most likely " +
1798 var msg = errorThrow;
1799 "this notebook is in a newer format than is supported by this " +
1799 } else if (xhr.status === 500) {
1800 "version of IPython. This version can load notebook formats " +
1800 var msg = "An unknown error occurred while loading this notebook. " +
1801 "This version can load notebook formats " +
1801 "v"+this.nbformat+" or earlier.";
1802 "v" + this.nbformat + " or earlier.";
1802
1803 }
1803 IPython.dialog.modal({
1804 IPython.dialog.modal({
1804 title: "Error loading notebook",
1805 title: "Error loading notebook",
1805 body : msg,
1806 body : msg,
1806 buttons : {
1807 buttons : {
1807 "OK": {}
1808 "OK": {}
1808 }
1809 }
1809 });
1810 });
1810 }
1811 }
1811 }
1812
1812
1813 /********************* checkpoint-related *********************/
1813 /********************* checkpoint-related *********************/
1814
1814
1815 /**
1815 /**
1816 * Save the notebook then immediately create a checkpoint.
1816 * Save the notebook then immediately create a checkpoint.
1817 *
1817 *
1818 * @method save_checkpoint
1818 * @method save_checkpoint
1819 */
1819 */
1820 Notebook.prototype.save_checkpoint = function () {
1820 Notebook.prototype.save_checkpoint = function () {
1821 this._checkpoint_after_save = true;
1821 this._checkpoint_after_save = true;
1822 this.save_notebook();
1822 this.save_notebook();
1823 };
1823 };
1824
1824
1825 /**
1825 /**
1826 * List checkpoints for this notebook.
1826 * List checkpoints for this notebook.
1827 *
1827 *
1828 * @method list_checkpoint
1828 * @method list_checkpoint
1829 */
1829 */
1830 Notebook.prototype.list_checkpoints = function () {
1830 Notebook.prototype.list_checkpoints = function () {
1831 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints';
1831 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints';
1832 $.get(url).done(
1832 $.get(url).done(
1833 $.proxy(this.list_checkpoints_success, this)
1833 $.proxy(this.list_checkpoints_success, this)
1834 ).fail(
1834 ).fail(
1835 $.proxy(this.list_checkpoints_error, this)
1835 $.proxy(this.list_checkpoints_error, this)
1836 );
1836 );
1837 };
1837 };
1838
1838
1839 /**
1839 /**
1840 * Success callback for listing checkpoints.
1840 * Success callback for listing checkpoints.
1841 *
1841 *
1842 * @method list_checkpoint_success
1842 * @method list_checkpoint_success
1843 * @param {Object} data JSON representation of a checkpoint
1843 * @param {Object} data JSON representation of a checkpoint
1844 * @param {String} status Description of response status
1844 * @param {String} status Description of response status
1845 * @param {jqXHR} xhr jQuery Ajax object
1845 * @param {jqXHR} xhr jQuery Ajax object
1846 */
1846 */
1847 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
1847 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
1848 var data = $.parseJSON(data);
1848 var data = $.parseJSON(data);
1849 if (data.length) {
1849 if (data.length) {
1850 this.last_checkpoint = data[0];
1850 this.last_checkpoint = data[0];
1851 } else {
1851 } else {
1852 this.last_checkpoint = null;
1852 this.last_checkpoint = null;
1853 }
1853 }
1854 $([IPython.events]).trigger('checkpoints_listed.Notebook', [data]);
1854 $([IPython.events]).trigger('checkpoints_listed.Notebook', [data]);
1855 };
1855 };
1856
1856
1857 /**
1857 /**
1858 * Failure callback for listing a checkpoint.
1858 * Failure callback for listing a checkpoint.
1859 *
1859 *
1860 * @method list_checkpoint_error
1860 * @method list_checkpoint_error
1861 * @param {jqXHR} xhr jQuery Ajax object
1861 * @param {jqXHR} xhr jQuery Ajax object
1862 * @param {String} status Description of response status
1862 * @param {String} status Description of response status
1863 * @param {String} error_msg HTTP error message
1863 * @param {String} error_msg HTTP error message
1864 */
1864 */
1865 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
1865 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
1866 $([IPython.events]).trigger('list_checkpoints_failed.Notebook');
1866 $([IPython.events]).trigger('list_checkpoints_failed.Notebook');
1867 };
1867 };
1868
1868
1869 /**
1869 /**
1870 * Create a checkpoint of this notebook on the server from the most recent save.
1870 * Create a checkpoint of this notebook on the server from the most recent save.
1871 *
1871 *
1872 * @method create_checkpoint
1872 * @method create_checkpoint
1873 */
1873 */
1874 Notebook.prototype.create_checkpoint = function () {
1874 Notebook.prototype.create_checkpoint = function () {
1875 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints';
1875 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints';
1876 $.post(url).done(
1876 $.post(url).done(
1877 $.proxy(this.create_checkpoint_success, this)
1877 $.proxy(this.create_checkpoint_success, this)
1878 ).fail(
1878 ).fail(
1879 $.proxy(this.create_checkpoint_error, this)
1879 $.proxy(this.create_checkpoint_error, this)
1880 );
1880 );
1881 };
1881 };
1882
1882
1883 /**
1883 /**
1884 * Success callback for creating a checkpoint.
1884 * Success callback for creating a checkpoint.
1885 *
1885 *
1886 * @method create_checkpoint_success
1886 * @method create_checkpoint_success
1887 * @param {Object} data JSON representation of a checkpoint
1887 * @param {Object} data JSON representation of a checkpoint
1888 * @param {String} status Description of response status
1888 * @param {String} status Description of response status
1889 * @param {jqXHR} xhr jQuery Ajax object
1889 * @param {jqXHR} xhr jQuery Ajax object
1890 */
1890 */
1891 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
1891 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
1892 var data = $.parseJSON(data);
1892 var data = $.parseJSON(data);
1893 this.last_checkpoint = data;
1893 this.last_checkpoint = data;
1894 $([IPython.events]).trigger('checkpoint_created.Notebook', data);
1894 $([IPython.events]).trigger('checkpoint_created.Notebook', data);
1895 };
1895 };
1896
1896
1897 /**
1897 /**
1898 * Failure callback for creating a checkpoint.
1898 * Failure callback for creating a checkpoint.
1899 *
1899 *
1900 * @method create_checkpoint_error
1900 * @method create_checkpoint_error
1901 * @param {jqXHR} xhr jQuery Ajax object
1901 * @param {jqXHR} xhr jQuery Ajax object
1902 * @param {String} status Description of response status
1902 * @param {String} status Description of response status
1903 * @param {String} error_msg HTTP error message
1903 * @param {String} error_msg HTTP error message
1904 */
1904 */
1905 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
1905 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
1906 $([IPython.events]).trigger('checkpoint_failed.Notebook');
1906 $([IPython.events]).trigger('checkpoint_failed.Notebook');
1907 };
1907 };
1908
1908
1909 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
1909 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
1910 var that = this;
1910 var that = this;
1911 var checkpoint = checkpoint || this.last_checkpoint;
1911 var checkpoint = checkpoint || this.last_checkpoint;
1912 if ( ! checkpoint ) {
1912 if ( ! checkpoint ) {
1913 console.log("restore dialog, but no checkpoint to restore to!");
1913 console.log("restore dialog, but no checkpoint to restore to!");
1914 return;
1914 return;
1915 }
1915 }
1916 var body = $('<div/>').append(
1916 var body = $('<div/>').append(
1917 $('<p/>').addClass("p-space").text(
1917 $('<p/>').addClass("p-space").text(
1918 "Are you sure you want to revert the notebook to " +
1918 "Are you sure you want to revert the notebook to " +
1919 "the latest checkpoint?"
1919 "the latest checkpoint?"
1920 ).append(
1920 ).append(
1921 $("<strong/>").text(
1921 $("<strong/>").text(
1922 " This cannot be undone."
1922 " This cannot be undone."
1923 )
1923 )
1924 )
1924 )
1925 ).append(
1925 ).append(
1926 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
1926 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
1927 ).append(
1927 ).append(
1928 $('<p/>').addClass("p-space").text(
1928 $('<p/>').addClass("p-space").text(
1929 Date(checkpoint.last_modified)
1929 Date(checkpoint.last_modified)
1930 ).css("text-align", "center")
1930 ).css("text-align", "center")
1931 );
1931 );
1932
1932
1933 IPython.dialog.modal({
1933 IPython.dialog.modal({
1934 title : "Revert notebook to checkpoint",
1934 title : "Revert notebook to checkpoint",
1935 body : body,
1935 body : body,
1936 buttons : {
1936 buttons : {
1937 Revert : {
1937 Revert : {
1938 class : "btn-danger",
1938 class : "btn-danger",
1939 click : function () {
1939 click : function () {
1940 that.restore_checkpoint(checkpoint.checkpoint_id);
1940 that.restore_checkpoint(checkpoint.checkpoint_id);
1941 }
1941 }
1942 },
1942 },
1943 Cancel : {}
1943 Cancel : {}
1944 }
1944 }
1945 });
1945 });
1946 }
1946 }
1947
1947
1948 /**
1948 /**
1949 * Restore the notebook to a checkpoint state.
1949 * Restore the notebook to a checkpoint state.
1950 *
1950 *
1951 * @method restore_checkpoint
1951 * @method restore_checkpoint
1952 * @param {String} checkpoint ID
1952 * @param {String} checkpoint ID
1953 */
1953 */
1954 Notebook.prototype.restore_checkpoint = function (checkpoint) {
1954 Notebook.prototype.restore_checkpoint = function (checkpoint) {
1955 $([IPython.events]).trigger('checkpoint_restoring.Notebook', checkpoint);
1955 $([IPython.events]).trigger('checkpoint_restoring.Notebook', checkpoint);
1956 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints/' + checkpoint;
1956 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints/' + checkpoint;
1957 $.post(url).done(
1957 $.post(url).done(
1958 $.proxy(this.restore_checkpoint_success, this)
1958 $.proxy(this.restore_checkpoint_success, this)
1959 ).fail(
1959 ).fail(
1960 $.proxy(this.restore_checkpoint_error, this)
1960 $.proxy(this.restore_checkpoint_error, this)
1961 );
1961 );
1962 };
1962 };
1963
1963
1964 /**
1964 /**
1965 * Success callback for restoring a notebook to a checkpoint.
1965 * Success callback for restoring a notebook to a checkpoint.
1966 *
1966 *
1967 * @method restore_checkpoint_success
1967 * @method restore_checkpoint_success
1968 * @param {Object} data (ignored, should be empty)
1968 * @param {Object} data (ignored, should be empty)
1969 * @param {String} status Description of response status
1969 * @param {String} status Description of response status
1970 * @param {jqXHR} xhr jQuery Ajax object
1970 * @param {jqXHR} xhr jQuery Ajax object
1971 */
1971 */
1972 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
1972 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
1973 $([IPython.events]).trigger('checkpoint_restored.Notebook');
1973 $([IPython.events]).trigger('checkpoint_restored.Notebook');
1974 this.load_notebook(this.notebook_id);
1974 this.load_notebook(this.notebook_id);
1975 };
1975 };
1976
1976
1977 /**
1977 /**
1978 * Failure callback for restoring a notebook to a checkpoint.
1978 * Failure callback for restoring a notebook to a checkpoint.
1979 *
1979 *
1980 * @method restore_checkpoint_error
1980 * @method restore_checkpoint_error
1981 * @param {jqXHR} xhr jQuery Ajax object
1981 * @param {jqXHR} xhr jQuery Ajax object
1982 * @param {String} status Description of response status
1982 * @param {String} status Description of response status
1983 * @param {String} error_msg HTTP error message
1983 * @param {String} error_msg HTTP error message
1984 */
1984 */
1985 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
1985 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
1986 $([IPython.events]).trigger('checkpoint_restore_failed.Notebook');
1986 $([IPython.events]).trigger('checkpoint_restore_failed.Notebook');
1987 };
1987 };
1988
1988
1989 /**
1989 /**
1990 * Delete a notebook checkpoint.
1990 * Delete a notebook checkpoint.
1991 *
1991 *
1992 * @method delete_checkpoint
1992 * @method delete_checkpoint
1993 * @param {String} checkpoint ID
1993 * @param {String} checkpoint ID
1994 */
1994 */
1995 Notebook.prototype.delete_checkpoint = function (checkpoint) {
1995 Notebook.prototype.delete_checkpoint = function (checkpoint) {
1996 $([IPython.events]).trigger('checkpoint_deleting.Notebook', checkpoint);
1996 $([IPython.events]).trigger('checkpoint_deleting.Notebook', checkpoint);
1997 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints/' + checkpoint;
1997 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints/' + checkpoint;
1998 $.ajax(url, {
1998 $.ajax(url, {
1999 type: 'DELETE',
1999 type: 'DELETE',
2000 success: $.proxy(this.delete_checkpoint_success, this),
2000 success: $.proxy(this.delete_checkpoint_success, this),
2001 error: $.proxy(this.delete_notebook_error,this)
2001 error: $.proxy(this.delete_notebook_error,this)
2002 });
2002 });
2003 };
2003 };
2004
2004
2005 /**
2005 /**
2006 * Success callback for deleting a notebook checkpoint
2006 * Success callback for deleting a notebook checkpoint
2007 *
2007 *
2008 * @method delete_checkpoint_success
2008 * @method delete_checkpoint_success
2009 * @param {Object} data (ignored, should be empty)
2009 * @param {Object} data (ignored, should be empty)
2010 * @param {String} status Description of response status
2010 * @param {String} status Description of response status
2011 * @param {jqXHR} xhr jQuery Ajax object
2011 * @param {jqXHR} xhr jQuery Ajax object
2012 */
2012 */
2013 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2013 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2014 $([IPython.events]).trigger('checkpoint_deleted.Notebook', data);
2014 $([IPython.events]).trigger('checkpoint_deleted.Notebook', data);
2015 this.load_notebook(this.notebook_id);
2015 this.load_notebook(this.notebook_id);
2016 };
2016 };
2017
2017
2018 /**
2018 /**
2019 * Failure callback for deleting a notebook checkpoint.
2019 * Failure callback for deleting a notebook checkpoint.
2020 *
2020 *
2021 * @method delete_checkpoint_error
2021 * @method delete_checkpoint_error
2022 * @param {jqXHR} xhr jQuery Ajax object
2022 * @param {jqXHR} xhr jQuery Ajax object
2023 * @param {String} status Description of response status
2023 * @param {String} status Description of response status
2024 * @param {String} error_msg HTTP error message
2024 * @param {String} error_msg HTTP error message
2025 */
2025 */
2026 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2026 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2027 $([IPython.events]).trigger('checkpoint_delete_failed.Notebook');
2027 $([IPython.events]).trigger('checkpoint_delete_failed.Notebook');
2028 };
2028 };
2029
2029
2030
2030
2031 IPython.Notebook = Notebook;
2031 IPython.Notebook = Notebook;
2032
2032
2033
2033
2034 return IPython;
2034 return IPython;
2035
2035
2036 }(IPython));
2036 }(IPython));
2037
2037
@@ -1,221 +1,226 b''
1 """The official API for working with notebooks in the current format version.
1 """The official API for working with notebooks in the current format version.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 from __future__ import print_function
19 from __future__ import print_function
20 import json
20 import json
21 from xml.etree import ElementTree as ET
21 from xml.etree import ElementTree as ET
22 import re
22 import re
23
23
24 from IPython.nbformat import v3
24 from IPython.nbformat import v3
25 from IPython.nbformat import v2
25 from IPython.nbformat import v2
26 from IPython.nbformat import v1
26 from IPython.nbformat import v1
27
27
28 from IPython.nbformat.v3 import (
28 from IPython.nbformat.v3 import (
29 NotebookNode,
29 NotebookNode,
30 new_code_cell, new_text_cell, new_notebook, new_output, new_worksheet,
30 new_code_cell, new_text_cell, new_notebook, new_output, new_worksheet,
31 parse_filename, new_metadata, new_author, new_heading_cell, nbformat,
31 parse_filename, new_metadata, new_author, new_heading_cell, nbformat,
32 nbformat_minor,
32 nbformat_minor,
33 )
33 )
34
34
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36 # Code
36 # Code
37 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
38
38
39 current_nbformat = nbformat
39 current_nbformat = nbformat
40 current_nbformat_minor = nbformat_minor
40 current_nbformat_minor = nbformat_minor
41
41
42
42
43 class NBFormatError(ValueError):
44 pass
43
45
44 class NBFormatError(Exception):
46 class NotJSONError(ValueError):
45 pass
47 pass
46
48
47
49
48 def parse_json(s, **kwargs):
50 def parse_json(s, **kwargs):
49 """Parse a string into a (nbformat, dict) tuple."""
51 """Parse a string into a (nbformat, dict) tuple."""
52 try:
50 d = json.loads(s, **kwargs)
53 d = json.loads(s, **kwargs)
54 except ValueError:
55 raise NotJSONError("Notebook does not appear to be JSON: %r" % s[:16])
51 nbf = d.get('nbformat', 1)
56 nbf = d.get('nbformat', 1)
52 nbm = d.get('nbformat_minor', 0)
57 nbm = d.get('nbformat_minor', 0)
53 return nbf, nbm, d
58 return nbf, nbm, d
54
59
55
60
56 def parse_py(s, **kwargs):
61 def parse_py(s, **kwargs):
57 """Parse a string into a (nbformat, string) tuple."""
62 """Parse a string into a (nbformat, string) tuple."""
58 nbf = current_nbformat
63 nbf = current_nbformat
59 nbm = current_nbformat_minor
64 nbm = current_nbformat_minor
60
65
61 pattern = r'# <nbformat>(?P<nbformat>\d+[\.\d+]*)</nbformat>'
66 pattern = r'# <nbformat>(?P<nbformat>\d+[\.\d+]*)</nbformat>'
62 m = re.search(pattern,s)
67 m = re.search(pattern,s)
63 if m is not None:
68 if m is not None:
64 digits = m.group('nbformat').split('.')
69 digits = m.group('nbformat').split('.')
65 nbf = int(digits[0])
70 nbf = int(digits[0])
66 if len(digits) > 1:
71 if len(digits) > 1:
67 nbm = int(digits[1])
72 nbm = int(digits[1])
68
73
69 return nbf, nbm, s
74 return nbf, nbm, s
70
75
71
76
72 def reads_json(s, **kwargs):
77 def reads_json(s, **kwargs):
73 """Read a JSON notebook from a string and return the NotebookNode object."""
78 """Read a JSON notebook from a string and return the NotebookNode object."""
74 nbf, minor, d = parse_json(s, **kwargs)
79 nbf, minor, d = parse_json(s, **kwargs)
75 if nbf == 1:
80 if nbf == 1:
76 nb = v1.to_notebook_json(d, **kwargs)
81 nb = v1.to_notebook_json(d, **kwargs)
77 nb = v3.convert_to_this_nbformat(nb, orig_version=1)
82 nb = v3.convert_to_this_nbformat(nb, orig_version=1)
78 elif nbf == 2:
83 elif nbf == 2:
79 nb = v2.to_notebook_json(d, **kwargs)
84 nb = v2.to_notebook_json(d, **kwargs)
80 nb = v3.convert_to_this_nbformat(nb, orig_version=2)
85 nb = v3.convert_to_this_nbformat(nb, orig_version=2)
81 elif nbf == 3:
86 elif nbf == 3:
82 nb = v3.to_notebook_json(d, **kwargs)
87 nb = v3.to_notebook_json(d, **kwargs)
83 nb = v3.convert_to_this_nbformat(nb, orig_version=3, orig_minor=minor)
88 nb = v3.convert_to_this_nbformat(nb, orig_version=3, orig_minor=minor)
84 else:
89 else:
85 raise NBFormatError('Unsupported JSON nbformat version: %i' % nbf)
90 raise NBFormatError('Unsupported JSON nbformat version %s (supported version: %i)' % (nbf, 3))
86 return nb
91 return nb
87
92
88
93
89 def writes_json(nb, **kwargs):
94 def writes_json(nb, **kwargs):
90 return v3.writes_json(nb, **kwargs)
95 return v3.writes_json(nb, **kwargs)
91
96
92
97
93 def reads_py(s, **kwargs):
98 def reads_py(s, **kwargs):
94 """Read a .py notebook from a string and return the NotebookNode object."""
99 """Read a .py notebook from a string and return the NotebookNode object."""
95 nbf, nbm, s = parse_py(s, **kwargs)
100 nbf, nbm, s = parse_py(s, **kwargs)
96 if nbf == 2:
101 if nbf == 2:
97 nb = v2.to_notebook_py(s, **kwargs)
102 nb = v2.to_notebook_py(s, **kwargs)
98 elif nbf == 3:
103 elif nbf == 3:
99 nb = v3.to_notebook_py(s, **kwargs)
104 nb = v3.to_notebook_py(s, **kwargs)
100 else:
105 else:
101 raise NBFormatError('Unsupported PY nbformat version: %i' % nbf)
106 raise NBFormatError('Unsupported PY nbformat version: %i' % nbf)
102 return nb
107 return nb
103
108
104
109
105 def writes_py(nb, **kwargs):
110 def writes_py(nb, **kwargs):
106 return v3.writes_py(nb, **kwargs)
111 return v3.writes_py(nb, **kwargs)
107
112
108
113
109 # High level API
114 # High level API
110
115
111
116
112 def reads(s, format, **kwargs):
117 def reads(s, format, **kwargs):
113 """Read a notebook from a string and return the NotebookNode object.
118 """Read a notebook from a string and return the NotebookNode object.
114
119
115 This function properly handles notebooks of any version. The notebook
120 This function properly handles notebooks of any version. The notebook
116 returned will always be in the current version's format.
121 returned will always be in the current version's format.
117
122
118 Parameters
123 Parameters
119 ----------
124 ----------
120 s : unicode
125 s : unicode
121 The raw unicode string to read the notebook from.
126 The raw unicode string to read the notebook from.
122 format : (u'json', u'ipynb', u'py')
127 format : (u'json', u'ipynb', u'py')
123 The format that the string is in.
128 The format that the string is in.
124
129
125 Returns
130 Returns
126 -------
131 -------
127 nb : NotebookNode
132 nb : NotebookNode
128 The notebook that was read.
133 The notebook that was read.
129 """
134 """
130 format = unicode(format)
135 format = unicode(format)
131 if format == u'json' or format == u'ipynb':
136 if format == u'json' or format == u'ipynb':
132 return reads_json(s, **kwargs)
137 return reads_json(s, **kwargs)
133 elif format == u'py':
138 elif format == u'py':
134 return reads_py(s, **kwargs)
139 return reads_py(s, **kwargs)
135 else:
140 else:
136 raise NBFormatError('Unsupported format: %s' % format)
141 raise NBFormatError('Unsupported format: %s' % format)
137
142
138
143
139 def writes(nb, format, **kwargs):
144 def writes(nb, format, **kwargs):
140 """Write a notebook to a string in a given format in the current nbformat version.
145 """Write a notebook to a string in a given format in the current nbformat version.
141
146
142 This function always writes the notebook in the current nbformat version.
147 This function always writes the notebook in the current nbformat version.
143
148
144 Parameters
149 Parameters
145 ----------
150 ----------
146 nb : NotebookNode
151 nb : NotebookNode
147 The notebook to write.
152 The notebook to write.
148 format : (u'json', u'ipynb', u'py')
153 format : (u'json', u'ipynb', u'py')
149 The format to write the notebook in.
154 The format to write the notebook in.
150
155
151 Returns
156 Returns
152 -------
157 -------
153 s : unicode
158 s : unicode
154 The notebook string.
159 The notebook string.
155 """
160 """
156 format = unicode(format)
161 format = unicode(format)
157 if format == u'json' or format == u'ipynb':
162 if format == u'json' or format == u'ipynb':
158 return writes_json(nb, **kwargs)
163 return writes_json(nb, **kwargs)
159 elif format == u'py':
164 elif format == u'py':
160 return writes_py(nb, **kwargs)
165 return writes_py(nb, **kwargs)
161 else:
166 else:
162 raise NBFormatError('Unsupported format: %s' % format)
167 raise NBFormatError('Unsupported format: %s' % format)
163
168
164
169
165 def read(fp, format, **kwargs):
170 def read(fp, format, **kwargs):
166 """Read a notebook from a file and return the NotebookNode object.
171 """Read a notebook from a file and return the NotebookNode object.
167
172
168 This function properly handles notebooks of any version. The notebook
173 This function properly handles notebooks of any version. The notebook
169 returned will always be in the current version's format.
174 returned will always be in the current version's format.
170
175
171 Parameters
176 Parameters
172 ----------
177 ----------
173 fp : file
178 fp : file
174 Any file-like object with a read method.
179 Any file-like object with a read method.
175 format : (u'json', u'ipynb', u'py')
180 format : (u'json', u'ipynb', u'py')
176 The format that the string is in.
181 The format that the string is in.
177
182
178 Returns
183 Returns
179 -------
184 -------
180 nb : NotebookNode
185 nb : NotebookNode
181 The notebook that was read.
186 The notebook that was read.
182 """
187 """
183 return reads(fp.read(), format, **kwargs)
188 return reads(fp.read(), format, **kwargs)
184
189
185
190
186 def write(nb, fp, format, **kwargs):
191 def write(nb, fp, format, **kwargs):
187 """Write a notebook to a file in a given format in the current nbformat version.
192 """Write a notebook to a file in a given format in the current nbformat version.
188
193
189 This function always writes the notebook in the current nbformat version.
194 This function always writes the notebook in the current nbformat version.
190
195
191 Parameters
196 Parameters
192 ----------
197 ----------
193 nb : NotebookNode
198 nb : NotebookNode
194 The notebook to write.
199 The notebook to write.
195 fp : file
200 fp : file
196 Any file-like object with a write method.
201 Any file-like object with a write method.
197 format : (u'json', u'ipynb', u'py')
202 format : (u'json', u'ipynb', u'py')
198 The format to write the notebook in.
203 The format to write the notebook in.
199
204
200 Returns
205 Returns
201 -------
206 -------
202 s : unicode
207 s : unicode
203 The notebook string.
208 The notebook string.
204 """
209 """
205 return fp.write(writes(nb, format, **kwargs))
210 return fp.write(writes(nb, format, **kwargs))
206
211
207 def _convert_to_metadata():
212 def _convert_to_metadata():
208 """Convert to a notebook having notebook metadata."""
213 """Convert to a notebook having notebook metadata."""
209 import glob
214 import glob
210 for fname in glob.glob('*.ipynb'):
215 for fname in glob.glob('*.ipynb'):
211 print('Converting file:',fname)
216 print('Converting file:',fname)
212 with open(fname,'r') as f:
217 with open(fname,'r') as f:
213 nb = read(f,u'json')
218 nb = read(f,u'json')
214 md = new_metadata()
219 md = new_metadata()
215 if u'name' in nb:
220 if u'name' in nb:
216 md.name = nb.name
221 md.name = nb.name
217 del nb[u'name']
222 del nb[u'name']
218 nb.metadata = md
223 nb.metadata = md
219 with open(fname,'w') as f:
224 with open(fname,'w') as f:
220 write(nb, f, u'json')
225 write(nb, f, u'json')
221
226
General Comments 0
You need to be logged in to leave comments. Login now