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