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