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