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