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