##// END OF EJS Templates
add dialogs for failed save/load...
MinRK -
Show More
@@ -1,536 +1,545 b''
1 1 """A contents manager that uses the local file system for storage."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import base64
7 7 import io
8 8 import os
9 9 import glob
10 10 import shutil
11 11
12 12 from tornado import web
13 13
14 14 from .manager import ContentsManager
15 15 from IPython.nbformat import current
16 16 from IPython.utils.io import atomic_writing
17 17 from IPython.utils.path import ensure_dir_exists
18 18 from IPython.utils.traitlets import Unicode, Bool, TraitError
19 19 from IPython.utils.py3compat import getcwd
20 20 from IPython.utils import tz
21 21 from IPython.html.utils import is_hidden, to_os_path, url_path_join
22 22
23 23
24 24 class FileContentsManager(ContentsManager):
25 25
26 26 root_dir = Unicode(getcwd(), config=True)
27 27
28 28 save_script = Bool(False, config=True, help='DEPRECATED, IGNORED')
29 29 def _save_script_changed(self):
30 30 self.log.warn("""
31 31 Automatically saving notebooks as scripts has been removed.
32 32 Use `ipython nbconvert --to python [notebook]` instead.
33 33 """)
34 34
35 35 def _root_dir_changed(self, name, old, new):
36 36 """Do a bit of validation of the root_dir."""
37 37 if not os.path.isabs(new):
38 38 # If we receive a non-absolute path, make it absolute.
39 39 self.root_dir = os.path.abspath(new)
40 40 return
41 41 if not os.path.isdir(new):
42 42 raise TraitError("%r is not a directory" % new)
43 43
44 44 checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
45 45 help="""The directory name in which to keep file checkpoints
46 46
47 47 This is a path relative to the file's own directory.
48 48
49 49 By default, it is .ipynb_checkpoints
50 50 """
51 51 )
52 52
53 53 def _copy(self, src, dest):
54 54 """copy src to dest
55 55
56 56 like shutil.copy2, but log errors in copystat
57 57 """
58 58 shutil.copyfile(src, dest)
59 59 try:
60 60 shutil.copystat(src, dest)
61 61 except OSError as e:
62 62 self.log.debug("copystat on %s failed", dest, exc_info=True)
63 63
64 64 def _get_os_path(self, name=None, path=''):
65 65 """Given a filename and API path, return its file system
66 66 path.
67 67
68 68 Parameters
69 69 ----------
70 70 name : string
71 71 A filename
72 72 path : string
73 73 The relative API path to the named file.
74 74
75 75 Returns
76 76 -------
77 77 path : string
78 78 API path to be evaluated relative to root_dir.
79 79 """
80 80 if name is not None:
81 81 path = url_path_join(path, name)
82 82 return to_os_path(path, self.root_dir)
83 83
84 84 def path_exists(self, path):
85 85 """Does the API-style path refer to an extant directory?
86 86
87 87 API-style wrapper for os.path.isdir
88 88
89 89 Parameters
90 90 ----------
91 91 path : string
92 92 The path to check. This is an API path (`/` separated,
93 93 relative to root_dir).
94 94
95 95 Returns
96 96 -------
97 97 exists : bool
98 98 Whether the path is indeed a directory.
99 99 """
100 100 path = path.strip('/')
101 101 os_path = self._get_os_path(path=path)
102 102 return os.path.isdir(os_path)
103 103
104 104 def is_hidden(self, path):
105 105 """Does the API style path correspond to a hidden directory or file?
106 106
107 107 Parameters
108 108 ----------
109 109 path : string
110 110 The path to check. This is an API path (`/` separated,
111 111 relative to root_dir).
112 112
113 113 Returns
114 114 -------
115 115 exists : bool
116 116 Whether the path is hidden.
117 117
118 118 """
119 119 path = path.strip('/')
120 120 os_path = self._get_os_path(path=path)
121 121 return is_hidden(os_path, self.root_dir)
122 122
123 123 def file_exists(self, name, path=''):
124 124 """Returns True if the file exists, else returns False.
125 125
126 126 API-style wrapper for os.path.isfile
127 127
128 128 Parameters
129 129 ----------
130 130 name : string
131 131 The name of the file you are checking.
132 132 path : string
133 133 The relative path to the file's directory (with '/' as separator)
134 134
135 135 Returns
136 136 -------
137 137 exists : bool
138 138 Whether the file exists.
139 139 """
140 140 path = path.strip('/')
141 141 nbpath = self._get_os_path(name, path=path)
142 142 return os.path.isfile(nbpath)
143 143
144 144 def exists(self, name=None, path=''):
145 145 """Returns True if the path [and name] exists, else returns False.
146 146
147 147 API-style wrapper for os.path.exists
148 148
149 149 Parameters
150 150 ----------
151 151 name : string
152 152 The name of the file you are checking.
153 153 path : string
154 154 The relative path to the file's directory (with '/' as separator)
155 155
156 156 Returns
157 157 -------
158 158 exists : bool
159 159 Whether the target exists.
160 160 """
161 161 path = path.strip('/')
162 162 os_path = self._get_os_path(name, path=path)
163 163 return os.path.exists(os_path)
164 164
165 165 def _base_model(self, name, path=''):
166 166 """Build the common base of a contents model"""
167 167 os_path = self._get_os_path(name, path)
168 168 info = os.stat(os_path)
169 169 last_modified = tz.utcfromtimestamp(info.st_mtime)
170 170 created = tz.utcfromtimestamp(info.st_ctime)
171 171 # Create the base model.
172 172 model = {}
173 173 model['name'] = name
174 174 model['path'] = path
175 175 model['last_modified'] = last_modified
176 176 model['created'] = created
177 177 model['content'] = None
178 178 model['format'] = None
179 model['message'] = None
179 180 return model
180 181
181 182 def _dir_model(self, name, path='', content=True):
182 183 """Build a model for a directory
183 184
184 185 if content is requested, will include a listing of the directory
185 186 """
186 187 os_path = self._get_os_path(name, path)
187 188
188 189 four_o_four = u'directory does not exist: %r' % os_path
189 190
190 191 if not os.path.isdir(os_path):
191 192 raise web.HTTPError(404, four_o_four)
192 193 elif is_hidden(os_path, self.root_dir):
193 194 self.log.info("Refusing to serve hidden directory %r, via 404 Error",
194 195 os_path
195 196 )
196 197 raise web.HTTPError(404, four_o_four)
197 198
198 199 if name is None:
199 200 if '/' in path:
200 201 path, name = path.rsplit('/', 1)
201 202 else:
202 203 name = ''
203 204 model = self._base_model(name, path)
204 205 model['type'] = 'directory'
205 206 dir_path = u'{}/{}'.format(path, name)
206 207 if content:
207 208 model['content'] = contents = []
208 209 for os_path in glob.glob(self._get_os_path('*', dir_path)):
209 210 name = os.path.basename(os_path)
210 211 # skip over broken symlinks in listing
211 212 if not os.path.exists(os_path):
212 213 self.log.warn("%s doesn't exist", os_path)
213 214 continue
214 215 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
215 216 contents.append(self.get_model(name=name, path=dir_path, content=False))
216 217
217 218 model['format'] = 'json'
218 219
219 220 return model
220 221
221 222 def _file_model(self, name, path='', content=True):
222 223 """Build a model for a file
223 224
224 225 if content is requested, include the file contents.
225 226 UTF-8 text files will be unicode, binary files will be base64-encoded.
226 227 """
227 228 model = self._base_model(name, path)
228 229 model['type'] = 'file'
229 230 if content:
230 231 os_path = self._get_os_path(name, path)
231 232 with io.open(os_path, 'rb') as f:
232 233 bcontent = f.read()
233 234 try:
234 235 model['content'] = bcontent.decode('utf8')
235 236 except UnicodeError as e:
236 237 model['content'] = base64.encodestring(bcontent).decode('ascii')
237 238 model['format'] = 'base64'
238 239 else:
239 240 model['format'] = 'text'
240 241 return model
241 242
242 243
243 244 def _notebook_model(self, name, path='', content=True):
244 245 """Build a notebook model
245 246
246 247 if content is requested, the notebook content will be populated
247 248 as a JSON structure (not double-serialized)
248 249 """
249 250 model = self._base_model(name, path)
250 251 model['type'] = 'notebook'
251 252 if content:
252 253 os_path = self._get_os_path(name, path)
253 254 with io.open(os_path, 'r', encoding='utf-8') as f:
254 255 try:
255 256 nb = current.read(f, u'json')
256 257 except Exception as e:
257 258 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
258 259 self.mark_trusted_cells(nb, name, path)
259 260 model['content'] = nb
260 261 model['format'] = 'json'
262 self.validate_notebook_model(model)
261 263 return model
262 264
263 265 def get_model(self, name, path='', content=True):
264 266 """ Takes a path and name for an entity and returns its model
265 267
266 268 Parameters
267 269 ----------
268 270 name : str
269 271 the name of the target
270 272 path : str
271 273 the API path that describes the relative path for the target
272 274
273 275 Returns
274 276 -------
275 277 model : dict
276 278 the contents model. If content=True, returns the contents
277 279 of the file or directory as well.
278 280 """
279 281 path = path.strip('/')
280 282
281 283 if not self.exists(name=name, path=path):
282 284 raise web.HTTPError(404, u'No such file or directory: %s/%s' % (path, name))
283 285
284 286 os_path = self._get_os_path(name, path)
285 287 if os.path.isdir(os_path):
286 288 model = self._dir_model(name, path, content)
287 289 elif name.endswith('.ipynb'):
288 290 model = self._notebook_model(name, path, content)
289 291 else:
290 292 model = self._file_model(name, path, content)
291 293 return model
292 294
293 295 def _save_notebook(self, os_path, model, name='', path=''):
294 296 """save a notebook file"""
295 297 # Save the notebook file
296 298 nb = current.to_notebook_json(model['content'])
297 299
298 300 self.check_and_sign(nb, name, path)
299 301
300 302 if 'name' in nb['metadata']:
301 303 nb['metadata']['name'] = u''
302 304
303 305 with atomic_writing(os_path, encoding='utf-8') as f:
304 306 current.write(nb, f, u'json')
305 307
306 308 def _save_file(self, os_path, model, name='', path=''):
307 309 """save a non-notebook file"""
308 310 fmt = model.get('format', None)
309 311 if fmt not in {'text', 'base64'}:
310 312 raise web.HTTPError(400, "Must specify format of file contents as 'text' or 'base64'")
311 313 try:
312 314 content = model['content']
313 315 if fmt == 'text':
314 316 bcontent = content.encode('utf8')
315 317 else:
316 318 b64_bytes = content.encode('ascii')
317 319 bcontent = base64.decodestring(b64_bytes)
318 320 except Exception as e:
319 321 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
320 322 with atomic_writing(os_path, text=False) as f:
321 323 f.write(bcontent)
322 324
323 325 def _save_directory(self, os_path, model, name='', path=''):
324 326 """create a directory"""
325 327 if is_hidden(os_path, self.root_dir):
326 328 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
327 329 if not os.path.exists(os_path):
328 330 os.mkdir(os_path)
329 331 elif not os.path.isdir(os_path):
330 332 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
331 333 else:
332 334 self.log.debug("Directory %r already exists", os_path)
333 335
334 336 def save(self, model, name='', path=''):
335 337 """Save the file model and return the model with no content."""
336 338 path = path.strip('/')
337 339
338 340 if 'type' not in model:
339 341 raise web.HTTPError(400, u'No file type provided')
340 342 if 'content' not in model and model['type'] != 'directory':
341 343 raise web.HTTPError(400, u'No file content provided')
342 344
343 345 # One checkpoint should always exist
344 346 if self.file_exists(name, path) and not self.list_checkpoints(name, path):
345 347 self.create_checkpoint(name, path)
346 348
347 349 new_path = model.get('path', path).strip('/')
348 350 new_name = model.get('name', name)
349 351
350 352 if path != new_path or name != new_name:
351 353 self.rename(name, path, new_name, new_path)
352 354
353 355 os_path = self._get_os_path(new_name, new_path)
354 356 self.log.debug("Saving %s", os_path)
355 357 try:
356 358 if model['type'] == 'notebook':
357 359 self._save_notebook(os_path, model, new_name, new_path)
358 360 elif model['type'] == 'file':
359 361 self._save_file(os_path, model, new_name, new_path)
360 362 elif model['type'] == 'directory':
361 363 self._save_directory(os_path, model, new_name, new_path)
362 364 else:
363 365 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
364 366 except web.HTTPError:
365 367 raise
366 368 except Exception as e:
367 369 raise web.HTTPError(400, u'Unexpected error while saving file: %s %s' % (os_path, e))
368 370
371 validation_message = None
372 if model['type'] == 'notebook':
373 self.validate_notebook_model(model)
374 validation_message = model.get('message', None)
375
369 376 model = self.get_model(new_name, new_path, content=False)
377 if validation_message:
378 model['message'] = validation_message
370 379 return model
371 380
372 381 def update(self, model, name, path=''):
373 382 """Update the file's path and/or name
374 383
375 384 For use in PATCH requests, to enable renaming a file without
376 385 re-uploading its contents. Only used for renaming at the moment.
377 386 """
378 387 path = path.strip('/')
379 388 new_name = model.get('name', name)
380 389 new_path = model.get('path', path).strip('/')
381 390 if path != new_path or name != new_name:
382 391 self.rename(name, path, new_name, new_path)
383 392 model = self.get_model(new_name, new_path, content=False)
384 393 return model
385 394
386 395 def delete(self, name, path=''):
387 396 """Delete file by name and path."""
388 397 path = path.strip('/')
389 398 os_path = self._get_os_path(name, path)
390 399 rm = os.unlink
391 400 if os.path.isdir(os_path):
392 401 listing = os.listdir(os_path)
393 402 # don't delete non-empty directories (checkpoints dir doesn't count)
394 403 if listing and listing != [self.checkpoint_dir]:
395 404 raise web.HTTPError(400, u'Directory %s not empty' % os_path)
396 405 elif not os.path.isfile(os_path):
397 406 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
398 407
399 408 # clear checkpoints
400 409 for checkpoint in self.list_checkpoints(name, path):
401 410 checkpoint_id = checkpoint['id']
402 411 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
403 412 if os.path.isfile(cp_path):
404 413 self.log.debug("Unlinking checkpoint %s", cp_path)
405 414 os.unlink(cp_path)
406 415
407 416 if os.path.isdir(os_path):
408 417 self.log.debug("Removing directory %s", os_path)
409 418 shutil.rmtree(os_path)
410 419 else:
411 420 self.log.debug("Unlinking file %s", os_path)
412 421 rm(os_path)
413 422
414 423 def rename(self, old_name, old_path, new_name, new_path):
415 424 """Rename a file."""
416 425 old_path = old_path.strip('/')
417 426 new_path = new_path.strip('/')
418 427 if new_name == old_name and new_path == old_path:
419 428 return
420 429
421 430 new_os_path = self._get_os_path(new_name, new_path)
422 431 old_os_path = self._get_os_path(old_name, old_path)
423 432
424 433 # Should we proceed with the move?
425 434 if os.path.isfile(new_os_path):
426 435 raise web.HTTPError(409, u'File with name already exists: %s' % new_os_path)
427 436
428 437 # Move the file
429 438 try:
430 439 shutil.move(old_os_path, new_os_path)
431 440 except Exception as e:
432 441 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_os_path, e))
433 442
434 443 # Move the checkpoints
435 444 old_checkpoints = self.list_checkpoints(old_name, old_path)
436 445 for cp in old_checkpoints:
437 446 checkpoint_id = cp['id']
438 447 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
439 448 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
440 449 if os.path.isfile(old_cp_path):
441 450 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
442 451 shutil.move(old_cp_path, new_cp_path)
443 452
444 453 # Checkpoint-related utilities
445 454
446 455 def get_checkpoint_path(self, checkpoint_id, name, path=''):
447 456 """find the path to a checkpoint"""
448 457 path = path.strip('/')
449 458 basename, ext = os.path.splitext(name)
450 459 filename = u"{name}-{checkpoint_id}{ext}".format(
451 460 name=basename,
452 461 checkpoint_id=checkpoint_id,
453 462 ext=ext,
454 463 )
455 464 os_path = self._get_os_path(path=path)
456 465 cp_dir = os.path.join(os_path, self.checkpoint_dir)
457 466 ensure_dir_exists(cp_dir)
458 467 cp_path = os.path.join(cp_dir, filename)
459 468 return cp_path
460 469
461 470 def get_checkpoint_model(self, checkpoint_id, name, path=''):
462 471 """construct the info dict for a given checkpoint"""
463 472 path = path.strip('/')
464 473 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
465 474 stats = os.stat(cp_path)
466 475 last_modified = tz.utcfromtimestamp(stats.st_mtime)
467 476 info = dict(
468 477 id = checkpoint_id,
469 478 last_modified = last_modified,
470 479 )
471 480 return info
472 481
473 482 # public checkpoint API
474 483
475 484 def create_checkpoint(self, name, path=''):
476 485 """Create a checkpoint from the current state of a file"""
477 486 path = path.strip('/')
478 487 src_path = self._get_os_path(name, path)
479 488 # only the one checkpoint ID:
480 489 checkpoint_id = u"checkpoint"
481 490 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
482 491 self.log.debug("creating checkpoint for %s", name)
483 492 self._copy(src_path, cp_path)
484 493
485 494 # return the checkpoint info
486 495 return self.get_checkpoint_model(checkpoint_id, name, path)
487 496
488 497 def list_checkpoints(self, name, path=''):
489 498 """list the checkpoints for a given file
490 499
491 500 This contents manager currently only supports one checkpoint per file.
492 501 """
493 502 path = path.strip('/')
494 503 checkpoint_id = "checkpoint"
495 504 os_path = self.get_checkpoint_path(checkpoint_id, name, path)
496 505 if not os.path.exists(os_path):
497 506 return []
498 507 else:
499 508 return [self.get_checkpoint_model(checkpoint_id, name, path)]
500 509
501 510
502 511 def restore_checkpoint(self, checkpoint_id, name, path=''):
503 512 """restore a file to a checkpointed state"""
504 513 path = path.strip('/')
505 514 self.log.info("restoring %s from checkpoint %s", name, checkpoint_id)
506 515 nb_path = self._get_os_path(name, path)
507 516 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
508 517 if not os.path.isfile(cp_path):
509 518 self.log.debug("checkpoint file does not exist: %s", cp_path)
510 519 raise web.HTTPError(404,
511 520 u'checkpoint does not exist: %s-%s' % (name, checkpoint_id)
512 521 )
513 522 # ensure notebook is readable (never restore from an unreadable notebook)
514 523 if cp_path.endswith('.ipynb'):
515 524 with io.open(cp_path, 'r', encoding='utf-8') as f:
516 525 current.read(f, u'json')
517 526 self._copy(cp_path, nb_path)
518 527 self.log.debug("copying %s -> %s", cp_path, nb_path)
519 528
520 529 def delete_checkpoint(self, checkpoint_id, name, path=''):
521 530 """delete a file's checkpoint"""
522 531 path = path.strip('/')
523 532 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
524 533 if not os.path.isfile(cp_path):
525 534 raise web.HTTPError(404,
526 535 u'Checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
527 536 )
528 537 self.log.debug("unlinking %s", cp_path)
529 538 os.unlink(cp_path)
530 539
531 540 def info_string(self):
532 541 return "Serving notebooks from local directory: %s" % self.root_dir
533 542
534 543 def get_kernel_path(self, name, path='', model=None):
535 544 """Return the initial working dir a kernel associated with a given notebook"""
536 545 return os.path.join(self.root_dir, path)
@@ -1,333 +1,344 b''
1 1 """A base class for contents managers."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from fnmatch import fnmatch
7 7 import itertools
8 import json
8 9 import os
9 10
10 11 from tornado.web import HTTPError
11 12
12 13 from IPython.config.configurable import LoggingConfigurable
13 14 from IPython.nbformat import current, sign
14 15 from IPython.utils.traitlets import Instance, Unicode, List
15 16
16 17
17 18 class ContentsManager(LoggingConfigurable):
18 19 """Base class for serving files and directories.
19 20
20 21 This serves any text or binary file,
21 22 as well as directories,
22 23 with special handling for JSON notebook documents.
23 24
24 25 Most APIs take a path argument,
25 26 which is always an API-style unicode path,
26 27 and always refers to a directory.
27 28
28 29 - unicode, not url-escaped
29 30 - '/'-separated
30 31 - leading and trailing '/' will be stripped
31 32 - if unspecified, path defaults to '',
32 33 indicating the root path.
33 34
34 35 name is also unicode, and refers to a specfic target:
35 36
36 37 - unicode, not url-escaped
37 38 - must not contain '/'
38 39 - It refers to an individual filename
39 40 - It may refer to a directory name,
40 41 in the case of listing or creating directories.
41 42
42 43 """
43 44
44 45 notary = Instance(sign.NotebookNotary)
45 46 def _notary_default(self):
46 47 return sign.NotebookNotary(parent=self)
47 48
48 49 hide_globs = List(Unicode, [
49 50 u'__pycache__', '*.pyc', '*.pyo',
50 51 '.DS_Store', '*.so', '*.dylib', '*~',
51 52 ], config=True, help="""
52 53 Glob patterns to hide in file and directory listings.
53 54 """)
54 55
55 56 untitled_notebook = Unicode("Untitled", config=True,
56 57 help="The base name used when creating untitled notebooks."
57 58 )
58 59
59 60 untitled_file = Unicode("untitled", config=True,
60 61 help="The base name used when creating untitled files."
61 62 )
62 63
63 64 untitled_directory = Unicode("Untitled Folder", config=True,
64 65 help="The base name used when creating untitled directories."
65 66 )
66 67
67 68 # ContentsManager API part 1: methods that must be
68 69 # implemented in subclasses.
69 70
70 71 def path_exists(self, path):
71 72 """Does the API-style path (directory) actually exist?
72 73
73 74 Like os.path.isdir
74 75
75 76 Override this method in subclasses.
76 77
77 78 Parameters
78 79 ----------
79 80 path : string
80 81 The path to check
81 82
82 83 Returns
83 84 -------
84 85 exists : bool
85 86 Whether the path does indeed exist.
86 87 """
87 88 raise NotImplementedError
88 89
89 90 def is_hidden(self, path):
90 91 """Does the API style path correspond to a hidden directory or file?
91 92
92 93 Parameters
93 94 ----------
94 95 path : string
95 96 The path to check. This is an API path (`/` separated,
96 97 relative to root dir).
97 98
98 99 Returns
99 100 -------
100 101 hidden : bool
101 102 Whether the path is hidden.
102 103
103 104 """
104 105 raise NotImplementedError
105 106
106 107 def file_exists(self, name, path=''):
107 108 """Does a file exist at the given name and path?
108 109
109 110 Like os.path.isfile
110 111
111 112 Override this method in subclasses.
112 113
113 114 Parameters
114 115 ----------
115 116 name : string
116 117 The name of the file you are checking.
117 118 path : string
118 119 The relative path to the file's directory (with '/' as separator)
119 120
120 121 Returns
121 122 -------
122 123 exists : bool
123 124 Whether the file exists.
124 125 """
125 126 raise NotImplementedError('must be implemented in a subclass')
126 127
127 128 def exists(self, name, path=''):
128 129 """Does a file or directory exist at the given name and path?
129 130
130 131 Like os.path.exists
131 132
132 133 Parameters
133 134 ----------
134 135 name : string
135 136 The name of the file you are checking.
136 137 path : string
137 138 The relative path to the file's directory (with '/' as separator)
138 139
139 140 Returns
140 141 -------
141 142 exists : bool
142 143 Whether the target exists.
143 144 """
144 145 return self.file_exists(name, path) or self.path_exists("%s/%s" % (path, name))
145 146
146 147 def get_model(self, name, path='', content=True):
147 148 """Get the model of a file or directory with or without content."""
148 149 raise NotImplementedError('must be implemented in a subclass')
149 150
150 151 def save(self, model, name, path=''):
151 152 """Save the file or directory and return the model with no content."""
152 153 raise NotImplementedError('must be implemented in a subclass')
153 154
154 155 def update(self, model, name, path=''):
155 156 """Update the file or directory and return the model with no content.
156 157
157 158 For use in PATCH requests, to enable renaming a file without
158 159 re-uploading its contents. Only used for renaming at the moment.
159 160 """
160 161 raise NotImplementedError('must be implemented in a subclass')
161 162
162 163 def delete(self, name, path=''):
163 164 """Delete file or directory by name and path."""
164 165 raise NotImplementedError('must be implemented in a subclass')
165 166
166 167 def create_checkpoint(self, name, path=''):
167 168 """Create a checkpoint of the current state of a file
168 169
169 170 Returns a checkpoint_id for the new checkpoint.
170 171 """
171 172 raise NotImplementedError("must be implemented in a subclass")
172 173
173 174 def list_checkpoints(self, name, path=''):
174 175 """Return a list of checkpoints for a given file"""
175 176 return []
176 177
177 178 def restore_checkpoint(self, checkpoint_id, name, path=''):
178 179 """Restore a file from one of its checkpoints"""
179 180 raise NotImplementedError("must be implemented in a subclass")
180 181
181 182 def delete_checkpoint(self, checkpoint_id, name, path=''):
182 183 """delete a checkpoint for a file"""
183 184 raise NotImplementedError("must be implemented in a subclass")
184 185
185 186 # ContentsManager API part 2: methods that have useable default
186 187 # implementations, but can be overridden in subclasses.
187 188
188 189 def info_string(self):
189 190 return "Serving contents"
190 191
191 192 def get_kernel_path(self, name, path='', model=None):
192 193 """ Return the path to start kernel in """
193 194 return path
194 195
195 196 def increment_filename(self, filename, path=''):
196 197 """Increment a filename until it is unique.
197 198
198 199 Parameters
199 200 ----------
200 201 filename : unicode
201 202 The name of a file, including extension
202 203 path : unicode
203 204 The API path of the target's directory
204 205
205 206 Returns
206 207 -------
207 208 name : unicode
208 209 A filename that is unique, based on the input filename.
209 210 """
210 211 path = path.strip('/')
211 212 basename, ext = os.path.splitext(filename)
212 213 for i in itertools.count():
213 214 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
214 215 ext=ext)
215 216 if not self.file_exists(name, path):
216 217 break
217 218 return name
218 219
220 def validate_notebook_model(self, model):
221 """Add failed-validation message to model"""
222 try:
223 current.validate(model['content'])
224 except current.ValidationError as e:
225 model['message'] = 'Notebook Validation failed: {}:\n{}'.format(
226 e.message, json.dumps(e.instance, indent=1, default=lambda obj: '<UNKNOWN>'),
227 )
228 return model
229
219 230 def create_file(self, model=None, path='', ext='.ipynb'):
220 231 """Create a new file or directory and return its model with no content."""
221 232 path = path.strip('/')
222 233 if model is None:
223 234 model = {}
224 235 if 'content' not in model and model.get('type', None) != 'directory':
225 236 if ext == '.ipynb':
226 237 metadata = current.new_metadata(name=u'')
227 238 model['content'] = current.new_notebook(metadata=metadata)
228 239 model['type'] = 'notebook'
229 240 model['format'] = 'json'
230 241 else:
231 242 model['content'] = ''
232 243 model['type'] = 'file'
233 244 model['format'] = 'text'
234 245 if 'name' not in model:
235 246 if model['type'] == 'directory':
236 247 untitled = self.untitled_directory
237 248 elif model['type'] == 'notebook':
238 249 untitled = self.untitled_notebook
239 250 elif model['type'] == 'file':
240 251 untitled = self.untitled_file
241 252 else:
242 253 raise HTTPError(400, "Unexpected model type: %r" % model['type'])
243 254 model['name'] = self.increment_filename(untitled + ext, path)
244 255
245 256 model['path'] = path
246 257 model = self.save(model, model['name'], model['path'])
247 258 return model
248 259
249 260 def copy(self, from_name, to_name=None, path=''):
250 261 """Copy an existing file and return its new model.
251 262
252 263 If to_name not specified, increment `from_name-Copy#.ext`.
253 264
254 265 copy_from can be a full path to a file,
255 266 or just a base name. If a base name, `path` is used.
256 267 """
257 268 path = path.strip('/')
258 269 if '/' in from_name:
259 270 from_path, from_name = from_name.rsplit('/', 1)
260 271 else:
261 272 from_path = path
262 273 model = self.get_model(from_name, from_path)
263 274 if model['type'] == 'directory':
264 275 raise HTTPError(400, "Can't copy directories")
265 276 if not to_name:
266 277 base, ext = os.path.splitext(from_name)
267 278 copy_name = u'{0}-Copy{1}'.format(base, ext)
268 279 to_name = self.increment_filename(copy_name, path)
269 280 model['name'] = to_name
270 281 model['path'] = path
271 282 model = self.save(model, to_name, path)
272 283 return model
273 284
274 285 def log_info(self):
275 286 self.log.info(self.info_string())
276 287
277 288 def trust_notebook(self, name, path=''):
278 289 """Explicitly trust a notebook
279 290
280 291 Parameters
281 292 ----------
282 293 name : string
283 294 The filename of the notebook
284 295 path : string
285 296 The notebook's directory
286 297 """
287 298 model = self.get_model(name, path)
288 299 nb = model['content']
289 300 self.log.warn("Trusting notebook %s/%s", path, name)
290 301 self.notary.mark_cells(nb, True)
291 302 self.save(model, name, path)
292 303
293 304 def check_and_sign(self, nb, name='', path=''):
294 305 """Check for trusted cells, and sign the notebook.
295 306
296 307 Called as a part of saving notebooks.
297 308
298 309 Parameters
299 310 ----------
300 311 nb : dict
301 312 The notebook object (in nbformat.current format)
302 313 name : string
303 314 The filename of the notebook (for logging)
304 315 path : string
305 316 The notebook's directory (for logging)
306 317 """
307 318 if self.notary.check_cells(nb):
308 319 self.notary.sign(nb)
309 320 else:
310 321 self.log.warn("Saving untrusted notebook %s/%s", path, name)
311 322
312 323 def mark_trusted_cells(self, nb, name='', path=''):
313 324 """Mark cells as trusted if the notebook signature matches.
314 325
315 326 Called as a part of loading notebooks.
316 327
317 328 Parameters
318 329 ----------
319 330 nb : dict
320 331 The notebook object (in nbformat.current format)
321 332 name : string
322 333 The filename of the notebook (for logging)
323 334 path : string
324 335 The notebook's directory (for logging)
325 336 """
326 337 trusted = self.notary.check_signature(nb)
327 338 if not trusted:
328 339 self.log.warn("Notebook %s/%s is not trusted", path, name)
329 340 self.notary.mark_cells(nb, trusted)
330 341
331 342 def should_list(self, name):
332 343 """Should this file/directory name be displayed in a listing?"""
333 344 return not any(fnmatch(name, glob) for glob in self.hide_globs)
@@ -1,2658 +1,2734 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'base/js/dialog',
9 9 'notebook/js/textcell',
10 10 'notebook/js/codecell',
11 11 'services/sessions/js/session',
12 12 'notebook/js/celltoolbar',
13 13 'components/marked/lib/marked',
14 14 'highlight',
15 15 'notebook/js/mathjaxutils',
16 16 'base/js/keyboard',
17 17 'notebook/js/tooltip',
18 18 'notebook/js/celltoolbarpresets/default',
19 19 'notebook/js/celltoolbarpresets/rawcell',
20 20 'notebook/js/celltoolbarpresets/slideshow',
21 21 'notebook/js/scrollmanager'
22 22 ], function (
23 23 IPython,
24 24 $,
25 25 utils,
26 26 dialog,
27 27 textcell,
28 28 codecell,
29 29 session,
30 30 celltoolbar,
31 31 marked,
32 32 hljs,
33 33 mathjaxutils,
34 34 keyboard,
35 35 tooltip,
36 36 default_celltoolbar,
37 37 rawcell_celltoolbar,
38 38 slideshow_celltoolbar,
39 39 scrollmanager
40 40 ) {
41 41
42 42 var Notebook = function (selector, options) {
43 43 // Constructor
44 44 //
45 45 // A notebook contains and manages cells.
46 46 //
47 47 // Parameters:
48 48 // selector: string
49 49 // options: dictionary
50 50 // Dictionary of keyword arguments.
51 51 // events: $(Events) instance
52 52 // keyboard_manager: KeyboardManager instance
53 53 // save_widget: SaveWidget instance
54 54 // config: dictionary
55 55 // base_url : string
56 56 // notebook_path : string
57 57 // notebook_name : string
58 58 this.config = utils.mergeopt(Notebook, options.config);
59 59 this.base_url = options.base_url;
60 60 this.notebook_path = options.notebook_path;
61 61 this.notebook_name = options.notebook_name;
62 62 this.events = options.events;
63 63 this.keyboard_manager = options.keyboard_manager;
64 64 this.save_widget = options.save_widget;
65 65 this.tooltip = new tooltip.Tooltip(this.events);
66 66 this.ws_url = options.ws_url;
67 67 this._session_starting = false;
68 68 this.default_cell_type = this.config.default_cell_type || 'code';
69 69
70 70 // Create default scroll manager.
71 71 this.scroll_manager = new scrollmanager.ScrollManager(this);
72 72
73 73 // TODO: This code smells (and the other `= this` line a couple lines down)
74 74 // We need a better way to deal with circular instance references.
75 75 this.keyboard_manager.notebook = this;
76 76 this.save_widget.notebook = this;
77 77
78 78 mathjaxutils.init();
79 79
80 80 if (marked) {
81 81 marked.setOptions({
82 82 gfm : true,
83 83 tables: true,
84 84 langPrefix: "language-",
85 85 highlight: function(code, lang) {
86 86 if (!lang) {
87 87 // no language, no highlight
88 88 return code;
89 89 }
90 90 var highlighted;
91 91 try {
92 92 highlighted = hljs.highlight(lang, code, false);
93 93 } catch(err) {
94 94 highlighted = hljs.highlightAuto(code);
95 95 }
96 96 return highlighted.value;
97 97 }
98 98 });
99 99 }
100 100
101 101 this.element = $(selector);
102 102 this.element.scroll();
103 103 this.element.data("notebook", this);
104 104 this.next_prompt_number = 1;
105 105 this.session = null;
106 106 this.kernel = null;
107 107 this.clipboard = null;
108 108 this.undelete_backup = null;
109 109 this.undelete_index = null;
110 110 this.undelete_below = false;
111 111 this.paste_enabled = false;
112 112 // It is important to start out in command mode to match the intial mode
113 113 // of the KeyboardManager.
114 114 this.mode = 'command';
115 115 this.set_dirty(false);
116 116 this.metadata = {};
117 117 this._checkpoint_after_save = false;
118 118 this.last_checkpoint = null;
119 119 this.checkpoints = [];
120 120 this.autosave_interval = 0;
121 121 this.autosave_timer = null;
122 122 // autosave *at most* every two minutes
123 123 this.minimum_autosave_interval = 120000;
124 124 // single worksheet for now
125 125 this.worksheet_metadata = {};
126 126 this.notebook_name_blacklist_re = /[\/\\:]/;
127 127 this.nbformat = 3; // Increment this when changing the nbformat
128 128 this.nbformat_minor = 0; // Increment this when changing the nbformat
129 129 this.codemirror_mode = 'ipython';
130 130 this.create_elements();
131 131 this.bind_events();
132 132 this.save_notebook = function() { // don't allow save until notebook_loaded
133 133 this.save_notebook_error(null, null, "Load failed, save is disabled");
134 134 };
135 135
136 136 // Trigger cell toolbar registration.
137 137 default_celltoolbar.register(this);
138 138 rawcell_celltoolbar.register(this);
139 139 slideshow_celltoolbar.register(this);
140 140 };
141 141
142 142 Notebook.options_default = {
143 143 // can be any cell type, or the special values of
144 144 // 'above', 'below', or 'selected' to get the value from another cell.
145 145 Notebook: {
146 146 default_cell_type: 'code',
147 147 }
148 148 };
149 149
150 150
151 151 /**
152 152 * Create an HTML and CSS representation of the notebook.
153 153 *
154 154 * @method create_elements
155 155 */
156 156 Notebook.prototype.create_elements = function () {
157 157 var that = this;
158 158 this.element.attr('tabindex','-1');
159 159 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
160 160 // We add this end_space div to the end of the notebook div to:
161 161 // i) provide a margin between the last cell and the end of the notebook
162 162 // ii) to prevent the div from scrolling up when the last cell is being
163 163 // edited, but is too low on the page, which browsers will do automatically.
164 164 var end_space = $('<div/>').addClass('end_space');
165 165 end_space.dblclick(function (e) {
166 166 var ncells = that.ncells();
167 167 that.insert_cell_below('code',ncells-1);
168 168 });
169 169 this.element.append(this.container);
170 170 this.container.append(end_space);
171 171 };
172 172
173 173 /**
174 174 * Bind JavaScript events: key presses and custom IPython events.
175 175 *
176 176 * @method bind_events
177 177 */
178 178 Notebook.prototype.bind_events = function () {
179 179 var that = this;
180 180
181 181 this.events.on('set_next_input.Notebook', function (event, data) {
182 182 var index = that.find_cell_index(data.cell);
183 183 var new_cell = that.insert_cell_below('code',index);
184 184 new_cell.set_text(data.text);
185 185 that.dirty = true;
186 186 });
187 187
188 188 this.events.on('set_dirty.Notebook', function (event, data) {
189 189 that.dirty = data.value;
190 190 });
191 191
192 192 this.events.on('trust_changed.Notebook', function (event, data) {
193 193 that.trusted = data.value;
194 194 });
195 195
196 196 this.events.on('select.Cell', function (event, data) {
197 197 var index = that.find_cell_index(data.cell);
198 198 that.select(index);
199 199 });
200 200
201 201 this.events.on('edit_mode.Cell', function (event, data) {
202 202 that.handle_edit_mode(data.cell);
203 203 });
204 204
205 205 this.events.on('command_mode.Cell', function (event, data) {
206 206 that.handle_command_mode(data.cell);
207 207 });
208 208
209 209 this.events.on('status_autorestarting.Kernel', function () {
210 210 dialog.modal({
211 211 notebook: that,
212 212 keyboard_manager: that.keyboard_manager,
213 213 title: "Kernel Restarting",
214 214 body: "The kernel appears to have died. It will restart automatically.",
215 215 buttons: {
216 216 OK : {
217 217 class : "btn-primary"
218 218 }
219 219 }
220 220 });
221 221 });
222 222
223 223 this.events.on('spec_changed.Kernel', function(event, data) {
224 224 that.set_kernelspec_metadata(data);
225 225 if (data.codemirror_mode) {
226 226 that.set_codemirror_mode(data.codemirror_mode);
227 227 }
228 228 });
229 229
230 230 var collapse_time = function (time) {
231 231 var app_height = $('#ipython-main-app').height(); // content height
232 232 var splitter_height = $('div#pager_splitter').outerHeight(true);
233 233 var new_height = app_height - splitter_height;
234 234 that.element.animate({height : new_height + 'px'}, time);
235 235 };
236 236
237 237 this.element.bind('collapse_pager', function (event, extrap) {
238 238 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
239 239 collapse_time(time);
240 240 });
241 241
242 242 var expand_time = function (time) {
243 243 var app_height = $('#ipython-main-app').height(); // content height
244 244 var splitter_height = $('div#pager_splitter').outerHeight(true);
245 245 var pager_height = $('div#pager').outerHeight(true);
246 246 var new_height = app_height - pager_height - splitter_height;
247 247 that.element.animate({height : new_height + 'px'}, time);
248 248 };
249 249
250 250 this.element.bind('expand_pager', function (event, extrap) {
251 251 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
252 252 expand_time(time);
253 253 });
254 254
255 255 // Firefox 22 broke $(window).on("beforeunload")
256 256 // I'm not sure why or how.
257 257 window.onbeforeunload = function (e) {
258 258 // TODO: Make killing the kernel configurable.
259 259 var kill_kernel = false;
260 260 if (kill_kernel) {
261 261 that.session.kill_kernel();
262 262 }
263 263 // if we are autosaving, trigger an autosave on nav-away.
264 264 // still warn, because if we don't the autosave may fail.
265 265 if (that.dirty) {
266 266 if ( that.autosave_interval ) {
267 267 // schedule autosave in a timeout
268 268 // this gives you a chance to forcefully discard changes
269 269 // by reloading the page if you *really* want to.
270 270 // the timer doesn't start until you *dismiss* the dialog.
271 271 setTimeout(function () {
272 272 if (that.dirty) {
273 273 that.save_notebook();
274 274 }
275 275 }, 1000);
276 276 return "Autosave in progress, latest changes may be lost.";
277 277 } else {
278 278 return "Unsaved changes will be lost.";
279 279 }
280 280 }
281 281 // Null is the *only* return value that will make the browser not
282 282 // pop up the "don't leave" dialog.
283 283 return null;
284 284 };
285 285 };
286 286
287 287 /**
288 288 * Set the dirty flag, and trigger the set_dirty.Notebook event
289 289 *
290 290 * @method set_dirty
291 291 */
292 292 Notebook.prototype.set_dirty = function (value) {
293 293 if (value === undefined) {
294 294 value = true;
295 295 }
296 296 if (this.dirty == value) {
297 297 return;
298 298 }
299 299 this.events.trigger('set_dirty.Notebook', {value: value});
300 300 };
301 301
302 302 /**
303 303 * Scroll the top of the page to a given cell.
304 304 *
305 305 * @method scroll_to_cell
306 306 * @param {Number} cell_number An index of the cell to view
307 307 * @param {Number} time Animation time in milliseconds
308 308 * @return {Number} Pixel offset from the top of the container
309 309 */
310 310 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
311 311 var cells = this.get_cells();
312 312 time = time || 0;
313 313 cell_number = Math.min(cells.length-1,cell_number);
314 314 cell_number = Math.max(0 ,cell_number);
315 315 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
316 316 this.element.animate({scrollTop:scroll_value}, time);
317 317 return scroll_value;
318 318 };
319 319
320 320 /**
321 321 * Scroll to the bottom of the page.
322 322 *
323 323 * @method scroll_to_bottom
324 324 */
325 325 Notebook.prototype.scroll_to_bottom = function () {
326 326 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
327 327 };
328 328
329 329 /**
330 330 * Scroll to the top of the page.
331 331 *
332 332 * @method scroll_to_top
333 333 */
334 334 Notebook.prototype.scroll_to_top = function () {
335 335 this.element.animate({scrollTop:0}, 0);
336 336 };
337 337
338 338 // Edit Notebook metadata
339 339
340 340 Notebook.prototype.edit_metadata = function () {
341 341 var that = this;
342 342 dialog.edit_metadata({
343 343 md: this.metadata,
344 344 callback: function (md) {
345 345 that.metadata = md;
346 346 },
347 347 name: 'Notebook',
348 348 notebook: this,
349 349 keyboard_manager: this.keyboard_manager});
350 350 };
351 351
352 352 Notebook.prototype.set_kernelspec_metadata = function(ks) {
353 353 var tostore = {};
354 354 $.map(ks, function(value, field) {
355 355 if (field !== 'argv' && field !== 'env') {
356 356 tostore[field] = value;
357 357 }
358 358 });
359 359 this.metadata.kernelspec = tostore;
360 360 }
361 361
362 362 // Cell indexing, retrieval, etc.
363 363
364 364 /**
365 365 * Get all cell elements in the notebook.
366 366 *
367 367 * @method get_cell_elements
368 368 * @return {jQuery} A selector of all cell elements
369 369 */
370 370 Notebook.prototype.get_cell_elements = function () {
371 371 return this.container.children("div.cell");
372 372 };
373 373
374 374 /**
375 375 * Get a particular cell element.
376 376 *
377 377 * @method get_cell_element
378 378 * @param {Number} index An index of a cell to select
379 379 * @return {jQuery} A selector of the given cell.
380 380 */
381 381 Notebook.prototype.get_cell_element = function (index) {
382 382 var result = null;
383 383 var e = this.get_cell_elements().eq(index);
384 384 if (e.length !== 0) {
385 385 result = e;
386 386 }
387 387 return result;
388 388 };
389 389
390 390 /**
391 391 * Try to get a particular cell by msg_id.
392 392 *
393 393 * @method get_msg_cell
394 394 * @param {String} msg_id A message UUID
395 395 * @return {Cell} Cell or null if no cell was found.
396 396 */
397 397 Notebook.prototype.get_msg_cell = function (msg_id) {
398 398 return codecell.CodeCell.msg_cells[msg_id] || null;
399 399 };
400 400
401 401 /**
402 402 * Count the cells in this notebook.
403 403 *
404 404 * @method ncells
405 405 * @return {Number} The number of cells in this notebook
406 406 */
407 407 Notebook.prototype.ncells = function () {
408 408 return this.get_cell_elements().length;
409 409 };
410 410
411 411 /**
412 412 * Get all Cell objects in this notebook.
413 413 *
414 414 * @method get_cells
415 415 * @return {Array} This notebook's Cell objects
416 416 */
417 417 // TODO: we are often calling cells as cells()[i], which we should optimize
418 418 // to cells(i) or a new method.
419 419 Notebook.prototype.get_cells = function () {
420 420 return this.get_cell_elements().toArray().map(function (e) {
421 421 return $(e).data("cell");
422 422 });
423 423 };
424 424
425 425 /**
426 426 * Get a Cell object from this notebook.
427 427 *
428 428 * @method get_cell
429 429 * @param {Number} index An index of a cell to retrieve
430 430 * @return {Cell} Cell or null if no cell was found.
431 431 */
432 432 Notebook.prototype.get_cell = function (index) {
433 433 var result = null;
434 434 var ce = this.get_cell_element(index);
435 435 if (ce !== null) {
436 436 result = ce.data('cell');
437 437 }
438 438 return result;
439 439 };
440 440
441 441 /**
442 442 * Get the cell below a given cell.
443 443 *
444 444 * @method get_next_cell
445 445 * @param {Cell} cell The provided cell
446 446 * @return {Cell} the next cell or null if no cell was found.
447 447 */
448 448 Notebook.prototype.get_next_cell = function (cell) {
449 449 var result = null;
450 450 var index = this.find_cell_index(cell);
451 451 if (this.is_valid_cell_index(index+1)) {
452 452 result = this.get_cell(index+1);
453 453 }
454 454 return result;
455 455 };
456 456
457 457 /**
458 458 * Get the cell above a given cell.
459 459 *
460 460 * @method get_prev_cell
461 461 * @param {Cell} cell The provided cell
462 462 * @return {Cell} The previous cell or null if no cell was found.
463 463 */
464 464 Notebook.prototype.get_prev_cell = function (cell) {
465 465 var result = null;
466 466 var index = this.find_cell_index(cell);
467 467 if (index !== null && index > 0) {
468 468 result = this.get_cell(index-1);
469 469 }
470 470 return result;
471 471 };
472 472
473 473 /**
474 474 * Get the numeric index of a given cell.
475 475 *
476 476 * @method find_cell_index
477 477 * @param {Cell} cell The provided cell
478 478 * @return {Number} The cell's numeric index or null if no cell was found.
479 479 */
480 480 Notebook.prototype.find_cell_index = function (cell) {
481 481 var result = null;
482 482 this.get_cell_elements().filter(function (index) {
483 483 if ($(this).data("cell") === cell) {
484 484 result = index;
485 485 }
486 486 });
487 487 return result;
488 488 };
489 489
490 490 /**
491 491 * Get a given index , or the selected index if none is provided.
492 492 *
493 493 * @method index_or_selected
494 494 * @param {Number} index A cell's index
495 495 * @return {Number} The given index, or selected index if none is provided.
496 496 */
497 497 Notebook.prototype.index_or_selected = function (index) {
498 498 var i;
499 499 if (index === undefined || index === null) {
500 500 i = this.get_selected_index();
501 501 if (i === null) {
502 502 i = 0;
503 503 }
504 504 } else {
505 505 i = index;
506 506 }
507 507 return i;
508 508 };
509 509
510 510 /**
511 511 * Get the currently selected cell.
512 512 * @method get_selected_cell
513 513 * @return {Cell} The selected cell
514 514 */
515 515 Notebook.prototype.get_selected_cell = function () {
516 516 var index = this.get_selected_index();
517 517 return this.get_cell(index);
518 518 };
519 519
520 520 /**
521 521 * Check whether a cell index is valid.
522 522 *
523 523 * @method is_valid_cell_index
524 524 * @param {Number} index A cell index
525 525 * @return True if the index is valid, false otherwise
526 526 */
527 527 Notebook.prototype.is_valid_cell_index = function (index) {
528 528 if (index !== null && index >= 0 && index < this.ncells()) {
529 529 return true;
530 530 } else {
531 531 return false;
532 532 }
533 533 };
534 534
535 535 /**
536 536 * Get the index of the currently selected cell.
537 537
538 538 * @method get_selected_index
539 539 * @return {Number} The selected cell's numeric index
540 540 */
541 541 Notebook.prototype.get_selected_index = function () {
542 542 var result = null;
543 543 this.get_cell_elements().filter(function (index) {
544 544 if ($(this).data("cell").selected === true) {
545 545 result = index;
546 546 }
547 547 });
548 548 return result;
549 549 };
550 550
551 551
552 552 // Cell selection.
553 553
554 554 /**
555 555 * Programmatically select a cell.
556 556 *
557 557 * @method select
558 558 * @param {Number} index A cell's index
559 559 * @return {Notebook} This notebook
560 560 */
561 561 Notebook.prototype.select = function (index) {
562 562 if (this.is_valid_cell_index(index)) {
563 563 var sindex = this.get_selected_index();
564 564 if (sindex !== null && index !== sindex) {
565 565 // If we are about to select a different cell, make sure we are
566 566 // first in command mode.
567 567 if (this.mode !== 'command') {
568 568 this.command_mode();
569 569 }
570 570 this.get_cell(sindex).unselect();
571 571 }
572 572 var cell = this.get_cell(index);
573 573 cell.select();
574 574 if (cell.cell_type === 'heading') {
575 575 this.events.trigger('selected_cell_type_changed.Notebook',
576 576 {'cell_type':cell.cell_type,level:cell.level}
577 577 );
578 578 } else {
579 579 this.events.trigger('selected_cell_type_changed.Notebook',
580 580 {'cell_type':cell.cell_type}
581 581 );
582 582 }
583 583 }
584 584 return this;
585 585 };
586 586
587 587 /**
588 588 * Programmatically select the next cell.
589 589 *
590 590 * @method select_next
591 591 * @return {Notebook} This notebook
592 592 */
593 593 Notebook.prototype.select_next = function () {
594 594 var index = this.get_selected_index();
595 595 this.select(index+1);
596 596 return this;
597 597 };
598 598
599 599 /**
600 600 * Programmatically select the previous cell.
601 601 *
602 602 * @method select_prev
603 603 * @return {Notebook} This notebook
604 604 */
605 605 Notebook.prototype.select_prev = function () {
606 606 var index = this.get_selected_index();
607 607 this.select(index-1);
608 608 return this;
609 609 };
610 610
611 611
612 612 // Edit/Command mode
613 613
614 614 /**
615 615 * Gets the index of the cell that is in edit mode.
616 616 *
617 617 * @method get_edit_index
618 618 *
619 619 * @return index {int}
620 620 **/
621 621 Notebook.prototype.get_edit_index = function () {
622 622 var result = null;
623 623 this.get_cell_elements().filter(function (index) {
624 624 if ($(this).data("cell").mode === 'edit') {
625 625 result = index;
626 626 }
627 627 });
628 628 return result;
629 629 };
630 630
631 631 /**
632 632 * Handle when a a cell blurs and the notebook should enter command mode.
633 633 *
634 634 * @method handle_command_mode
635 635 * @param [cell] {Cell} Cell to enter command mode on.
636 636 **/
637 637 Notebook.prototype.handle_command_mode = function (cell) {
638 638 if (this.mode !== 'command') {
639 639 cell.command_mode();
640 640 this.mode = 'command';
641 641 this.events.trigger('command_mode.Notebook');
642 642 this.keyboard_manager.command_mode();
643 643 }
644 644 };
645 645
646 646 /**
647 647 * Make the notebook enter command mode.
648 648 *
649 649 * @method command_mode
650 650 **/
651 651 Notebook.prototype.command_mode = function () {
652 652 var cell = this.get_cell(this.get_edit_index());
653 653 if (cell && this.mode !== 'command') {
654 654 // We don't call cell.command_mode, but rather call cell.focus_cell()
655 655 // which will blur and CM editor and trigger the call to
656 656 // handle_command_mode.
657 657 cell.focus_cell();
658 658 }
659 659 };
660 660
661 661 /**
662 662 * Handle when a cell fires it's edit_mode event.
663 663 *
664 664 * @method handle_edit_mode
665 665 * @param [cell] {Cell} Cell to enter edit mode on.
666 666 **/
667 667 Notebook.prototype.handle_edit_mode = function (cell) {
668 668 if (cell && this.mode !== 'edit') {
669 669 cell.edit_mode();
670 670 this.mode = 'edit';
671 671 this.events.trigger('edit_mode.Notebook');
672 672 this.keyboard_manager.edit_mode();
673 673 }
674 674 };
675 675
676 676 /**
677 677 * Make a cell enter edit mode.
678 678 *
679 679 * @method edit_mode
680 680 **/
681 681 Notebook.prototype.edit_mode = function () {
682 682 var cell = this.get_selected_cell();
683 683 if (cell && this.mode !== 'edit') {
684 684 cell.unrender();
685 685 cell.focus_editor();
686 686 }
687 687 };
688 688
689 689 /**
690 690 * Focus the currently selected cell.
691 691 *
692 692 * @method focus_cell
693 693 **/
694 694 Notebook.prototype.focus_cell = function () {
695 695 var cell = this.get_selected_cell();
696 696 if (cell === null) {return;} // No cell is selected
697 697 cell.focus_cell();
698 698 };
699 699
700 700 // Cell movement
701 701
702 702 /**
703 703 * Move given (or selected) cell up and select it.
704 704 *
705 705 * @method move_cell_up
706 706 * @param [index] {integer} cell index
707 707 * @return {Notebook} This notebook
708 708 **/
709 709 Notebook.prototype.move_cell_up = function (index) {
710 710 var i = this.index_or_selected(index);
711 711 if (this.is_valid_cell_index(i) && i > 0) {
712 712 var pivot = this.get_cell_element(i-1);
713 713 var tomove = this.get_cell_element(i);
714 714 if (pivot !== null && tomove !== null) {
715 715 tomove.detach();
716 716 pivot.before(tomove);
717 717 this.select(i-1);
718 718 var cell = this.get_selected_cell();
719 719 cell.focus_cell();
720 720 }
721 721 this.set_dirty(true);
722 722 }
723 723 return this;
724 724 };
725 725
726 726
727 727 /**
728 728 * Move given (or selected) cell down and select it
729 729 *
730 730 * @method move_cell_down
731 731 * @param [index] {integer} cell index
732 732 * @return {Notebook} This notebook
733 733 **/
734 734 Notebook.prototype.move_cell_down = function (index) {
735 735 var i = this.index_or_selected(index);
736 736 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
737 737 var pivot = this.get_cell_element(i+1);
738 738 var tomove = this.get_cell_element(i);
739 739 if (pivot !== null && tomove !== null) {
740 740 tomove.detach();
741 741 pivot.after(tomove);
742 742 this.select(i+1);
743 743 var cell = this.get_selected_cell();
744 744 cell.focus_cell();
745 745 }
746 746 }
747 747 this.set_dirty();
748 748 return this;
749 749 };
750 750
751 751
752 752 // Insertion, deletion.
753 753
754 754 /**
755 755 * Delete a cell from the notebook.
756 756 *
757 757 * @method delete_cell
758 758 * @param [index] A cell's numeric index
759 759 * @return {Notebook} This notebook
760 760 */
761 761 Notebook.prototype.delete_cell = function (index) {
762 762 var i = this.index_or_selected(index);
763 763 var cell = this.get_cell(i);
764 764 if (!cell.is_deletable()) {
765 765 return this;
766 766 }
767 767
768 768 this.undelete_backup = cell.toJSON();
769 769 $('#undelete_cell').removeClass('disabled');
770 770 if (this.is_valid_cell_index(i)) {
771 771 var old_ncells = this.ncells();
772 772 var ce = this.get_cell_element(i);
773 773 ce.remove();
774 774 if (i === 0) {
775 775 // Always make sure we have at least one cell.
776 776 if (old_ncells === 1) {
777 777 this.insert_cell_below('code');
778 778 }
779 779 this.select(0);
780 780 this.undelete_index = 0;
781 781 this.undelete_below = false;
782 782 } else if (i === old_ncells-1 && i !== 0) {
783 783 this.select(i-1);
784 784 this.undelete_index = i - 1;
785 785 this.undelete_below = true;
786 786 } else {
787 787 this.select(i);
788 788 this.undelete_index = i;
789 789 this.undelete_below = false;
790 790 }
791 791 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
792 792 this.set_dirty(true);
793 793 }
794 794 return this;
795 795 };
796 796
797 797 /**
798 798 * Restore the most recently deleted cell.
799 799 *
800 800 * @method undelete
801 801 */
802 802 Notebook.prototype.undelete_cell = function() {
803 803 if (this.undelete_backup !== null && this.undelete_index !== null) {
804 804 var current_index = this.get_selected_index();
805 805 if (this.undelete_index < current_index) {
806 806 current_index = current_index + 1;
807 807 }
808 808 if (this.undelete_index >= this.ncells()) {
809 809 this.select(this.ncells() - 1);
810 810 }
811 811 else {
812 812 this.select(this.undelete_index);
813 813 }
814 814 var cell_data = this.undelete_backup;
815 815 var new_cell = null;
816 816 if (this.undelete_below) {
817 817 new_cell = this.insert_cell_below(cell_data.cell_type);
818 818 } else {
819 819 new_cell = this.insert_cell_above(cell_data.cell_type);
820 820 }
821 821 new_cell.fromJSON(cell_data);
822 822 if (this.undelete_below) {
823 823 this.select(current_index+1);
824 824 } else {
825 825 this.select(current_index);
826 826 }
827 827 this.undelete_backup = null;
828 828 this.undelete_index = null;
829 829 }
830 830 $('#undelete_cell').addClass('disabled');
831 831 };
832 832
833 833 /**
834 834 * Insert a cell so that after insertion the cell is at given index.
835 835 *
836 836 * If cell type is not provided, it will default to the type of the
837 837 * currently active cell.
838 838 *
839 839 * Similar to insert_above, but index parameter is mandatory
840 840 *
841 841 * Index will be brought back into the accessible range [0,n]
842 842 *
843 843 * @method insert_cell_at_index
844 844 * @param [type] {string} in ['code','markdown','heading'], defaults to 'code'
845 845 * @param [index] {int} a valid index where to insert cell
846 846 *
847 847 * @return cell {cell|null} created cell or null
848 848 **/
849 849 Notebook.prototype.insert_cell_at_index = function(type, index){
850 850
851 851 var ncells = this.ncells();
852 852 index = Math.min(index, ncells);
853 853 index = Math.max(index, 0);
854 854 var cell = null;
855 855 type = type || this.default_cell_type;
856 856 if (type === 'above') {
857 857 if (index > 0) {
858 858 type = this.get_cell(index-1).cell_type;
859 859 } else {
860 860 type = 'code';
861 861 }
862 862 } else if (type === 'below') {
863 863 if (index < ncells) {
864 864 type = this.get_cell(index).cell_type;
865 865 } else {
866 866 type = 'code';
867 867 }
868 868 } else if (type === 'selected') {
869 869 type = this.get_selected_cell().cell_type;
870 870 }
871 871
872 872 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
873 873 var cell_options = {
874 874 events: this.events,
875 875 config: this.config,
876 876 keyboard_manager: this.keyboard_manager,
877 877 notebook: this,
878 878 tooltip: this.tooltip,
879 879 };
880 880 if (type === 'code') {
881 881 cell = new codecell.CodeCell(this.kernel, cell_options);
882 882 cell.set_input_prompt();
883 883 } else if (type === 'markdown') {
884 884 cell = new textcell.MarkdownCell(cell_options);
885 885 } else if (type === 'raw') {
886 886 cell = new textcell.RawCell(cell_options);
887 887 } else if (type === 'heading') {
888 888 cell = new textcell.HeadingCell(cell_options);
889 889 }
890 890
891 891 if(this._insert_element_at_index(cell.element,index)) {
892 892 cell.render();
893 893 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
894 894 cell.refresh();
895 895 // We used to select the cell after we refresh it, but there
896 896 // are now cases were this method is called where select is
897 897 // not appropriate. The selection logic should be handled by the
898 898 // caller of the the top level insert_cell methods.
899 899 this.set_dirty(true);
900 900 }
901 901 }
902 902 return cell;
903 903
904 904 };
905 905
906 906 /**
907 907 * Insert an element at given cell index.
908 908 *
909 909 * @method _insert_element_at_index
910 910 * @param element {dom element} a cell element
911 911 * @param [index] {int} a valid index where to inser cell
912 912 * @private
913 913 *
914 914 * return true if everything whent fine.
915 915 **/
916 916 Notebook.prototype._insert_element_at_index = function(element, index){
917 917 if (element === undefined){
918 918 return false;
919 919 }
920 920
921 921 var ncells = this.ncells();
922 922
923 923 if (ncells === 0) {
924 924 // special case append if empty
925 925 this.element.find('div.end_space').before(element);
926 926 } else if ( ncells === index ) {
927 927 // special case append it the end, but not empty
928 928 this.get_cell_element(index-1).after(element);
929 929 } else if (this.is_valid_cell_index(index)) {
930 930 // otherwise always somewhere to append to
931 931 this.get_cell_element(index).before(element);
932 932 } else {
933 933 return false;
934 934 }
935 935
936 936 if (this.undelete_index !== null && index <= this.undelete_index) {
937 937 this.undelete_index = this.undelete_index + 1;
938 938 this.set_dirty(true);
939 939 }
940 940 return true;
941 941 };
942 942
943 943 /**
944 944 * Insert a cell of given type above given index, or at top
945 945 * of notebook if index smaller than 0.
946 946 *
947 947 * default index value is the one of currently selected cell
948 948 *
949 949 * @method insert_cell_above
950 950 * @param [type] {string} cell type
951 951 * @param [index] {integer}
952 952 *
953 953 * @return handle to created cell or null
954 954 **/
955 955 Notebook.prototype.insert_cell_above = function (type, index) {
956 956 index = this.index_or_selected(index);
957 957 return this.insert_cell_at_index(type, index);
958 958 };
959 959
960 960 /**
961 961 * Insert a cell of given type below given index, or at bottom
962 962 * of notebook if index greater than number of cells
963 963 *
964 964 * default index value is the one of currently selected cell
965 965 *
966 966 * @method insert_cell_below
967 967 * @param [type] {string} cell type
968 968 * @param [index] {integer}
969 969 *
970 970 * @return handle to created cell or null
971 971 *
972 972 **/
973 973 Notebook.prototype.insert_cell_below = function (type, index) {
974 974 index = this.index_or_selected(index);
975 975 return this.insert_cell_at_index(type, index+1);
976 976 };
977 977
978 978
979 979 /**
980 980 * Insert cell at end of notebook
981 981 *
982 982 * @method insert_cell_at_bottom
983 983 * @param {String} type cell type
984 984 *
985 985 * @return the added cell; or null
986 986 **/
987 987 Notebook.prototype.insert_cell_at_bottom = function (type){
988 988 var len = this.ncells();
989 989 return this.insert_cell_below(type,len-1);
990 990 };
991 991
992 992 /**
993 993 * Turn a cell into a code cell.
994 994 *
995 995 * @method to_code
996 996 * @param {Number} [index] A cell's index
997 997 */
998 998 Notebook.prototype.to_code = function (index) {
999 999 var i = this.index_or_selected(index);
1000 1000 if (this.is_valid_cell_index(i)) {
1001 1001 var source_cell = this.get_cell(i);
1002 1002 if (!(source_cell instanceof codecell.CodeCell)) {
1003 1003 var target_cell = this.insert_cell_below('code',i);
1004 1004 var text = source_cell.get_text();
1005 1005 if (text === source_cell.placeholder) {
1006 1006 text = '';
1007 1007 }
1008 1008 //metadata
1009 1009 target_cell.metadata = source_cell.metadata;
1010 1010
1011 1011 target_cell.set_text(text);
1012 1012 // make this value the starting point, so that we can only undo
1013 1013 // to this state, instead of a blank cell
1014 1014 target_cell.code_mirror.clearHistory();
1015 1015 source_cell.element.remove();
1016 1016 this.select(i);
1017 1017 var cursor = source_cell.code_mirror.getCursor();
1018 1018 target_cell.code_mirror.setCursor(cursor);
1019 1019 this.set_dirty(true);
1020 1020 }
1021 1021 }
1022 1022 };
1023 1023
1024 1024 /**
1025 1025 * Turn a cell into a Markdown cell.
1026 1026 *
1027 1027 * @method to_markdown
1028 1028 * @param {Number} [index] A cell's index
1029 1029 */
1030 1030 Notebook.prototype.to_markdown = function (index) {
1031 1031 var i = this.index_or_selected(index);
1032 1032 if (this.is_valid_cell_index(i)) {
1033 1033 var source_cell = this.get_cell(i);
1034 1034
1035 1035 if (!(source_cell instanceof textcell.MarkdownCell)) {
1036 1036 var target_cell = this.insert_cell_below('markdown',i);
1037 1037 var text = source_cell.get_text();
1038 1038
1039 1039 if (text === source_cell.placeholder) {
1040 1040 text = '';
1041 1041 }
1042 1042 // metadata
1043 1043 target_cell.metadata = source_cell.metadata
1044 1044 // We must show the editor before setting its contents
1045 1045 target_cell.unrender();
1046 1046 target_cell.set_text(text);
1047 1047 // make this value the starting point, so that we can only undo
1048 1048 // to this state, instead of a blank cell
1049 1049 target_cell.code_mirror.clearHistory();
1050 1050 source_cell.element.remove();
1051 1051 this.select(i);
1052 1052 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1053 1053 target_cell.render();
1054 1054 }
1055 1055 var cursor = source_cell.code_mirror.getCursor();
1056 1056 target_cell.code_mirror.setCursor(cursor);
1057 1057 this.set_dirty(true);
1058 1058 }
1059 1059 }
1060 1060 };
1061 1061
1062 1062 /**
1063 1063 * Turn a cell into a raw text cell.
1064 1064 *
1065 1065 * @method to_raw
1066 1066 * @param {Number} [index] A cell's index
1067 1067 */
1068 1068 Notebook.prototype.to_raw = function (index) {
1069 1069 var i = this.index_or_selected(index);
1070 1070 if (this.is_valid_cell_index(i)) {
1071 1071 var target_cell = null;
1072 1072 var source_cell = this.get_cell(i);
1073 1073
1074 1074 if (!(source_cell instanceof textcell.RawCell)) {
1075 1075 target_cell = this.insert_cell_below('raw',i);
1076 1076 var text = source_cell.get_text();
1077 1077 if (text === source_cell.placeholder) {
1078 1078 text = '';
1079 1079 }
1080 1080 //metadata
1081 1081 target_cell.metadata = source_cell.metadata;
1082 1082 // We must show the editor before setting its contents
1083 1083 target_cell.unrender();
1084 1084 target_cell.set_text(text);
1085 1085 // make this value the starting point, so that we can only undo
1086 1086 // to this state, instead of a blank cell
1087 1087 target_cell.code_mirror.clearHistory();
1088 1088 source_cell.element.remove();
1089 1089 this.select(i);
1090 1090 var cursor = source_cell.code_mirror.getCursor();
1091 1091 target_cell.code_mirror.setCursor(cursor);
1092 1092 this.set_dirty(true);
1093 1093 }
1094 1094 }
1095 1095 };
1096 1096
1097 1097 /**
1098 1098 * Turn a cell into a heading cell.
1099 1099 *
1100 1100 * @method to_heading
1101 1101 * @param {Number} [index] A cell's index
1102 1102 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
1103 1103 */
1104 1104 Notebook.prototype.to_heading = function (index, level) {
1105 1105 level = level || 1;
1106 1106 var i = this.index_or_selected(index);
1107 1107 if (this.is_valid_cell_index(i)) {
1108 1108 var source_cell = this.get_cell(i);
1109 1109 var target_cell = null;
1110 1110 if (source_cell instanceof textcell.HeadingCell) {
1111 1111 source_cell.set_level(level);
1112 1112 } else {
1113 1113 target_cell = this.insert_cell_below('heading',i);
1114 1114 var text = source_cell.get_text();
1115 1115 if (text === source_cell.placeholder) {
1116 1116 text = '';
1117 1117 }
1118 1118 //metadata
1119 1119 target_cell.metadata = source_cell.metadata;
1120 1120 // We must show the editor before setting its contents
1121 1121 target_cell.set_level(level);
1122 1122 target_cell.unrender();
1123 1123 target_cell.set_text(text);
1124 1124 // make this value the starting point, so that we can only undo
1125 1125 // to this state, instead of a blank cell
1126 1126 target_cell.code_mirror.clearHistory();
1127 1127 source_cell.element.remove();
1128 1128 this.select(i);
1129 1129 var cursor = source_cell.code_mirror.getCursor();
1130 1130 target_cell.code_mirror.setCursor(cursor);
1131 1131 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1132 1132 target_cell.render();
1133 1133 }
1134 1134 }
1135 1135 this.set_dirty(true);
1136 1136 this.events.trigger('selected_cell_type_changed.Notebook',
1137 1137 {'cell_type':'heading',level:level}
1138 1138 );
1139 1139 }
1140 1140 };
1141 1141
1142 1142
1143 1143 // Cut/Copy/Paste
1144 1144
1145 1145 /**
1146 1146 * Enable UI elements for pasting cells.
1147 1147 *
1148 1148 * @method enable_paste
1149 1149 */
1150 1150 Notebook.prototype.enable_paste = function () {
1151 1151 var that = this;
1152 1152 if (!this.paste_enabled) {
1153 1153 $('#paste_cell_replace').removeClass('disabled')
1154 1154 .on('click', function () {that.paste_cell_replace();});
1155 1155 $('#paste_cell_above').removeClass('disabled')
1156 1156 .on('click', function () {that.paste_cell_above();});
1157 1157 $('#paste_cell_below').removeClass('disabled')
1158 1158 .on('click', function () {that.paste_cell_below();});
1159 1159 this.paste_enabled = true;
1160 1160 }
1161 1161 };
1162 1162
1163 1163 /**
1164 1164 * Disable UI elements for pasting cells.
1165 1165 *
1166 1166 * @method disable_paste
1167 1167 */
1168 1168 Notebook.prototype.disable_paste = function () {
1169 1169 if (this.paste_enabled) {
1170 1170 $('#paste_cell_replace').addClass('disabled').off('click');
1171 1171 $('#paste_cell_above').addClass('disabled').off('click');
1172 1172 $('#paste_cell_below').addClass('disabled').off('click');
1173 1173 this.paste_enabled = false;
1174 1174 }
1175 1175 };
1176 1176
1177 1177 /**
1178 1178 * Cut a cell.
1179 1179 *
1180 1180 * @method cut_cell
1181 1181 */
1182 1182 Notebook.prototype.cut_cell = function () {
1183 1183 this.copy_cell();
1184 1184 this.delete_cell();
1185 1185 };
1186 1186
1187 1187 /**
1188 1188 * Copy a cell.
1189 1189 *
1190 1190 * @method copy_cell
1191 1191 */
1192 1192 Notebook.prototype.copy_cell = function () {
1193 1193 var cell = this.get_selected_cell();
1194 1194 this.clipboard = cell.toJSON();
1195 1195 // remove undeletable status from the copied cell
1196 1196 if (this.clipboard.metadata.deletable !== undefined) {
1197 1197 delete this.clipboard.metadata.deletable;
1198 1198 }
1199 1199 this.enable_paste();
1200 1200 };
1201 1201
1202 1202 /**
1203 1203 * Replace the selected cell with a cell in the clipboard.
1204 1204 *
1205 1205 * @method paste_cell_replace
1206 1206 */
1207 1207 Notebook.prototype.paste_cell_replace = function () {
1208 1208 if (this.clipboard !== null && this.paste_enabled) {
1209 1209 var cell_data = this.clipboard;
1210 1210 var new_cell = this.insert_cell_above(cell_data.cell_type);
1211 1211 new_cell.fromJSON(cell_data);
1212 1212 var old_cell = this.get_next_cell(new_cell);
1213 1213 this.delete_cell(this.find_cell_index(old_cell));
1214 1214 this.select(this.find_cell_index(new_cell));
1215 1215 }
1216 1216 };
1217 1217
1218 1218 /**
1219 1219 * Paste a cell from the clipboard above the selected cell.
1220 1220 *
1221 1221 * @method paste_cell_above
1222 1222 */
1223 1223 Notebook.prototype.paste_cell_above = function () {
1224 1224 if (this.clipboard !== null && this.paste_enabled) {
1225 1225 var cell_data = this.clipboard;
1226 1226 var new_cell = this.insert_cell_above(cell_data.cell_type);
1227 1227 new_cell.fromJSON(cell_data);
1228 1228 new_cell.focus_cell();
1229 1229 }
1230 1230 };
1231 1231
1232 1232 /**
1233 1233 * Paste a cell from the clipboard below the selected cell.
1234 1234 *
1235 1235 * @method paste_cell_below
1236 1236 */
1237 1237 Notebook.prototype.paste_cell_below = function () {
1238 1238 if (this.clipboard !== null && this.paste_enabled) {
1239 1239 var cell_data = this.clipboard;
1240 1240 var new_cell = this.insert_cell_below(cell_data.cell_type);
1241 1241 new_cell.fromJSON(cell_data);
1242 1242 new_cell.focus_cell();
1243 1243 }
1244 1244 };
1245 1245
1246 1246 // Split/merge
1247 1247
1248 1248 /**
1249 1249 * Split the selected cell into two, at the cursor.
1250 1250 *
1251 1251 * @method split_cell
1252 1252 */
1253 1253 Notebook.prototype.split_cell = function () {
1254 1254 var mdc = textcell.MarkdownCell;
1255 1255 var rc = textcell.RawCell;
1256 1256 var cell = this.get_selected_cell();
1257 1257 if (cell.is_splittable()) {
1258 1258 var texta = cell.get_pre_cursor();
1259 1259 var textb = cell.get_post_cursor();
1260 1260 cell.set_text(textb);
1261 1261 var new_cell = this.insert_cell_above(cell.cell_type);
1262 1262 // Unrender the new cell so we can call set_text.
1263 1263 new_cell.unrender();
1264 1264 new_cell.set_text(texta);
1265 1265 }
1266 1266 };
1267 1267
1268 1268 /**
1269 1269 * Combine the selected cell into the cell above it.
1270 1270 *
1271 1271 * @method merge_cell_above
1272 1272 */
1273 1273 Notebook.prototype.merge_cell_above = function () {
1274 1274 var mdc = textcell.MarkdownCell;
1275 1275 var rc = textcell.RawCell;
1276 1276 var index = this.get_selected_index();
1277 1277 var cell = this.get_cell(index);
1278 1278 var render = cell.rendered;
1279 1279 if (!cell.is_mergeable()) {
1280 1280 return;
1281 1281 }
1282 1282 if (index > 0) {
1283 1283 var upper_cell = this.get_cell(index-1);
1284 1284 if (!upper_cell.is_mergeable()) {
1285 1285 return;
1286 1286 }
1287 1287 var upper_text = upper_cell.get_text();
1288 1288 var text = cell.get_text();
1289 1289 if (cell instanceof codecell.CodeCell) {
1290 1290 cell.set_text(upper_text+'\n'+text);
1291 1291 } else {
1292 1292 cell.unrender(); // Must unrender before we set_text.
1293 1293 cell.set_text(upper_text+'\n\n'+text);
1294 1294 if (render) {
1295 1295 // The rendered state of the final cell should match
1296 1296 // that of the original selected cell;
1297 1297 cell.render();
1298 1298 }
1299 1299 }
1300 1300 this.delete_cell(index-1);
1301 1301 this.select(this.find_cell_index(cell));
1302 1302 }
1303 1303 };
1304 1304
1305 1305 /**
1306 1306 * Combine the selected cell into the cell below it.
1307 1307 *
1308 1308 * @method merge_cell_below
1309 1309 */
1310 1310 Notebook.prototype.merge_cell_below = function () {
1311 1311 var mdc = textcell.MarkdownCell;
1312 1312 var rc = textcell.RawCell;
1313 1313 var index = this.get_selected_index();
1314 1314 var cell = this.get_cell(index);
1315 1315 var render = cell.rendered;
1316 1316 if (!cell.is_mergeable()) {
1317 1317 return;
1318 1318 }
1319 1319 if (index < this.ncells()-1) {
1320 1320 var lower_cell = this.get_cell(index+1);
1321 1321 if (!lower_cell.is_mergeable()) {
1322 1322 return;
1323 1323 }
1324 1324 var lower_text = lower_cell.get_text();
1325 1325 var text = cell.get_text();
1326 1326 if (cell instanceof codecell.CodeCell) {
1327 1327 cell.set_text(text+'\n'+lower_text);
1328 1328 } else {
1329 1329 cell.unrender(); // Must unrender before we set_text.
1330 1330 cell.set_text(text+'\n\n'+lower_text);
1331 1331 if (render) {
1332 1332 // The rendered state of the final cell should match
1333 1333 // that of the original selected cell;
1334 1334 cell.render();
1335 1335 }
1336 1336 }
1337 1337 this.delete_cell(index+1);
1338 1338 this.select(this.find_cell_index(cell));
1339 1339 }
1340 1340 };
1341 1341
1342 1342
1343 1343 // Cell collapsing and output clearing
1344 1344
1345 1345 /**
1346 1346 * Hide a cell's output.
1347 1347 *
1348 1348 * @method collapse_output
1349 1349 * @param {Number} index A cell's numeric index
1350 1350 */
1351 1351 Notebook.prototype.collapse_output = function (index) {
1352 1352 var i = this.index_or_selected(index);
1353 1353 var cell = this.get_cell(i);
1354 1354 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1355 1355 cell.collapse_output();
1356 1356 this.set_dirty(true);
1357 1357 }
1358 1358 };
1359 1359
1360 1360 /**
1361 1361 * Hide each code cell's output area.
1362 1362 *
1363 1363 * @method collapse_all_output
1364 1364 */
1365 1365 Notebook.prototype.collapse_all_output = function () {
1366 1366 $.map(this.get_cells(), function (cell, i) {
1367 1367 if (cell instanceof codecell.CodeCell) {
1368 1368 cell.collapse_output();
1369 1369 }
1370 1370 });
1371 1371 // this should not be set if the `collapse` key is removed from nbformat
1372 1372 this.set_dirty(true);
1373 1373 };
1374 1374
1375 1375 /**
1376 1376 * Show a cell's output.
1377 1377 *
1378 1378 * @method expand_output
1379 1379 * @param {Number} index A cell's numeric index
1380 1380 */
1381 1381 Notebook.prototype.expand_output = function (index) {
1382 1382 var i = this.index_or_selected(index);
1383 1383 var cell = this.get_cell(i);
1384 1384 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1385 1385 cell.expand_output();
1386 1386 this.set_dirty(true);
1387 1387 }
1388 1388 };
1389 1389
1390 1390 /**
1391 1391 * Expand each code cell's output area, and remove scrollbars.
1392 1392 *
1393 1393 * @method expand_all_output
1394 1394 */
1395 1395 Notebook.prototype.expand_all_output = function () {
1396 1396 $.map(this.get_cells(), function (cell, i) {
1397 1397 if (cell instanceof codecell.CodeCell) {
1398 1398 cell.expand_output();
1399 1399 }
1400 1400 });
1401 1401 // this should not be set if the `collapse` key is removed from nbformat
1402 1402 this.set_dirty(true);
1403 1403 };
1404 1404
1405 1405 /**
1406 1406 * Clear the selected CodeCell's output area.
1407 1407 *
1408 1408 * @method clear_output
1409 1409 * @param {Number} index A cell's numeric index
1410 1410 */
1411 1411 Notebook.prototype.clear_output = function (index) {
1412 1412 var i = this.index_or_selected(index);
1413 1413 var cell = this.get_cell(i);
1414 1414 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1415 1415 cell.clear_output();
1416 1416 this.set_dirty(true);
1417 1417 }
1418 1418 };
1419 1419
1420 1420 /**
1421 1421 * Clear each code cell's output area.
1422 1422 *
1423 1423 * @method clear_all_output
1424 1424 */
1425 1425 Notebook.prototype.clear_all_output = function () {
1426 1426 $.map(this.get_cells(), function (cell, i) {
1427 1427 if (cell instanceof codecell.CodeCell) {
1428 1428 cell.clear_output();
1429 1429 }
1430 1430 });
1431 1431 this.set_dirty(true);
1432 1432 };
1433 1433
1434 1434 /**
1435 1435 * Scroll the selected CodeCell's output area.
1436 1436 *
1437 1437 * @method scroll_output
1438 1438 * @param {Number} index A cell's numeric index
1439 1439 */
1440 1440 Notebook.prototype.scroll_output = function (index) {
1441 1441 var i = this.index_or_selected(index);
1442 1442 var cell = this.get_cell(i);
1443 1443 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1444 1444 cell.scroll_output();
1445 1445 this.set_dirty(true);
1446 1446 }
1447 1447 };
1448 1448
1449 1449 /**
1450 1450 * Expand each code cell's output area, and add a scrollbar for long output.
1451 1451 *
1452 1452 * @method scroll_all_output
1453 1453 */
1454 1454 Notebook.prototype.scroll_all_output = function () {
1455 1455 $.map(this.get_cells(), function (cell, i) {
1456 1456 if (cell instanceof codecell.CodeCell) {
1457 1457 cell.scroll_output();
1458 1458 }
1459 1459 });
1460 1460 // this should not be set if the `collapse` key is removed from nbformat
1461 1461 this.set_dirty(true);
1462 1462 };
1463 1463
1464 1464 /** Toggle whether a cell's output is collapsed or expanded.
1465 1465 *
1466 1466 * @method toggle_output
1467 1467 * @param {Number} index A cell's numeric index
1468 1468 */
1469 1469 Notebook.prototype.toggle_output = function (index) {
1470 1470 var i = this.index_or_selected(index);
1471 1471 var cell = this.get_cell(i);
1472 1472 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1473 1473 cell.toggle_output();
1474 1474 this.set_dirty(true);
1475 1475 }
1476 1476 };
1477 1477
1478 1478 /**
1479 1479 * Hide/show the output of all cells.
1480 1480 *
1481 1481 * @method toggle_all_output
1482 1482 */
1483 1483 Notebook.prototype.toggle_all_output = function () {
1484 1484 $.map(this.get_cells(), function (cell, i) {
1485 1485 if (cell instanceof codecell.CodeCell) {
1486 1486 cell.toggle_output();
1487 1487 }
1488 1488 });
1489 1489 // this should not be set if the `collapse` key is removed from nbformat
1490 1490 this.set_dirty(true);
1491 1491 };
1492 1492
1493 1493 /**
1494 1494 * Toggle a scrollbar for long cell outputs.
1495 1495 *
1496 1496 * @method toggle_output_scroll
1497 1497 * @param {Number} index A cell's numeric index
1498 1498 */
1499 1499 Notebook.prototype.toggle_output_scroll = function (index) {
1500 1500 var i = this.index_or_selected(index);
1501 1501 var cell = this.get_cell(i);
1502 1502 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1503 1503 cell.toggle_output_scroll();
1504 1504 this.set_dirty(true);
1505 1505 }
1506 1506 };
1507 1507
1508 1508 /**
1509 1509 * Toggle the scrolling of long output on all cells.
1510 1510 *
1511 1511 * @method toggle_all_output_scrolling
1512 1512 */
1513 1513 Notebook.prototype.toggle_all_output_scroll = function () {
1514 1514 $.map(this.get_cells(), function (cell, i) {
1515 1515 if (cell instanceof codecell.CodeCell) {
1516 1516 cell.toggle_output_scroll();
1517 1517 }
1518 1518 });
1519 1519 // this should not be set if the `collapse` key is removed from nbformat
1520 1520 this.set_dirty(true);
1521 1521 };
1522 1522
1523 1523 // Other cell functions: line numbers, ...
1524 1524
1525 1525 /**
1526 1526 * Toggle line numbers in the selected cell's input area.
1527 1527 *
1528 1528 * @method cell_toggle_line_numbers
1529 1529 */
1530 1530 Notebook.prototype.cell_toggle_line_numbers = function() {
1531 1531 this.get_selected_cell().toggle_line_numbers();
1532 1532 };
1533 1533
1534 1534 /**
1535 1535 * Set the codemirror mode for all code cells, including the default for
1536 1536 * new code cells.
1537 1537 *
1538 1538 * @method set_codemirror_mode
1539 1539 */
1540 1540 Notebook.prototype.set_codemirror_mode = function(newmode){
1541 1541 if (newmode === this.codemirror_mode) {
1542 1542 return;
1543 1543 }
1544 1544 this.codemirror_mode = newmode;
1545 1545 codecell.CodeCell.options_default.cm_config.mode = newmode;
1546 1546 modename = newmode.name || newmode
1547 1547
1548 1548 that = this;
1549 1549 CodeMirror.requireMode(modename, function(){
1550 1550 $.map(that.get_cells(), function(cell, i) {
1551 1551 if (cell.cell_type === 'code'){
1552 1552 cell.code_mirror.setOption('mode', newmode);
1553 1553 // This is currently redundant, because cm_config ends up as
1554 1554 // codemirror's own .options object, but I don't want to
1555 1555 // rely on that.
1556 1556 cell.cm_config.mode = newmode;
1557 1557 }
1558 1558 });
1559 1559 })
1560 1560 };
1561 1561
1562 1562 // Session related things
1563 1563
1564 1564 /**
1565 1565 * Start a new session and set it on each code cell.
1566 1566 *
1567 1567 * @method start_session
1568 1568 */
1569 1569 Notebook.prototype.start_session = function (kernel_name) {
1570 1570 var that = this;
1571 1571 if (this._session_starting) {
1572 1572 throw new session.SessionAlreadyStarting();
1573 1573 }
1574 1574 this._session_starting = true;
1575 1575
1576 1576 if (this.session !== null) {
1577 1577 var s = this.session;
1578 1578 this.session = null;
1579 1579 // need to start the new session in a callback after delete,
1580 1580 // because javascript does not guarantee the ordering of AJAX requests (?!)
1581 1581 s.delete(function () {
1582 1582 // on successful delete, start new session
1583 1583 that._session_starting = false;
1584 1584 that.start_session(kernel_name);
1585 1585 }, function (jqXHR, status, error) {
1586 1586 // log the failed delete, but still create a new session
1587 1587 // 404 just means it was already deleted by someone else,
1588 1588 // but other errors are possible.
1589 1589 utils.log_ajax_error(jqXHR, status, error);
1590 1590 that._session_starting = false;
1591 1591 that.start_session(kernel_name);
1592 1592 }
1593 1593 );
1594 1594 return;
1595 1595 }
1596 1596
1597 1597
1598 1598
1599 1599 this.session = new session.Session({
1600 1600 base_url: this.base_url,
1601 1601 ws_url: this.ws_url,
1602 1602 notebook_path: this.notebook_path,
1603 1603 notebook_name: this.notebook_name,
1604 1604 // For now, create all sessions with the 'python' kernel, which is the
1605 1605 // default. Later, the user will be able to select kernels. This is
1606 1606 // overridden if KernelManager.kernel_cmd is specified for the server.
1607 1607 kernel_name: kernel_name,
1608 1608 notebook: this});
1609 1609
1610 1610 this.session.start(
1611 1611 $.proxy(this._session_started, this),
1612 1612 $.proxy(this._session_start_failed, this)
1613 1613 );
1614 1614 };
1615 1615
1616 1616
1617 1617 /**
1618 1618 * Once a session is started, link the code cells to the kernel and pass the
1619 1619 * comm manager to the widget manager
1620 1620 *
1621 1621 */
1622 1622 Notebook.prototype._session_started = function (){
1623 1623 this._session_starting = false;
1624 1624 this.kernel = this.session.kernel;
1625 1625 var ncells = this.ncells();
1626 1626 for (var i=0; i<ncells; i++) {
1627 1627 var cell = this.get_cell(i);
1628 1628 if (cell instanceof codecell.CodeCell) {
1629 1629 cell.set_kernel(this.session.kernel);
1630 1630 }
1631 1631 }
1632 1632 };
1633 1633 Notebook.prototype._session_start_failed = function (jqxhr, status, error){
1634 1634 this._session_starting = false;
1635 1635 utils.log_ajax_error(jqxhr, status, error);
1636 1636 };
1637 1637
1638 1638 /**
1639 1639 * Prompt the user to restart the IPython kernel.
1640 1640 *
1641 1641 * @method restart_kernel
1642 1642 */
1643 1643 Notebook.prototype.restart_kernel = function () {
1644 1644 var that = this;
1645 1645 dialog.modal({
1646 1646 notebook: this,
1647 1647 keyboard_manager: this.keyboard_manager,
1648 1648 title : "Restart kernel or continue running?",
1649 1649 body : $("<p/>").text(
1650 1650 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1651 1651 ),
1652 1652 buttons : {
1653 1653 "Continue running" : {},
1654 1654 "Restart" : {
1655 1655 "class" : "btn-danger",
1656 1656 "click" : function() {
1657 1657 that.session.restart_kernel();
1658 1658 }
1659 1659 }
1660 1660 }
1661 1661 });
1662 1662 };
1663 1663
1664 1664 /**
1665 1665 * Execute or render cell outputs and go into command mode.
1666 1666 *
1667 1667 * @method execute_cell
1668 1668 */
1669 1669 Notebook.prototype.execute_cell = function () {
1670 1670 // mode = shift, ctrl, alt
1671 1671 var cell = this.get_selected_cell();
1672 1672 var cell_index = this.find_cell_index(cell);
1673 1673
1674 1674 cell.execute();
1675 1675 this.command_mode();
1676 1676 this.set_dirty(true);
1677 1677 };
1678 1678
1679 1679 /**
1680 1680 * Execute or render cell outputs and insert a new cell below.
1681 1681 *
1682 1682 * @method execute_cell_and_insert_below
1683 1683 */
1684 1684 Notebook.prototype.execute_cell_and_insert_below = function () {
1685 1685 var cell = this.get_selected_cell();
1686 1686 var cell_index = this.find_cell_index(cell);
1687 1687
1688 1688 cell.execute();
1689 1689
1690 1690 // If we are at the end always insert a new cell and return
1691 1691 if (cell_index === (this.ncells()-1)) {
1692 1692 this.command_mode();
1693 1693 this.insert_cell_below();
1694 1694 this.select(cell_index+1);
1695 1695 this.edit_mode();
1696 1696 this.scroll_to_bottom();
1697 1697 this.set_dirty(true);
1698 1698 return;
1699 1699 }
1700 1700
1701 1701 this.command_mode();
1702 1702 this.insert_cell_below();
1703 1703 this.select(cell_index+1);
1704 1704 this.edit_mode();
1705 1705 this.set_dirty(true);
1706 1706 };
1707 1707
1708 1708 /**
1709 1709 * Execute or render cell outputs and select the next cell.
1710 1710 *
1711 1711 * @method execute_cell_and_select_below
1712 1712 */
1713 1713 Notebook.prototype.execute_cell_and_select_below = function () {
1714 1714
1715 1715 var cell = this.get_selected_cell();
1716 1716 var cell_index = this.find_cell_index(cell);
1717 1717
1718 1718 cell.execute();
1719 1719
1720 1720 // If we are at the end always insert a new cell and return
1721 1721 if (cell_index === (this.ncells()-1)) {
1722 1722 this.command_mode();
1723 1723 this.insert_cell_below();
1724 1724 this.select(cell_index+1);
1725 1725 this.edit_mode();
1726 1726 this.scroll_to_bottom();
1727 1727 this.set_dirty(true);
1728 1728 return;
1729 1729 }
1730 1730
1731 1731 this.command_mode();
1732 1732 this.select(cell_index+1);
1733 1733 this.focus_cell();
1734 1734 this.set_dirty(true);
1735 1735 };
1736 1736
1737 1737 /**
1738 1738 * Execute all cells below the selected cell.
1739 1739 *
1740 1740 * @method execute_cells_below
1741 1741 */
1742 1742 Notebook.prototype.execute_cells_below = function () {
1743 1743 this.execute_cell_range(this.get_selected_index(), this.ncells());
1744 1744 this.scroll_to_bottom();
1745 1745 };
1746 1746
1747 1747 /**
1748 1748 * Execute all cells above the selected cell.
1749 1749 *
1750 1750 * @method execute_cells_above
1751 1751 */
1752 1752 Notebook.prototype.execute_cells_above = function () {
1753 1753 this.execute_cell_range(0, this.get_selected_index());
1754 1754 };
1755 1755
1756 1756 /**
1757 1757 * Execute all cells.
1758 1758 *
1759 1759 * @method execute_all_cells
1760 1760 */
1761 1761 Notebook.prototype.execute_all_cells = function () {
1762 1762 this.execute_cell_range(0, this.ncells());
1763 1763 this.scroll_to_bottom();
1764 1764 };
1765 1765
1766 1766 /**
1767 1767 * Execute a contiguous range of cells.
1768 1768 *
1769 1769 * @method execute_cell_range
1770 1770 * @param {Number} start Index of the first cell to execute (inclusive)
1771 1771 * @param {Number} end Index of the last cell to execute (exclusive)
1772 1772 */
1773 1773 Notebook.prototype.execute_cell_range = function (start, end) {
1774 1774 this.command_mode();
1775 1775 for (var i=start; i<end; i++) {
1776 1776 this.select(i);
1777 1777 this.execute_cell();
1778 1778 }
1779 1779 };
1780 1780
1781 1781 // Persistance and loading
1782 1782
1783 1783 /**
1784 1784 * Getter method for this notebook's name.
1785 1785 *
1786 1786 * @method get_notebook_name
1787 1787 * @return {String} This notebook's name (excluding file extension)
1788 1788 */
1789 1789 Notebook.prototype.get_notebook_name = function () {
1790 1790 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1791 1791 return nbname;
1792 1792 };
1793 1793
1794 1794 /**
1795 1795 * Setter method for this notebook's name.
1796 1796 *
1797 1797 * @method set_notebook_name
1798 1798 * @param {String} name A new name for this notebook
1799 1799 */
1800 1800 Notebook.prototype.set_notebook_name = function (name) {
1801 1801 this.notebook_name = name;
1802 1802 };
1803 1803
1804 1804 /**
1805 1805 * Check that a notebook's name is valid.
1806 1806 *
1807 1807 * @method test_notebook_name
1808 1808 * @param {String} nbname A name for this notebook
1809 1809 * @return {Boolean} True if the name is valid, false if invalid
1810 1810 */
1811 1811 Notebook.prototype.test_notebook_name = function (nbname) {
1812 1812 nbname = nbname || '';
1813 1813 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1814 1814 return true;
1815 1815 } else {
1816 1816 return false;
1817 1817 }
1818 1818 };
1819 1819
1820 1820 /**
1821 1821 * Load a notebook from JSON (.ipynb).
1822 1822 *
1823 1823 * This currently handles one worksheet: others are deleted.
1824 1824 *
1825 1825 * @method fromJSON
1826 1826 * @param {Object} data JSON representation of a notebook
1827 1827 */
1828 1828 Notebook.prototype.fromJSON = function (data) {
1829
1829 1830 var content = data.content;
1830 1831 var ncells = this.ncells();
1831 1832 var i;
1832 1833 for (i=0; i<ncells; i++) {
1833 1834 // Always delete cell 0 as they get renumbered as they are deleted.
1834 1835 this.delete_cell(0);
1835 1836 }
1836 1837 // Save the metadata and name.
1837 1838 this.metadata = content.metadata;
1838 1839 this.notebook_name = data.name;
1839 1840 var trusted = true;
1840 1841
1841 1842 // Trigger an event changing the kernel spec - this will set the default
1842 1843 // codemirror mode
1843 1844 if (this.metadata.kernelspec !== undefined) {
1844 1845 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1845 1846 }
1846 1847
1847 1848 // Only handle 1 worksheet for now.
1848 1849 var worksheet = content.worksheets[0];
1849 1850 if (worksheet !== undefined) {
1850 1851 if (worksheet.metadata) {
1851 1852 this.worksheet_metadata = worksheet.metadata;
1852 1853 }
1853 1854 var new_cells = worksheet.cells;
1854 1855 ncells = new_cells.length;
1855 1856 var cell_data = null;
1856 1857 var new_cell = null;
1857 1858 for (i=0; i<ncells; i++) {
1858 1859 cell_data = new_cells[i];
1859 1860 // VERSIONHACK: plaintext -> raw
1860 1861 // handle never-released plaintext name for raw cells
1861 1862 if (cell_data.cell_type === 'plaintext'){
1862 1863 cell_data.cell_type = 'raw';
1863 1864 }
1864 1865
1865 1866 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1866 1867 new_cell.fromJSON(cell_data);
1867 1868 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1868 1869 trusted = false;
1869 1870 }
1870 1871 }
1871 1872 }
1872 1873 if (trusted !== this.trusted) {
1873 1874 this.trusted = trusted;
1874 1875 this.events.trigger("trust_changed.Notebook", {value: trusted});
1875 1876 }
1876 1877 if (content.worksheets.length > 1) {
1877 1878 dialog.modal({
1878 1879 notebook: this,
1879 1880 keyboard_manager: this.keyboard_manager,
1880 1881 title : "Multiple worksheets",
1881 1882 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1882 1883 "but this version of IPython can only handle the first. " +
1883 1884 "If you save this notebook, worksheets after the first will be lost.",
1884 1885 buttons : {
1885 1886 OK : {
1886 1887 class : "btn-danger"
1887 1888 }
1888 1889 }
1889 1890 });
1890 1891 }
1891 1892 };
1892 1893
1893 1894 /**
1894 1895 * Dump this notebook into a JSON-friendly object.
1895 1896 *
1896 1897 * @method toJSON
1897 1898 * @return {Object} A JSON-friendly representation of this notebook.
1898 1899 */
1899 1900 Notebook.prototype.toJSON = function () {
1900 1901 var cells = this.get_cells();
1901 1902 var ncells = cells.length;
1902 1903 var cell_array = new Array(ncells);
1903 1904 var trusted = true;
1904 1905 for (var i=0; i<ncells; i++) {
1905 1906 var cell = cells[i];
1906 1907 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1907 1908 trusted = false;
1908 1909 }
1909 1910 cell_array[i] = cell.toJSON();
1910 1911 }
1911 1912 var data = {
1912 1913 // Only handle 1 worksheet for now.
1913 1914 worksheets : [{
1914 1915 cells: cell_array,
1915 1916 metadata: this.worksheet_metadata
1916 1917 }],
1917 1918 metadata : this.metadata
1918 1919 };
1919 1920 if (trusted != this.trusted) {
1920 1921 this.trusted = trusted;
1921 1922 this.events.trigger("trust_changed.Notebook", trusted);
1922 1923 }
1923 1924 return data;
1924 1925 };
1925 1926
1926 1927 /**
1927 1928 * Start an autosave timer, for periodically saving the notebook.
1928 1929 *
1929 1930 * @method set_autosave_interval
1930 1931 * @param {Integer} interval the autosave interval in milliseconds
1931 1932 */
1932 1933 Notebook.prototype.set_autosave_interval = function (interval) {
1933 1934 var that = this;
1934 1935 // clear previous interval, so we don't get simultaneous timers
1935 1936 if (this.autosave_timer) {
1936 1937 clearInterval(this.autosave_timer);
1937 1938 }
1938 1939
1939 1940 this.autosave_interval = this.minimum_autosave_interval = interval;
1940 1941 if (interval) {
1941 1942 this.autosave_timer = setInterval(function() {
1942 1943 if (that.dirty) {
1943 1944 that.save_notebook();
1944 1945 }
1945 1946 }, interval);
1946 1947 this.events.trigger("autosave_enabled.Notebook", interval);
1947 1948 } else {
1948 1949 this.autosave_timer = null;
1949 1950 this.events.trigger("autosave_disabled.Notebook");
1950 1951 }
1951 1952 };
1952 1953
1953 1954 /**
1954 1955 * Save this notebook on the server. This becomes a notebook instance's
1955 1956 * .save_notebook method *after* the entire notebook has been loaded.
1956 1957 *
1957 1958 * @method save_notebook
1958 1959 */
1959 1960 Notebook.prototype.save_notebook = function (extra_settings) {
1960 1961 // Create a JSON model to be sent to the server.
1961 1962 var model = {};
1962 1963 model.name = this.notebook_name;
1963 1964 model.path = this.notebook_path;
1964 1965 model.type = 'notebook';
1965 1966 model.format = 'json';
1966 1967 model.content = this.toJSON();
1967 1968 model.content.nbformat = this.nbformat;
1968 1969 model.content.nbformat_minor = this.nbformat_minor;
1969 1970 // time the ajax call for autosave tuning purposes.
1970 1971 var start = new Date().getTime();
1971 1972 // We do the call with settings so we can set cache to false.
1972 1973 var settings = {
1973 1974 processData : false,
1974 1975 cache : false,
1975 1976 type : "PUT",
1976 1977 data : JSON.stringify(model),
1977 1978 headers : {'Content-Type': 'application/json'},
1979 dataType : "json",
1978 1980 success : $.proxy(this.save_notebook_success, this, start),
1979 1981 error : $.proxy(this.save_notebook_error, this)
1980 1982 };
1981 1983 if (extra_settings) {
1982 1984 for (var key in extra_settings) {
1983 1985 settings[key] = extra_settings[key];
1984 1986 }
1985 1987 }
1986 1988 this.events.trigger('notebook_saving.Notebook');
1987 1989 var url = utils.url_join_encode(
1988 1990 this.base_url,
1989 1991 'api/contents',
1990 1992 this.notebook_path,
1991 1993 this.notebook_name
1992 1994 );
1993 1995 $.ajax(url, settings);
1994 1996 };
1995 1997
1996 1998 /**
1997 1999 * Success callback for saving a notebook.
1998 2000 *
1999 2001 * @method save_notebook_success
2000 2002 * @param {Integer} start the time when the save request started
2001 2003 * @param {Object} data JSON representation of a notebook
2002 2004 * @param {String} status Description of response status
2003 2005 * @param {jqXHR} xhr jQuery Ajax object
2004 2006 */
2005 2007 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
2006 2008 this.set_dirty(false);
2009 if (data.message) {
2010 // save succeeded, but validation failed.
2011 var body = $("<div>");
2012 var title = "Notebook validation failed";
2013
2014 body.append($("<p>").text(
2015 "The save operation succeeded," +
2016 " but the notebook does not appear to be valid." +
2017 " The validation error was:"
2018 )).append($("<div>").addClass("validation-error").append(
2019 $("<pre>").text(data.message)
2020 ));
2021 dialog.modal({
2022 notebook: this,
2023 keyboard_manager: this.keyboard_manager,
2024 title: title,
2025 body: body,
2026 buttons : {
2027 OK : {
2028 "class" : "btn-primary"
2029 }
2030 }
2031 });
2032 }
2007 2033 this.events.trigger('notebook_saved.Notebook');
2008 2034 this._update_autosave_interval(start);
2009 2035 if (this._checkpoint_after_save) {
2010 2036 this.create_checkpoint();
2011 2037 this._checkpoint_after_save = false;
2012 2038 }
2013 2039 };
2014 2040
2015 2041 /**
2016 2042 * update the autosave interval based on how long the last save took
2017 2043 *
2018 2044 * @method _update_autosave_interval
2019 2045 * @param {Integer} timestamp when the save request started
2020 2046 */
2021 2047 Notebook.prototype._update_autosave_interval = function (start) {
2022 2048 var duration = (new Date().getTime() - start);
2023 2049 if (this.autosave_interval) {
2024 2050 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
2025 2051 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
2026 2052 // round to 10 seconds, otherwise we will be setting a new interval too often
2027 2053 interval = 10000 * Math.round(interval / 10000);
2028 2054 // set new interval, if it's changed
2029 2055 if (interval != this.autosave_interval) {
2030 2056 this.set_autosave_interval(interval);
2031 2057 }
2032 2058 }
2033 2059 };
2034 2060
2035 2061 /**
2036 2062 * Failure callback for saving a notebook.
2037 2063 *
2038 2064 * @method save_notebook_error
2039 2065 * @param {jqXHR} xhr jQuery Ajax object
2040 2066 * @param {String} status Description of response status
2041 2067 * @param {String} error HTTP error message
2042 2068 */
2043 2069 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
2044 2070 this.events.trigger('notebook_save_failed.Notebook', [xhr, status, error]);
2045 2071 };
2046 2072
2047 2073 /**
2048 2074 * Explicitly trust the output of this notebook.
2049 2075 *
2050 2076 * @method trust_notebook
2051 2077 */
2052 2078 Notebook.prototype.trust_notebook = function (extra_settings) {
2053 2079 var body = $("<div>").append($("<p>")
2054 2080 .text("A trusted IPython notebook may execute hidden malicious code ")
2055 2081 .append($("<strong>")
2056 2082 .append(
2057 2083 $("<em>").text("when you open it")
2058 2084 )
2059 2085 ).append(".").append(
2060 2086 " Selecting trust will immediately reload this notebook in a trusted state."
2061 2087 ).append(
2062 2088 " For more information, see the "
2063 2089 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
2064 2090 .text("IPython security documentation")
2065 2091 ).append(".")
2066 2092 );
2067 2093
2068 2094 var nb = this;
2069 2095 dialog.modal({
2070 2096 notebook: this,
2071 2097 keyboard_manager: this.keyboard_manager,
2072 2098 title: "Trust this notebook?",
2073 2099 body: body,
2074 2100
2075 2101 buttons: {
2076 2102 Cancel : {},
2077 2103 Trust : {
2078 2104 class : "btn-danger",
2079 2105 click : function () {
2080 2106 var cells = nb.get_cells();
2081 2107 for (var i = 0; i < cells.length; i++) {
2082 2108 var cell = cells[i];
2083 2109 if (cell.cell_type == 'code') {
2084 2110 cell.output_area.trusted = true;
2085 2111 }
2086 2112 }
2087 2113 nb.events.on('notebook_saved.Notebook', function () {
2088 2114 window.location.reload();
2089 2115 });
2090 2116 nb.save_notebook();
2091 2117 }
2092 2118 }
2093 2119 }
2094 2120 });
2095 2121 };
2096 2122
2097 2123 Notebook.prototype.new_notebook = function(){
2098 2124 var path = this.notebook_path;
2099 2125 var base_url = this.base_url;
2100 2126 var settings = {
2101 2127 processData : false,
2102 2128 cache : false,
2103 2129 type : "POST",
2104 2130 dataType : "json",
2105 2131 async : false,
2106 2132 success : function (data, status, xhr){
2107 2133 var notebook_name = data.name;
2108 2134 window.open(
2109 2135 utils.url_join_encode(
2110 2136 base_url,
2111 2137 'notebooks',
2112 2138 path,
2113 2139 notebook_name
2114 2140 ),
2115 2141 '_blank'
2116 2142 );
2117 2143 },
2118 2144 error : utils.log_ajax_error,
2119 2145 };
2120 2146 var url = utils.url_join_encode(
2121 2147 base_url,
2122 2148 'api/contents',
2123 2149 path
2124 2150 );
2125 2151 $.ajax(url,settings);
2126 2152 };
2127 2153
2128 2154
2129 2155 Notebook.prototype.copy_notebook = function(){
2130 2156 var path = this.notebook_path;
2131 2157 var base_url = this.base_url;
2132 2158 var settings = {
2133 2159 processData : false,
2134 2160 cache : false,
2135 2161 type : "POST",
2136 2162 dataType : "json",
2137 2163 data : JSON.stringify({copy_from : this.notebook_name}),
2138 2164 async : false,
2139 2165 success : function (data, status, xhr) {
2140 2166 window.open(utils.url_join_encode(
2141 2167 base_url,
2142 2168 'notebooks',
2143 2169 data.path,
2144 2170 data.name
2145 2171 ), '_blank');
2146 2172 },
2147 2173 error : utils.log_ajax_error,
2148 2174 };
2149 2175 var url = utils.url_join_encode(
2150 2176 base_url,
2151 2177 'api/contents',
2152 2178 path
2153 2179 );
2154 2180 $.ajax(url,settings);
2155 2181 };
2156 2182
2157 2183 Notebook.prototype.rename = function (nbname) {
2158 2184 var that = this;
2159 2185 if (!nbname.match(/\.ipynb$/)) {
2160 2186 nbname = nbname + ".ipynb";
2161 2187 }
2162 2188 var data = {name: nbname};
2163 2189 var settings = {
2164 2190 processData : false,
2165 2191 cache : false,
2166 2192 type : "PATCH",
2167 2193 data : JSON.stringify(data),
2168 2194 dataType: "json",
2169 2195 headers : {'Content-Type': 'application/json'},
2170 2196 success : $.proxy(that.rename_success, this),
2171 2197 error : $.proxy(that.rename_error, this)
2172 2198 };
2173 2199 this.events.trigger('rename_notebook.Notebook', data);
2174 2200 var url = utils.url_join_encode(
2175 2201 this.base_url,
2176 2202 'api/contents',
2177 2203 this.notebook_path,
2178 2204 this.notebook_name
2179 2205 );
2180 2206 $.ajax(url, settings);
2181 2207 };
2182 2208
2183 2209 Notebook.prototype.delete = function () {
2184 2210 var that = this;
2185 2211 var settings = {
2186 2212 processData : false,
2187 2213 cache : false,
2188 2214 type : "DELETE",
2189 2215 dataType: "json",
2190 2216 error : utils.log_ajax_error,
2191 2217 };
2192 2218 var url = utils.url_join_encode(
2193 2219 this.base_url,
2194 2220 'api/contents',
2195 2221 this.notebook_path,
2196 2222 this.notebook_name
2197 2223 );
2198 2224 $.ajax(url, settings);
2199 2225 };
2200 2226
2201 2227
2202 2228 Notebook.prototype.rename_success = function (json, status, xhr) {
2203 2229 var name = this.notebook_name = json.name;
2204 2230 var path = json.path;
2205 2231 this.session.rename_notebook(name, path);
2206 2232 this.events.trigger('notebook_renamed.Notebook', json);
2207 2233 };
2208 2234
2209 2235 Notebook.prototype.rename_error = function (xhr, status, error) {
2210 2236 var that = this;
2211 2237 var dialog_body = $('<div/>').append(
2212 2238 $("<p/>").text('This notebook name already exists.')
2213 2239 );
2214 2240 this.events.trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
2215 2241 dialog.modal({
2216 2242 notebook: this,
2217 2243 keyboard_manager: this.keyboard_manager,
2218 2244 title: "Notebook Rename Error!",
2219 2245 body: dialog_body,
2220 2246 buttons : {
2221 2247 "Cancel": {},
2222 2248 "OK": {
2223 2249 class: "btn-primary",
2224 2250 click: function () {
2225 2251 this.save_widget.rename_notebook({notebook:that});
2226 2252 }}
2227 2253 },
2228 2254 open : function (event, ui) {
2229 2255 var that = $(this);
2230 2256 // Upon ENTER, click the OK button.
2231 2257 that.find('input[type="text"]').keydown(function (event, ui) {
2232 2258 if (event.which === this.keyboard.keycodes.enter) {
2233 2259 that.find('.btn-primary').first().click();
2234 2260 }
2235 2261 });
2236 2262 that.find('input[type="text"]').focus();
2237 2263 }
2238 2264 });
2239 2265 };
2240 2266
2241 2267 /**
2242 2268 * Request a notebook's data from the server.
2243 2269 *
2244 2270 * @method load_notebook
2245 2271 * @param {String} notebook_name and path A notebook to load
2246 2272 */
2247 2273 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
2248 2274 var that = this;
2249 2275 this.notebook_name = notebook_name;
2250 2276 this.notebook_path = notebook_path;
2251 2277 // We do the call with settings so we can set cache to false.
2252 2278 var settings = {
2253 2279 processData : false,
2254 2280 cache : false,
2255 2281 type : "GET",
2256 2282 dataType : "json",
2257 2283 success : $.proxy(this.load_notebook_success,this),
2258 2284 error : $.proxy(this.load_notebook_error,this),
2259 2285 };
2260 2286 this.events.trigger('notebook_loading.Notebook');
2261 2287 var url = utils.url_join_encode(
2262 2288 this.base_url,
2263 2289 'api/contents',
2264 2290 this.notebook_path,
2265 2291 this.notebook_name
2266 2292 );
2267 2293 $.ajax(url, settings);
2268 2294 };
2269 2295
2270 2296 /**
2271 2297 * Success callback for loading a notebook from the server.
2272 2298 *
2273 2299 * Load notebook data from the JSON response.
2274 2300 *
2275 2301 * @method load_notebook_success
2276 2302 * @param {Object} data JSON representation of a notebook
2277 2303 * @param {String} status Description of response status
2278 2304 * @param {jqXHR} xhr jQuery Ajax object
2279 2305 */
2280 2306 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
2307 var failed;
2308 try {
2281 2309 this.fromJSON(data);
2310 } catch (e) {
2311 failed = e;
2312 console.log("Notebook failed to load from JSON:", e);
2313 }
2314 if (failed || data.message) {
2315 // *either* fromJSON failed or validation failed
2316 var body = $("<div>");
2317 var title;
2318 if (failed) {
2319 title = "Notebook failed to load";
2320 body.append($("<p>").text(
2321 "The error was: "
2322 )).append($("<div>").addClass("js-error").text(
2323 failed.toString()
2324 )).append($("<p>").text(
2325 "See the error console for details."
2326 ));
2327 } else {
2328 title = "Notebook validation failed";
2329 }
2330
2331 if (data.message) {
2332 var msg;
2333 if (failed) {
2334 msg = "The notebook also failed validation:"
2335 } else {
2336 msg = "An invalid notebook may not function properly." +
2337 " The validation error was:"
2338 }
2339 body.append($("<p>").text(
2340 msg
2341 )).append($("<div>").addClass("validation-error").append(
2342 $("<pre>").text(data.message)
2343 ));
2344 }
2345
2346 dialog.modal({
2347 notebook: this,
2348 keyboard_manager: this.keyboard_manager,
2349 title: title,
2350 body: body,
2351 buttons : {
2352 OK : {
2353 "class" : "btn-primary"
2354 }
2355 }
2356 });
2357 }
2282 2358 if (this.ncells() === 0) {
2283 2359 this.insert_cell_below('code');
2284 2360 this.edit_mode(0);
2285 2361 } else {
2286 2362 this.select(0);
2287 2363 this.handle_command_mode(this.get_cell(0));
2288 2364 }
2289 2365 this.set_dirty(false);
2290 2366 this.scroll_to_top();
2291 2367 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
2292 2368 var msg = "This notebook has been converted from an older " +
2293 2369 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
2294 2370 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
2295 2371 "newer notebook format will be used and older versions of IPython " +
2296 2372 "may not be able to read it. To keep the older version, close the " +
2297 2373 "notebook without saving it.";
2298 2374 dialog.modal({
2299 2375 notebook: this,
2300 2376 keyboard_manager: this.keyboard_manager,
2301 2377 title : "Notebook converted",
2302 2378 body : msg,
2303 2379 buttons : {
2304 2380 OK : {
2305 2381 class : "btn-primary"
2306 2382 }
2307 2383 }
2308 2384 });
2309 2385 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
2310 2386 var that = this;
2311 2387 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
2312 2388 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
2313 2389 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2314 2390 this_vs + ". You can still work with this notebook, but some features " +
2315 2391 "introduced in later notebook versions may not be available.";
2316 2392
2317 2393 dialog.modal({
2318 2394 notebook: this,
2319 2395 keyboard_manager: this.keyboard_manager,
2320 2396 title : "Newer Notebook",
2321 2397 body : msg,
2322 2398 buttons : {
2323 2399 OK : {
2324 2400 class : "btn-danger"
2325 2401 }
2326 2402 }
2327 2403 });
2328 2404
2329 2405 }
2330 2406
2331 2407 // Create the session after the notebook is completely loaded to prevent
2332 2408 // code execution upon loading, which is a security risk.
2333 2409 if (this.session === null) {
2334 2410 var kernelspec = this.metadata.kernelspec || {};
2335 2411 var kernel_name = kernelspec.name;
2336 2412
2337 2413 this.start_session(kernel_name);
2338 2414 }
2339 2415 // load our checkpoint list
2340 2416 this.list_checkpoints();
2341 2417
2342 2418 // load toolbar state
2343 2419 if (this.metadata.celltoolbar) {
2344 2420 celltoolbar.CellToolbar.global_show();
2345 2421 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2346 2422 } else {
2347 2423 celltoolbar.CellToolbar.global_hide();
2348 2424 }
2349 2425
2350 2426 // now that we're fully loaded, it is safe to restore save functionality
2351 2427 delete(this.save_notebook);
2352 2428 this.events.trigger('notebook_loaded.Notebook');
2353 2429 };
2354 2430
2355 2431 /**
2356 2432 * Failure callback for loading a notebook from the server.
2357 2433 *
2358 2434 * @method load_notebook_error
2359 2435 * @param {jqXHR} xhr jQuery Ajax object
2360 2436 * @param {String} status Description of response status
2361 2437 * @param {String} error HTTP error message
2362 2438 */
2363 2439 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
2364 2440 this.events.trigger('notebook_load_failed.Notebook', [xhr, status, error]);
2365 2441 utils.log_ajax_error(xhr, status, error);
2366 2442 var msg;
2367 2443 if (xhr.status === 400) {
2368 2444 msg = escape(utils.ajax_error_msg(xhr));
2369 2445 } else if (xhr.status === 500) {
2370 2446 msg = "An unknown error occurred while loading this notebook. " +
2371 2447 "This version can load notebook formats " +
2372 2448 "v" + this.nbformat + " or earlier. See the server log for details.";
2373 2449 }
2374 2450 dialog.modal({
2375 2451 notebook: this,
2376 2452 keyboard_manager: this.keyboard_manager,
2377 2453 title: "Error loading notebook",
2378 2454 body : msg,
2379 2455 buttons : {
2380 2456 "OK": {}
2381 2457 }
2382 2458 });
2383 2459 };
2384 2460
2385 2461 /********************* checkpoint-related *********************/
2386 2462
2387 2463 /**
2388 2464 * Save the notebook then immediately create a checkpoint.
2389 2465 *
2390 2466 * @method save_checkpoint
2391 2467 */
2392 2468 Notebook.prototype.save_checkpoint = function () {
2393 2469 this._checkpoint_after_save = true;
2394 2470 this.save_notebook();
2395 2471 };
2396 2472
2397 2473 /**
2398 2474 * Add a checkpoint for this notebook.
2399 2475 * for use as a callback from checkpoint creation.
2400 2476 *
2401 2477 * @method add_checkpoint
2402 2478 */
2403 2479 Notebook.prototype.add_checkpoint = function (checkpoint) {
2404 2480 var found = false;
2405 2481 for (var i = 0; i < this.checkpoints.length; i++) {
2406 2482 var existing = this.checkpoints[i];
2407 2483 if (existing.id == checkpoint.id) {
2408 2484 found = true;
2409 2485 this.checkpoints[i] = checkpoint;
2410 2486 break;
2411 2487 }
2412 2488 }
2413 2489 if (!found) {
2414 2490 this.checkpoints.push(checkpoint);
2415 2491 }
2416 2492 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2417 2493 };
2418 2494
2419 2495 /**
2420 2496 * List checkpoints for this notebook.
2421 2497 *
2422 2498 * @method list_checkpoints
2423 2499 */
2424 2500 Notebook.prototype.list_checkpoints = function () {
2425 2501 var url = utils.url_join_encode(
2426 2502 this.base_url,
2427 2503 'api/contents',
2428 2504 this.notebook_path,
2429 2505 this.notebook_name,
2430 2506 'checkpoints'
2431 2507 );
2432 2508 $.get(url).done(
2433 2509 $.proxy(this.list_checkpoints_success, this)
2434 2510 ).fail(
2435 2511 $.proxy(this.list_checkpoints_error, this)
2436 2512 );
2437 2513 };
2438 2514
2439 2515 /**
2440 2516 * Success callback for listing checkpoints.
2441 2517 *
2442 2518 * @method list_checkpoint_success
2443 2519 * @param {Object} data JSON representation of a checkpoint
2444 2520 * @param {String} status Description of response status
2445 2521 * @param {jqXHR} xhr jQuery Ajax object
2446 2522 */
2447 2523 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2448 2524 data = $.parseJSON(data);
2449 2525 this.checkpoints = data;
2450 2526 if (data.length) {
2451 2527 this.last_checkpoint = data[data.length - 1];
2452 2528 } else {
2453 2529 this.last_checkpoint = null;
2454 2530 }
2455 2531 this.events.trigger('checkpoints_listed.Notebook', [data]);
2456 2532 };
2457 2533
2458 2534 /**
2459 2535 * Failure callback for listing a checkpoint.
2460 2536 *
2461 2537 * @method list_checkpoint_error
2462 2538 * @param {jqXHR} xhr jQuery Ajax object
2463 2539 * @param {String} status Description of response status
2464 2540 * @param {String} error_msg HTTP error message
2465 2541 */
2466 2542 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2467 2543 this.events.trigger('list_checkpoints_failed.Notebook');
2468 2544 };
2469 2545
2470 2546 /**
2471 2547 * Create a checkpoint of this notebook on the server from the most recent save.
2472 2548 *
2473 2549 * @method create_checkpoint
2474 2550 */
2475 2551 Notebook.prototype.create_checkpoint = function () {
2476 2552 var url = utils.url_join_encode(
2477 2553 this.base_url,
2478 2554 'api/contents',
2479 2555 this.notebook_path,
2480 2556 this.notebook_name,
2481 2557 'checkpoints'
2482 2558 );
2483 2559 $.post(url).done(
2484 2560 $.proxy(this.create_checkpoint_success, this)
2485 2561 ).fail(
2486 2562 $.proxy(this.create_checkpoint_error, this)
2487 2563 );
2488 2564 };
2489 2565
2490 2566 /**
2491 2567 * Success callback for creating a checkpoint.
2492 2568 *
2493 2569 * @method create_checkpoint_success
2494 2570 * @param {Object} data JSON representation of a checkpoint
2495 2571 * @param {String} status Description of response status
2496 2572 * @param {jqXHR} xhr jQuery Ajax object
2497 2573 */
2498 2574 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2499 2575 data = $.parseJSON(data);
2500 2576 this.add_checkpoint(data);
2501 2577 this.events.trigger('checkpoint_created.Notebook', data);
2502 2578 };
2503 2579
2504 2580 /**
2505 2581 * Failure callback for creating a checkpoint.
2506 2582 *
2507 2583 * @method create_checkpoint_error
2508 2584 * @param {jqXHR} xhr jQuery Ajax object
2509 2585 * @param {String} status Description of response status
2510 2586 * @param {String} error_msg HTTP error message
2511 2587 */
2512 2588 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2513 2589 this.events.trigger('checkpoint_failed.Notebook');
2514 2590 };
2515 2591
2516 2592 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2517 2593 var that = this;
2518 2594 checkpoint = checkpoint || this.last_checkpoint;
2519 2595 if ( ! checkpoint ) {
2520 2596 console.log("restore dialog, but no checkpoint to restore to!");
2521 2597 return;
2522 2598 }
2523 2599 var body = $('<div/>').append(
2524 2600 $('<p/>').addClass("p-space").text(
2525 2601 "Are you sure you want to revert the notebook to " +
2526 2602 "the latest checkpoint?"
2527 2603 ).append(
2528 2604 $("<strong/>").text(
2529 2605 " This cannot be undone."
2530 2606 )
2531 2607 )
2532 2608 ).append(
2533 2609 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2534 2610 ).append(
2535 2611 $('<p/>').addClass("p-space").text(
2536 2612 Date(checkpoint.last_modified)
2537 2613 ).css("text-align", "center")
2538 2614 );
2539 2615
2540 2616 dialog.modal({
2541 2617 notebook: this,
2542 2618 keyboard_manager: this.keyboard_manager,
2543 2619 title : "Revert notebook to checkpoint",
2544 2620 body : body,
2545 2621 buttons : {
2546 2622 Revert : {
2547 2623 class : "btn-danger",
2548 2624 click : function () {
2549 2625 that.restore_checkpoint(checkpoint.id);
2550 2626 }
2551 2627 },
2552 2628 Cancel : {}
2553 2629 }
2554 2630 });
2555 2631 };
2556 2632
2557 2633 /**
2558 2634 * Restore the notebook to a checkpoint state.
2559 2635 *
2560 2636 * @method restore_checkpoint
2561 2637 * @param {String} checkpoint ID
2562 2638 */
2563 2639 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2564 2640 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2565 2641 var url = utils.url_join_encode(
2566 2642 this.base_url,
2567 2643 'api/contents',
2568 2644 this.notebook_path,
2569 2645 this.notebook_name,
2570 2646 'checkpoints',
2571 2647 checkpoint
2572 2648 );
2573 2649 $.post(url).done(
2574 2650 $.proxy(this.restore_checkpoint_success, this)
2575 2651 ).fail(
2576 2652 $.proxy(this.restore_checkpoint_error, this)
2577 2653 );
2578 2654 };
2579 2655
2580 2656 /**
2581 2657 * Success callback for restoring a notebook to a checkpoint.
2582 2658 *
2583 2659 * @method restore_checkpoint_success
2584 2660 * @param {Object} data (ignored, should be empty)
2585 2661 * @param {String} status Description of response status
2586 2662 * @param {jqXHR} xhr jQuery Ajax object
2587 2663 */
2588 2664 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2589 2665 this.events.trigger('checkpoint_restored.Notebook');
2590 2666 this.load_notebook(this.notebook_name, this.notebook_path);
2591 2667 };
2592 2668
2593 2669 /**
2594 2670 * Failure callback for restoring a notebook to a checkpoint.
2595 2671 *
2596 2672 * @method restore_checkpoint_error
2597 2673 * @param {jqXHR} xhr jQuery Ajax object
2598 2674 * @param {String} status Description of response status
2599 2675 * @param {String} error_msg HTTP error message
2600 2676 */
2601 2677 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2602 2678 this.events.trigger('checkpoint_restore_failed.Notebook');
2603 2679 };
2604 2680
2605 2681 /**
2606 2682 * Delete a notebook checkpoint.
2607 2683 *
2608 2684 * @method delete_checkpoint
2609 2685 * @param {String} checkpoint ID
2610 2686 */
2611 2687 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2612 2688 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2613 2689 var url = utils.url_join_encode(
2614 2690 this.base_url,
2615 2691 'api/contents',
2616 2692 this.notebook_path,
2617 2693 this.notebook_name,
2618 2694 'checkpoints',
2619 2695 checkpoint
2620 2696 );
2621 2697 $.ajax(url, {
2622 2698 type: 'DELETE',
2623 2699 success: $.proxy(this.delete_checkpoint_success, this),
2624 2700 error: $.proxy(this.delete_checkpoint_error, this)
2625 2701 });
2626 2702 };
2627 2703
2628 2704 /**
2629 2705 * Success callback for deleting a notebook checkpoint
2630 2706 *
2631 2707 * @method delete_checkpoint_success
2632 2708 * @param {Object} data (ignored, should be empty)
2633 2709 * @param {String} status Description of response status
2634 2710 * @param {jqXHR} xhr jQuery Ajax object
2635 2711 */
2636 2712 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2637 2713 this.events.trigger('checkpoint_deleted.Notebook', data);
2638 2714 this.load_notebook(this.notebook_name, this.notebook_path);
2639 2715 };
2640 2716
2641 2717 /**
2642 2718 * Failure callback for deleting a notebook checkpoint.
2643 2719 *
2644 2720 * @method delete_checkpoint_error
2645 2721 * @param {jqXHR} xhr jQuery Ajax object
2646 2722 * @param {String} status Description of response status
2647 2723 * @param {String} error HTTP error message
2648 2724 */
2649 2725 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error) {
2650 2726 this.events.trigger('checkpoint_delete_failed.Notebook', [xhr, status, error]);
2651 2727 };
2652 2728
2653 2729
2654 2730 // For backwards compatability.
2655 2731 IPython.Notebook = Notebook;
2656 2732
2657 2733 return {'Notebook': Notebook};
2658 2734 });
General Comments 0
You need to be logged in to leave comments. Login now