##// END OF EJS Templates
Merge pull request #6809 from minrk/rm-contents-path-name...
Thomas Kluyver -
r18761:39315baa merge
parent child Browse files
Show More
@@ -0,0 +1,61 b''
1
2 import re
3 import nose.tools as nt
4
5 from IPython.html.base.handlers import path_regex, notebook_path_regex
6
7 try: # py3
8 assert_regex = nt.assert_regex
9 assert_not_regex = nt.assert_not_regex
10 except AttributeError: # py2
11 assert_regex = nt.assert_regexp_matches
12 assert_not_regex = nt.assert_not_regexp_matches
13
14
15 # build regexps that tornado uses:
16 path_pat = re.compile('^' + '/x%s' % path_regex + '$')
17 nb_path_pat = re.compile('^' + '/y%s' % notebook_path_regex + '$')
18
19 def test_path_regex():
20 for path in (
21 '/x',
22 '/x/',
23 '/x/foo',
24 '/x/foo.ipynb',
25 '/x/foo/bar',
26 '/x/foo/bar.txt',
27 ):
28 assert_regex(path, path_pat)
29
30 def test_path_regex_bad():
31 for path in (
32 '/xfoo',
33 '/xfoo/',
34 '/xfoo/bar',
35 '/xfoo/bar/',
36 '/x/foo/bar/',
37 '/x//foo',
38 '/y',
39 '/y/x/foo',
40 ):
41 assert_not_regex(path, path_pat)
42
43 def test_notebook_path_regex():
44 for path in (
45 '/y/asdf.ipynb',
46 '/y/foo/bar.ipynb',
47 '/y/a/b/c/d/e.ipynb',
48 ):
49 assert_regex(path, nb_path_pat)
50
51 def test_notebook_path_regex_bad():
52 for path in (
53 '/y',
54 '/y/',
55 '/y/.ipynb',
56 '/y/foo/.ipynb',
57 '/y/foo/bar',
58 '/yfoo.ipynb',
59 '/yfoo/bar.ipynb',
60 ):
61 assert_not_regex(path, nb_path_pat)
@@ -298,7 +298,7 b' class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):'
298 @web.authenticated
298 @web.authenticated
299 def get(self, path):
299 def get(self, path):
300 if os.path.splitext(path)[1] == '.ipynb':
300 if os.path.splitext(path)[1] == '.ipynb':
301 name = os.path.basename(path)
301 name = path.rsplit('/', 1)[-1]
302 self.set_header('Content-Type', 'application/json')
302 self.set_header('Content-Type', 'application/json')
303 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
303 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
304
304
@@ -418,43 +418,42 b' class ApiVersionHandler(IPythonHandler):'
418 # not authenticated, so give as few info as possible
418 # not authenticated, so give as few info as possible
419 self.finish(json.dumps({"version":IPython.__version__}))
419 self.finish(json.dumps({"version":IPython.__version__}))
420
420
421
421 class TrailingSlashHandler(web.RequestHandler):
422 class TrailingSlashHandler(web.RequestHandler):
422 """Simple redirect handler that strips trailing slashes
423 """Simple redirect handler that strips trailing slashes
423
424
424 This should be the first, highest priority handler.
425 This should be the first, highest priority handler.
425 """
426 """
426
427
427 SUPPORTED_METHODS = ['GET']
428
429 def get(self):
428 def get(self):
430 self.redirect(self.request.uri.rstrip('/'))
429 self.redirect(self.request.uri.rstrip('/'))
430
431 post = put = get
431
432
432
433
433 class FilesRedirectHandler(IPythonHandler):
434 class FilesRedirectHandler(IPythonHandler):
434 """Handler for redirecting relative URLs to the /files/ handler"""
435 """Handler for redirecting relative URLs to the /files/ handler"""
435 def get(self, path=''):
436 def get(self, path=''):
436 cm = self.contents_manager
437 cm = self.contents_manager
437 if cm.path_exists(path):
438 if cm.dir_exists(path):
438 # it's a *directory*, redirect to /tree
439 # it's a *directory*, redirect to /tree
439 url = url_path_join(self.base_url, 'tree', path)
440 url = url_path_join(self.base_url, 'tree', path)
440 else:
441 else:
441 orig_path = path
442 orig_path = path
442 # otherwise, redirect to /files
443 # otherwise, redirect to /files
443 parts = path.split('/')
444 parts = path.split('/')
444 path = '/'.join(parts[:-1])
445 name = parts[-1]
446
445
447 if not cm.file_exists(name=name, path=path) and 'files' in parts:
446 if not cm.file_exists(path=path) and 'files' in parts:
448 # redirect without files/ iff it would 404
447 # redirect without files/ iff it would 404
449 # this preserves pre-2.0-style 'files/' links
448 # this preserves pre-2.0-style 'files/' links
450 self.log.warn("Deprecated files/ URL: %s", orig_path)
449 self.log.warn("Deprecated files/ URL: %s", orig_path)
451 parts.remove('files')
450 parts.remove('files')
452 path = '/'.join(parts[:-1])
451 path = '/'.join(parts)
453
452
454 if not cm.file_exists(name=name, path=path):
453 if not cm.file_exists(path=path):
455 raise web.HTTPError(404)
454 raise web.HTTPError(404)
456
455
457 url = url_path_join(self.base_url, 'files', path, name)
456 url = url_path_join(self.base_url, 'files', path)
458 url = url_escape(url)
457 url = url_escape(url)
459 self.log.debug("Redirecting %s to %s", self.request.path, url)
458 self.log.debug("Redirecting %s to %s", self.request.path, url)
460 self.redirect(url)
459 self.redirect(url)
@@ -464,11 +463,9 b' class FilesRedirectHandler(IPythonHandler):'
464 # URL pattern fragments for re-use
463 # URL pattern fragments for re-use
465 #-----------------------------------------------------------------------------
464 #-----------------------------------------------------------------------------
466
465
467 path_regex = r"(?P<path>(?:/.*)*)"
466 # path matches any number of `/foo[/bar...]` or just `/` or ''
468 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
467 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
469 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
468 notebook_path_regex = r"(?P<path>(?:/[^/]+)+\.ipynb)"
470 file_name_regex = r"(?P<name>[^/]+)"
471 file_path_regex = "%s/%s" % (path_regex, file_name_regex)
472
469
473 #-----------------------------------------------------------------------------
470 #-----------------------------------------------------------------------------
474 # URL to handler mappings
471 # URL to handler mappings
@@ -17,13 +17,18 b' class FilesHandler(IPythonHandler):'
17
17
18 @web.authenticated
18 @web.authenticated
19 def get(self, path):
19 def get(self, path):
20 cm = self.settings['contents_manager']
20 cm = self.contents_manager
21 if cm.is_hidden(path):
21 if cm.is_hidden(path):
22 self.log.info("Refusing to serve hidden file, via 404 Error")
22 self.log.info("Refusing to serve hidden file, via 404 Error")
23 raise web.HTTPError(404)
23 raise web.HTTPError(404)
24
24
25 path, name = os.path.split(path)
25 path = path.strip('/')
26 model = cm.get_model(name, path)
26 if '/' in path:
27 _, name = path.rsplit('/', 1)
28 else:
29 name = path
30
31 model = cm.get_model(path)
27
32
28 if self.get_argument("download", False):
33 if self.get_argument("download", False):
29 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
34 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
@@ -76,12 +76,13 b' class NbconvertFileHandler(IPythonHandler):'
76 SUPPORTED_METHODS = ('GET',)
76 SUPPORTED_METHODS = ('GET',)
77
77
78 @web.authenticated
78 @web.authenticated
79 def get(self, format, path='', name=None):
79 def get(self, format, path):
80
80
81 exporter = get_exporter(format, config=self.config, log=self.log)
81 exporter = get_exporter(format, config=self.config, log=self.log)
82
82
83 path = path.strip('/')
83 path = path.strip('/')
84 model = self.contents_manager.get_model(name=name, path=path)
84 model = self.contents_manager.get_model(path=path)
85 name = model['name']
85
86
86 self.set_header('Last-Modified', model['last_modified'])
87 self.set_header('Last-Modified', model['last_modified'])
87
88
@@ -109,7 +110,7 b' class NbconvertFileHandler(IPythonHandler):'
109 class NbconvertPostHandler(IPythonHandler):
110 class NbconvertPostHandler(IPythonHandler):
110 SUPPORTED_METHODS = ('POST',)
111 SUPPORTED_METHODS = ('POST',)
111
112
112 @web.authenticated
113 @web.authenticated
113 def post(self, format):
114 def post(self, format):
114 exporter = get_exporter(format, config=self.config)
115 exporter = get_exporter(format, config=self.config)
115
116
@@ -17,18 +17,16 b' from ..utils import url_escape'
17 class NotebookHandler(IPythonHandler):
17 class NotebookHandler(IPythonHandler):
18
18
19 @web.authenticated
19 @web.authenticated
20 def get(self, path='', name=None):
20 def get(self, path):
21 """get renders the notebook template if a name is given, or
21 """get renders the notebook template if a name is given, or
22 redirects to the '/files/' handler if the name is not given."""
22 redirects to the '/files/' handler if the name is not given."""
23 path = path.strip('/')
23 path = path.strip('/')
24 cm = self.contents_manager
24 cm = self.contents_manager
25 if name is None:
26 raise web.HTTPError(500, "This shouldn't be accessible: %s" % self.request.uri)
27
25
28 # a .ipynb filename was given
26 # a .ipynb filename was given
29 if not cm.file_exists(name, path):
27 if not cm.file_exists(path):
30 raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name))
28 raise web.HTTPError(404, u'Notebook does not exist: %s' % path)
31 name = url_escape(name)
29 name = url_escape(path.rsplit('/', 1)[-1])
32 path = url_escape(path)
30 path = url_escape(path)
33 self.write(self.render_template('notebook.html',
31 self.write(self.render_template('notebook.html',
34 notebook_path=path,
32 notebook_path=path,
@@ -187,9 +187,10 b' class NotebookWebApplication(web.Application):'
187 return settings
187 return settings
188
188
189 def init_handlers(self, settings):
189 def init_handlers(self, settings):
190 # Load the (URL pattern, handler) tuples for each component.
190 """Load the (URL pattern, handler) tuples for each component."""
191
192 # Order matters. The first handler to match the URL will handle the request.
191 handlers = []
193 handlers = []
192 handlers.extend(load_handlers('base.handlers'))
193 handlers.extend(load_handlers('tree.handlers'))
194 handlers.extend(load_handlers('tree.handlers'))
194 handlers.extend(load_handlers('auth.login'))
195 handlers.extend(load_handlers('auth.login'))
195 handlers.extend(load_handlers('auth.logout'))
196 handlers.extend(load_handlers('auth.logout'))
@@ -206,6 +207,8 b' class NotebookWebApplication(web.Application):'
206 handlers.append(
207 handlers.append(
207 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
208 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
208 )
209 )
210 # register base handlers last
211 handlers.extend(load_handlers('base.handlers'))
209 # set the URL that will be redirected from `/`
212 # set the URL that will be redirected from `/`
210 handlers.append(
213 handlers.append(
211 (r'/?', web.RedirectHandler, {
214 (r'/?', web.RedirectHandler, {
@@ -61,27 +61,22 b' class FileContentsManager(ContentsManager):'
61 except OSError as e:
61 except OSError as e:
62 self.log.debug("copystat on %s failed", dest, exc_info=True)
62 self.log.debug("copystat on %s failed", dest, exc_info=True)
63
63
64 def _get_os_path(self, name=None, path=''):
64 def _get_os_path(self, path):
65 """Given a filename and API path, return its file system
65 """Given an API path, return its file system path.
66 path.
67
66
68 Parameters
67 Parameters
69 ----------
68 ----------
70 name : string
71 A filename
72 path : string
69 path : string
73 The relative API path to the named file.
70 The relative API path to the named file.
74
71
75 Returns
72 Returns
76 -------
73 -------
77 path : string
74 path : string
78 API path to be evaluated relative to root_dir.
75 Native, absolute OS path to for a file.
79 """
76 """
80 if name is not None:
81 path = url_path_join(path, name)
82 return to_os_path(path, self.root_dir)
77 return to_os_path(path, self.root_dir)
83
78
84 def path_exists(self, path):
79 def dir_exists(self, path):
85 """Does the API-style path refer to an extant directory?
80 """Does the API-style path refer to an extant directory?
86
81
87 API-style wrapper for os.path.isdir
82 API-style wrapper for os.path.isdir
@@ -112,25 +107,22 b' class FileContentsManager(ContentsManager):'
112
107
113 Returns
108 Returns
114 -------
109 -------
115 exists : bool
110 hidden : bool
116 Whether the path is hidden.
111 Whether the path exists and is hidden.
117
118 """
112 """
119 path = path.strip('/')
113 path = path.strip('/')
120 os_path = self._get_os_path(path=path)
114 os_path = self._get_os_path(path=path)
121 return is_hidden(os_path, self.root_dir)
115 return is_hidden(os_path, self.root_dir)
122
116
123 def file_exists(self, name, path=''):
117 def file_exists(self, path):
124 """Returns True if the file exists, else returns False.
118 """Returns True if the file exists, else returns False.
125
119
126 API-style wrapper for os.path.isfile
120 API-style wrapper for os.path.isfile
127
121
128 Parameters
122 Parameters
129 ----------
123 ----------
130 name : string
131 The name of the file you are checking.
132 path : string
124 path : string
133 The relative path to the file's directory (with '/' as separator)
125 The relative path to the file (with '/' as separator)
134
126
135 Returns
127 Returns
136 -------
128 -------
@@ -138,20 +130,18 b' class FileContentsManager(ContentsManager):'
138 Whether the file exists.
130 Whether the file exists.
139 """
131 """
140 path = path.strip('/')
132 path = path.strip('/')
141 nbpath = self._get_os_path(name, path=path)
133 os_path = self._get_os_path(path)
142 return os.path.isfile(nbpath)
134 return os.path.isfile(os_path)
143
135
144 def exists(self, name=None, path=''):
136 def exists(self, path):
145 """Returns True if the path [and name] exists, else returns False.
137 """Returns True if the path exists, else returns False.
146
138
147 API-style wrapper for os.path.exists
139 API-style wrapper for os.path.exists
148
140
149 Parameters
141 Parameters
150 ----------
142 ----------
151 name : string
152 The name of the file you are checking.
153 path : string
143 path : string
154 The relative path to the file's directory (with '/' as separator)
144 The API path to the file (with '/' as separator)
155
145
156 Returns
146 Returns
157 -------
147 -------
@@ -159,32 +149,31 b' class FileContentsManager(ContentsManager):'
159 Whether the target exists.
149 Whether the target exists.
160 """
150 """
161 path = path.strip('/')
151 path = path.strip('/')
162 os_path = self._get_os_path(name, path=path)
152 os_path = self._get_os_path(path=path)
163 return os.path.exists(os_path)
153 return os.path.exists(os_path)
164
154
165 def _base_model(self, name, path=''):
155 def _base_model(self, path):
166 """Build the common base of a contents model"""
156 """Build the common base of a contents model"""
167 os_path = self._get_os_path(name, path)
157 os_path = self._get_os_path(path)
168 info = os.stat(os_path)
158 info = os.stat(os_path)
169 last_modified = tz.utcfromtimestamp(info.st_mtime)
159 last_modified = tz.utcfromtimestamp(info.st_mtime)
170 created = tz.utcfromtimestamp(info.st_ctime)
160 created = tz.utcfromtimestamp(info.st_ctime)
171 # Create the base model.
161 # Create the base model.
172 model = {}
162 model = {}
173 model['name'] = name
163 model['name'] = path.rsplit('/', 1)[-1]
174 model['path'] = path
164 model['path'] = path
175 model['last_modified'] = last_modified
165 model['last_modified'] = last_modified
176 model['created'] = created
166 model['created'] = created
177 model['content'] = None
167 model['content'] = None
178 model['format'] = None
168 model['format'] = None
179 model['message'] = None
180 return model
169 return model
181
170
182 def _dir_model(self, name, path='', content=True):
171 def _dir_model(self, path, content=True):
183 """Build a model for a directory
172 """Build a model for a directory
184
173
185 if content is requested, will include a listing of the directory
174 if content is requested, will include a listing of the directory
186 """
175 """
187 os_path = self._get_os_path(name, path)
176 os_path = self._get_os_path(path)
188
177
189 four_o_four = u'directory does not exist: %r' % os_path
178 four_o_four = u'directory does not exist: %r' % os_path
190
179
@@ -196,39 +185,43 b' class FileContentsManager(ContentsManager):'
196 )
185 )
197 raise web.HTTPError(404, four_o_four)
186 raise web.HTTPError(404, four_o_four)
198
187
199 if name is None:
188 model = self._base_model(path)
200 if '/' in path:
201 path, name = path.rsplit('/', 1)
202 else:
203 name = ''
204 model = self._base_model(name, path)
205 model['type'] = 'directory'
189 model['type'] = 'directory'
206 dir_path = u'{}/{}'.format(path, name)
207 if content:
190 if content:
208 model['content'] = contents = []
191 model['content'] = contents = []
209 for os_path in glob.glob(self._get_os_path('*', dir_path)):
192 os_dir = self._get_os_path(path)
210 name = os.path.basename(os_path)
193 for name in os.listdir(os_dir):
194 os_path = os.path.join(os_dir, name)
211 # skip over broken symlinks in listing
195 # skip over broken symlinks in listing
212 if not os.path.exists(os_path):
196 if not os.path.exists(os_path):
213 self.log.warn("%s doesn't exist", os_path)
197 self.log.warn("%s doesn't exist", os_path)
214 continue
198 continue
199 elif not os.path.isfile(os_path) and not os.path.isdir(os_path):
200 self.log.debug("%s not a regular file", os_path)
201 continue
215 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
202 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
216 contents.append(self.get_model(name=name, path=dir_path, content=False))
203 contents.append(self.get_model(
204 path='%s/%s' % (path, name),
205 content=False)
206 )
217
207
218 model['format'] = 'json'
208 model['format'] = 'json'
219
209
220 return model
210 return model
221
211
222 def _file_model(self, name, path='', content=True):
212 def _file_model(self, path, content=True):
223 """Build a model for a file
213 """Build a model for a file
224
214
225 if content is requested, include the file contents.
215 if content is requested, include the file contents.
226 UTF-8 text files will be unicode, binary files will be base64-encoded.
216 UTF-8 text files will be unicode, binary files will be base64-encoded.
227 """
217 """
228 model = self._base_model(name, path)
218 model = self._base_model(path)
229 model['type'] = 'file'
219 model['type'] = 'file'
230 if content:
220 if content:
231 os_path = self._get_os_path(name, path)
221 os_path = self._get_os_path(path)
222 if not os.path.isfile(os_path):
223 # could be FIFO
224 raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path)
232 with io.open(os_path, 'rb') as f:
225 with io.open(os_path, 'rb') as f:
233 bcontent = f.read()
226 bcontent = f.read()
234 try:
227 try:
@@ -241,34 +234,32 b' class FileContentsManager(ContentsManager):'
241 return model
234 return model
242
235
243
236
244 def _notebook_model(self, name, path='', content=True):
237 def _notebook_model(self, path, content=True):
245 """Build a notebook model
238 """Build a notebook model
246
239
247 if content is requested, the notebook content will be populated
240 if content is requested, the notebook content will be populated
248 as a JSON structure (not double-serialized)
241 as a JSON structure (not double-serialized)
249 """
242 """
250 model = self._base_model(name, path)
243 model = self._base_model(path)
251 model['type'] = 'notebook'
244 model['type'] = 'notebook'
252 if content:
245 if content:
253 os_path = self._get_os_path(name, path)
246 os_path = self._get_os_path(path)
254 with io.open(os_path, 'r', encoding='utf-8') as f:
247 with io.open(os_path, 'r', encoding='utf-8') as f:
255 try:
248 try:
256 nb = nbformat.read(f, as_version=4)
249 nb = nbformat.read(f, as_version=4)
257 except Exception as e:
250 except Exception as e:
258 raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e))
251 raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e))
259 self.mark_trusted_cells(nb, name, path)
252 self.mark_trusted_cells(nb, path)
260 model['content'] = nb
253 model['content'] = nb
261 model['format'] = 'json'
254 model['format'] = 'json'
262 self.validate_notebook_model(model)
255 self.validate_notebook_model(model)
263 return model
256 return model
264
257
265 def get_model(self, name, path='', content=True):
258 def get_model(self, path, content=True):
266 """ Takes a path and name for an entity and returns its model
259 """ Takes a path for an entity and returns its model
267
260
268 Parameters
261 Parameters
269 ----------
262 ----------
270 name : str
271 the name of the target
272 path : str
263 path : str
273 the API path that describes the relative path for the target
264 the API path that describes the relative path for the target
274
265
@@ -280,32 +271,29 b' class FileContentsManager(ContentsManager):'
280 """
271 """
281 path = path.strip('/')
272 path = path.strip('/')
282
273
283 if not self.exists(name=name, path=path):
274 if not self.exists(path):
284 raise web.HTTPError(404, u'No such file or directory: %s/%s' % (path, name))
275 raise web.HTTPError(404, u'No such file or directory: %s' % path)
285
276
286 os_path = self._get_os_path(name, path)
277 os_path = self._get_os_path(path)
287 if os.path.isdir(os_path):
278 if os.path.isdir(os_path):
288 model = self._dir_model(name, path, content)
279 model = self._dir_model(path, content=content)
289 elif name.endswith('.ipynb'):
280 elif path.endswith('.ipynb'):
290 model = self._notebook_model(name, path, content)
281 model = self._notebook_model(path, content=content)
291 else:
282 else:
292 model = self._file_model(name, path, content)
283 model = self._file_model(path, content=content)
293 return model
284 return model
294
285
295 def _save_notebook(self, os_path, model, name='', path=''):
286 def _save_notebook(self, os_path, model, path=''):
296 """save a notebook file"""
287 """save a notebook file"""
297 # Save the notebook file
288 # Save the notebook file
298 nb = nbformat.from_dict(model['content'])
289 nb = nbformat.from_dict(model['content'])
299
290
300 self.check_and_sign(nb, name, path)
291 self.check_and_sign(nb, path)
301
302 if 'name' in nb['metadata']:
303 nb['metadata']['name'] = u''
304
292
305 with atomic_writing(os_path, encoding='utf-8') as f:
293 with atomic_writing(os_path, encoding='utf-8') as f:
306 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
294 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
307
295
308 def _save_file(self, os_path, model, name='', path=''):
296 def _save_file(self, os_path, model, path=''):
309 """save a non-notebook file"""
297 """save a non-notebook file"""
310 fmt = model.get('format', None)
298 fmt = model.get('format', None)
311 if fmt not in {'text', 'base64'}:
299 if fmt not in {'text', 'base64'}:
@@ -322,7 +310,7 b' class FileContentsManager(ContentsManager):'
322 with atomic_writing(os_path, text=False) as f:
310 with atomic_writing(os_path, text=False) as f:
323 f.write(bcontent)
311 f.write(bcontent)
324
312
325 def _save_directory(self, os_path, model, name='', path=''):
313 def _save_directory(self, os_path, model, path=''):
326 """create a directory"""
314 """create a directory"""
327 if is_hidden(os_path, self.root_dir):
315 if is_hidden(os_path, self.root_dir):
328 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
316 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
@@ -333,7 +321,7 b' class FileContentsManager(ContentsManager):'
333 else:
321 else:
334 self.log.debug("Directory %r already exists", os_path)
322 self.log.debug("Directory %r already exists", os_path)
335
323
336 def save(self, model, name='', path=''):
324 def save(self, model, path=''):
337 """Save the file model and return the model with no content."""
325 """Save the file model and return the model with no content."""
338 path = path.strip('/')
326 path = path.strip('/')
339
327
@@ -343,24 +331,18 b' class FileContentsManager(ContentsManager):'
343 raise web.HTTPError(400, u'No file content provided')
331 raise web.HTTPError(400, u'No file content provided')
344
332
345 # One checkpoint should always exist
333 # One checkpoint should always exist
346 if self.file_exists(name, path) and not self.list_checkpoints(name, path):
334 if self.file_exists(path) and not self.list_checkpoints(path):
347 self.create_checkpoint(name, path)
335 self.create_checkpoint(path)
348
336
349 new_path = model.get('path', path).strip('/')
337 os_path = self._get_os_path(path)
350 new_name = model.get('name', name)
351
352 if path != new_path or name != new_name:
353 self.rename(name, path, new_name, new_path)
354
355 os_path = self._get_os_path(new_name, new_path)
356 self.log.debug("Saving %s", os_path)
338 self.log.debug("Saving %s", os_path)
357 try:
339 try:
358 if model['type'] == 'notebook':
340 if model['type'] == 'notebook':
359 self._save_notebook(os_path, model, new_name, new_path)
341 self._save_notebook(os_path, model, path)
360 elif model['type'] == 'file':
342 elif model['type'] == 'file':
361 self._save_file(os_path, model, new_name, new_path)
343 self._save_file(os_path, model, path)
362 elif model['type'] == 'directory':
344 elif model['type'] == 'directory':
363 self._save_directory(os_path, model, new_name, new_path)
345 self._save_directory(os_path, model, path)
364 else:
346 else:
365 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
347 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
366 except web.HTTPError:
348 except web.HTTPError:
@@ -373,29 +355,28 b' class FileContentsManager(ContentsManager):'
373 self.validate_notebook_model(model)
355 self.validate_notebook_model(model)
374 validation_message = model.get('message', None)
356 validation_message = model.get('message', None)
375
357
376 model = self.get_model(new_name, new_path, content=False)
358 model = self.get_model(path, content=False)
377 if validation_message:
359 if validation_message:
378 model['message'] = validation_message
360 model['message'] = validation_message
379 return model
361 return model
380
362
381 def update(self, model, name, path=''):
363 def update(self, model, path):
382 """Update the file's path and/or name
364 """Update the file's path
383
365
384 For use in PATCH requests, to enable renaming a file without
366 For use in PATCH requests, to enable renaming a file without
385 re-uploading its contents. Only used for renaming at the moment.
367 re-uploading its contents. Only used for renaming at the moment.
386 """
368 """
387 path = path.strip('/')
369 path = path.strip('/')
388 new_name = model.get('name', name)
389 new_path = model.get('path', path).strip('/')
370 new_path = model.get('path', path).strip('/')
390 if path != new_path or name != new_name:
371 if path != new_path:
391 self.rename(name, path, new_name, new_path)
372 self.rename(path, new_path)
392 model = self.get_model(new_name, new_path, content=False)
373 model = self.get_model(new_path, content=False)
393 return model
374 return model
394
375
395 def delete(self, name, path=''):
376 def delete(self, path):
396 """Delete file by name and path."""
377 """Delete file at path."""
397 path = path.strip('/')
378 path = path.strip('/')
398 os_path = self._get_os_path(name, path)
379 os_path = self._get_os_path(path)
399 rm = os.unlink
380 rm = os.unlink
400 if os.path.isdir(os_path):
381 if os.path.isdir(os_path):
401 listing = os.listdir(os_path)
382 listing = os.listdir(os_path)
@@ -406,9 +387,9 b' class FileContentsManager(ContentsManager):'
406 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
387 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
407
388
408 # clear checkpoints
389 # clear checkpoints
409 for checkpoint in self.list_checkpoints(name, path):
390 for checkpoint in self.list_checkpoints(path):
410 checkpoint_id = checkpoint['id']
391 checkpoint_id = checkpoint['id']
411 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
392 cp_path = self.get_checkpoint_path(checkpoint_id, path)
412 if os.path.isfile(cp_path):
393 if os.path.isfile(cp_path):
413 self.log.debug("Unlinking checkpoint %s", cp_path)
394 self.log.debug("Unlinking checkpoint %s", cp_path)
414 os.unlink(cp_path)
395 os.unlink(cp_path)
@@ -420,57 +401,59 b' class FileContentsManager(ContentsManager):'
420 self.log.debug("Unlinking file %s", os_path)
401 self.log.debug("Unlinking file %s", os_path)
421 rm(os_path)
402 rm(os_path)
422
403
423 def rename(self, old_name, old_path, new_name, new_path):
404 def rename(self, old_path, new_path):
424 """Rename a file."""
405 """Rename a file."""
425 old_path = old_path.strip('/')
406 old_path = old_path.strip('/')
426 new_path = new_path.strip('/')
407 new_path = new_path.strip('/')
427 if new_name == old_name and new_path == old_path:
408 if new_path == old_path:
428 return
409 return
429
410
430 new_os_path = self._get_os_path(new_name, new_path)
411 new_os_path = self._get_os_path(new_path)
431 old_os_path = self._get_os_path(old_name, old_path)
412 old_os_path = self._get_os_path(old_path)
432
413
433 # Should we proceed with the move?
414 # Should we proceed with the move?
434 if os.path.isfile(new_os_path):
415 if os.path.exists(new_os_path):
435 raise web.HTTPError(409, u'File with name already exists: %s' % new_os_path)
416 raise web.HTTPError(409, u'File already exists: %s' % new_path)
436
417
437 # Move the file
418 # Move the file
438 try:
419 try:
439 shutil.move(old_os_path, new_os_path)
420 shutil.move(old_os_path, new_os_path)
440 except Exception as e:
421 except Exception as e:
441 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_os_path, e))
422 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
442
423
443 # Move the checkpoints
424 # Move the checkpoints
444 old_checkpoints = self.list_checkpoints(old_name, old_path)
425 old_checkpoints = self.list_checkpoints(old_path)
445 for cp in old_checkpoints:
426 for cp in old_checkpoints:
446 checkpoint_id = cp['id']
427 checkpoint_id = cp['id']
447 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
428 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path)
448 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
429 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_path)
449 if os.path.isfile(old_cp_path):
430 if os.path.isfile(old_cp_path):
450 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
431 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
451 shutil.move(old_cp_path, new_cp_path)
432 shutil.move(old_cp_path, new_cp_path)
452
433
453 # Checkpoint-related utilities
434 # Checkpoint-related utilities
454
435
455 def get_checkpoint_path(self, checkpoint_id, name, path=''):
436 def get_checkpoint_path(self, checkpoint_id, path):
456 """find the path to a checkpoint"""
437 """find the path to a checkpoint"""
457 path = path.strip('/')
438 path = path.strip('/')
439 parent, name = ('/' + path).rsplit('/', 1)
440 parent = parent.strip('/')
458 basename, ext = os.path.splitext(name)
441 basename, ext = os.path.splitext(name)
459 filename = u"{name}-{checkpoint_id}{ext}".format(
442 filename = u"{name}-{checkpoint_id}{ext}".format(
460 name=basename,
443 name=basename,
461 checkpoint_id=checkpoint_id,
444 checkpoint_id=checkpoint_id,
462 ext=ext,
445 ext=ext,
463 )
446 )
464 os_path = self._get_os_path(path=path)
447 os_path = self._get_os_path(path=parent)
465 cp_dir = os.path.join(os_path, self.checkpoint_dir)
448 cp_dir = os.path.join(os_path, self.checkpoint_dir)
466 ensure_dir_exists(cp_dir)
449 ensure_dir_exists(cp_dir)
467 cp_path = os.path.join(cp_dir, filename)
450 cp_path = os.path.join(cp_dir, filename)
468 return cp_path
451 return cp_path
469
452
470 def get_checkpoint_model(self, checkpoint_id, name, path=''):
453 def get_checkpoint_model(self, checkpoint_id, path):
471 """construct the info dict for a given checkpoint"""
454 """construct the info dict for a given checkpoint"""
472 path = path.strip('/')
455 path = path.strip('/')
473 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
456 cp_path = self.get_checkpoint_path(checkpoint_id, path)
474 stats = os.stat(cp_path)
457 stats = os.stat(cp_path)
475 last_modified = tz.utcfromtimestamp(stats.st_mtime)
458 last_modified = tz.utcfromtimestamp(stats.st_mtime)
476 info = dict(
459 info = dict(
@@ -481,43 +464,45 b' class FileContentsManager(ContentsManager):'
481
464
482 # public checkpoint API
465 # public checkpoint API
483
466
484 def create_checkpoint(self, name, path=''):
467 def create_checkpoint(self, path):
485 """Create a checkpoint from the current state of a file"""
468 """Create a checkpoint from the current state of a file"""
486 path = path.strip('/')
469 path = path.strip('/')
487 src_path = self._get_os_path(name, path)
470 if not self.file_exists(path):
471 raise web.HTTPError(404)
472 src_path = self._get_os_path(path)
488 # only the one checkpoint ID:
473 # only the one checkpoint ID:
489 checkpoint_id = u"checkpoint"
474 checkpoint_id = u"checkpoint"
490 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
475 cp_path = self.get_checkpoint_path(checkpoint_id, path)
491 self.log.debug("creating checkpoint for %s", name)
476 self.log.debug("creating checkpoint for %s", path)
492 self._copy(src_path, cp_path)
477 self._copy(src_path, cp_path)
493
478
494 # return the checkpoint info
479 # return the checkpoint info
495 return self.get_checkpoint_model(checkpoint_id, name, path)
480 return self.get_checkpoint_model(checkpoint_id, path)
496
481
497 def list_checkpoints(self, name, path=''):
482 def list_checkpoints(self, path):
498 """list the checkpoints for a given file
483 """list the checkpoints for a given file
499
484
500 This contents manager currently only supports one checkpoint per file.
485 This contents manager currently only supports one checkpoint per file.
501 """
486 """
502 path = path.strip('/')
487 path = path.strip('/')
503 checkpoint_id = "checkpoint"
488 checkpoint_id = "checkpoint"
504 os_path = self.get_checkpoint_path(checkpoint_id, name, path)
489 os_path = self.get_checkpoint_path(checkpoint_id, path)
505 if not os.path.exists(os_path):
490 if not os.path.exists(os_path):
506 return []
491 return []
507 else:
492 else:
508 return [self.get_checkpoint_model(checkpoint_id, name, path)]
493 return [self.get_checkpoint_model(checkpoint_id, path)]
509
494
510
495
511 def restore_checkpoint(self, checkpoint_id, name, path=''):
496 def restore_checkpoint(self, checkpoint_id, path):
512 """restore a file to a checkpointed state"""
497 """restore a file to a checkpointed state"""
513 path = path.strip('/')
498 path = path.strip('/')
514 self.log.info("restoring %s from checkpoint %s", name, checkpoint_id)
499 self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
515 nb_path = self._get_os_path(name, path)
500 nb_path = self._get_os_path(path)
516 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
501 cp_path = self.get_checkpoint_path(checkpoint_id, path)
517 if not os.path.isfile(cp_path):
502 if not os.path.isfile(cp_path):
518 self.log.debug("checkpoint file does not exist: %s", cp_path)
503 self.log.debug("checkpoint file does not exist: %s", cp_path)
519 raise web.HTTPError(404,
504 raise web.HTTPError(404,
520 u'checkpoint does not exist: %s-%s' % (name, checkpoint_id)
505 u'checkpoint does not exist: %s@%s' % (path, checkpoint_id)
521 )
506 )
522 # ensure notebook is readable (never restore from an unreadable notebook)
507 # ensure notebook is readable (never restore from an unreadable notebook)
523 if cp_path.endswith('.ipynb'):
508 if cp_path.endswith('.ipynb'):
@@ -526,13 +511,13 b' class FileContentsManager(ContentsManager):'
526 self._copy(cp_path, nb_path)
511 self._copy(cp_path, nb_path)
527 self.log.debug("copying %s -> %s", cp_path, nb_path)
512 self.log.debug("copying %s -> %s", cp_path, nb_path)
528
513
529 def delete_checkpoint(self, checkpoint_id, name, path=''):
514 def delete_checkpoint(self, checkpoint_id, path):
530 """delete a file's checkpoint"""
515 """delete a file's checkpoint"""
531 path = path.strip('/')
516 path = path.strip('/')
532 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
517 cp_path = self.get_checkpoint_path(checkpoint_id, path)
533 if not os.path.isfile(cp_path):
518 if not os.path.isfile(cp_path):
534 raise web.HTTPError(404,
519 raise web.HTTPError(404,
535 u'Checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
520 u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id)
536 )
521 )
537 self.log.debug("unlinking %s", cp_path)
522 self.log.debug("unlinking %s", cp_path)
538 os.unlink(cp_path)
523 os.unlink(cp_path)
@@ -540,6 +525,10 b' class FileContentsManager(ContentsManager):'
540 def info_string(self):
525 def info_string(self):
541 return "Serving notebooks from local directory: %s" % self.root_dir
526 return "Serving notebooks from local directory: %s" % self.root_dir
542
527
543 def get_kernel_path(self, name, path='', model=None):
528 def get_kernel_path(self, path, model=None):
544 """Return the initial working dir a kernel associated with a given notebook"""
529 """Return the initial working dir a kernel associated with a given notebook"""
545 return os.path.join(self.root_dir, path)
530 if '/' in path:
531 parent_dir = path.rsplit('/', 1)[0]
532 else:
533 parent_dir = ''
534 return self._get_os_path(parent_dir)
@@ -10,9 +10,9 b' from tornado import web'
10 from IPython.html.utils import url_path_join, url_escape
10 from IPython.html.utils import url_path_join, url_escape
11 from IPython.utils.jsonutil import date_default
11 from IPython.utils.jsonutil import date_default
12
12
13 from IPython.html.base.handlers import (IPythonHandler, json_errors,
13 from IPython.html.base.handlers import (
14 file_path_regex, path_regex,
14 IPythonHandler, json_errors, path_regex,
15 file_name_regex)
15 )
16
16
17
17
18 def sort_key(model):
18 def sort_key(model):
@@ -29,38 +29,36 b' class ContentsHandler(IPythonHandler):'
29
29
30 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
30 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
31
31
32 def location_url(self, name, path):
32 def location_url(self, path):
33 """Return the full URL location of a file.
33 """Return the full URL location of a file.
34
34
35 Parameters
35 Parameters
36 ----------
36 ----------
37 name : unicode
38 The base name of the file, such as "foo.ipynb".
39 path : unicode
37 path : unicode
40 The API path of the file, such as "foo/bar".
38 The API path of the file, such as "foo/bar.txt".
41 """
39 """
42 return url_escape(url_path_join(
40 return url_escape(url_path_join(
43 self.base_url, 'api', 'contents', path, name
41 self.base_url, 'api', 'contents', path
44 ))
42 ))
45
43
46 def _finish_model(self, model, location=True):
44 def _finish_model(self, model, location=True):
47 """Finish a JSON request with a model, setting relevant headers, etc."""
45 """Finish a JSON request with a model, setting relevant headers, etc."""
48 if location:
46 if location:
49 location = self.location_url(model['name'], model['path'])
47 location = self.location_url(model['path'])
50 self.set_header('Location', location)
48 self.set_header('Location', location)
51 self.set_header('Last-Modified', model['last_modified'])
49 self.set_header('Last-Modified', model['last_modified'])
52 self.finish(json.dumps(model, default=date_default))
50 self.finish(json.dumps(model, default=date_default))
53
51
54 @web.authenticated
52 @web.authenticated
55 @json_errors
53 @json_errors
56 def get(self, path='', name=None):
54 def get(self, path=''):
57 """Return a model for a file or directory.
55 """Return a model for a file or directory.
58
56
59 A directory model contains a list of models (without content)
57 A directory model contains a list of models (without content)
60 of the files and directories it contains.
58 of the files and directories it contains.
61 """
59 """
62 path = path or ''
60 path = path or ''
63 model = self.contents_manager.get_model(name=name, path=path)
61 model = self.contents_manager.get_model(path=path)
64 if model['type'] == 'directory':
62 if model['type'] == 'directory':
65 # group listing by type, then by name (case-insensitive)
63 # group listing by type, then by name (case-insensitive)
66 # FIXME: sorting should be done in the frontends
64 # FIXME: sorting should be done in the frontends
@@ -69,112 +67,83 b' class ContentsHandler(IPythonHandler):'
69
67
70 @web.authenticated
68 @web.authenticated
71 @json_errors
69 @json_errors
72 def patch(self, path='', name=None):
70 def patch(self, path=''):
73 """PATCH renames a notebook without re-uploading content."""
71 """PATCH renames a file or directory without re-uploading content."""
74 cm = self.contents_manager
72 cm = self.contents_manager
75 if name is None:
76 raise web.HTTPError(400, u'Filename missing')
77 model = self.get_json_body()
73 model = self.get_json_body()
78 if model is None:
74 if model is None:
79 raise web.HTTPError(400, u'JSON body missing')
75 raise web.HTTPError(400, u'JSON body missing')
80 model = cm.update(model, name, path)
76 model = cm.update(model, path)
81 self._finish_model(model)
77 self._finish_model(model)
82
78
83 def _copy(self, copy_from, path, copy_to=None):
79 def _copy(self, copy_from, copy_to=None):
84 """Copy a file, optionally specifying the new name.
80 """Copy a file, optionally specifying a target directory."""
85 """
81 self.log.info(u"Copying {copy_from} to {copy_to}".format(
86 self.log.info(u"Copying {copy_from} to {path}/{copy_to}".format(
87 copy_from=copy_from,
82 copy_from=copy_from,
88 path=path,
89 copy_to=copy_to or '',
83 copy_to=copy_to or '',
90 ))
84 ))
91 model = self.contents_manager.copy(copy_from, copy_to, path)
85 model = self.contents_manager.copy(copy_from, copy_to)
92 self.set_status(201)
86 self.set_status(201)
93 self._finish_model(model)
87 self._finish_model(model)
94
88
95 def _upload(self, model, path, name=None):
89 def _upload(self, model, path):
96 """Handle upload of a new file
90 """Handle upload of a new file to path"""
97
91 self.log.info(u"Uploading file to %s", path)
98 If name specified, create it in path/name,
92 model = self.contents_manager.new(model, path)
99 otherwise create a new untitled file in path.
100 """
101 self.log.info(u"Uploading file to %s/%s", path, name or '')
102 if name:
103 model['name'] = name
104
105 model = self.contents_manager.create_file(model, path)
106 self.set_status(201)
93 self.set_status(201)
107 self._finish_model(model)
94 self._finish_model(model)
108
95
109 def _create_empty_file(self, path, name=None, ext='.ipynb'):
96 def _new_untitled(self, path, type='', ext=''):
110 """Create an empty file in path
97 """Create a new, empty untitled entity"""
111
98 self.log.info(u"Creating new %s in %s", type or 'file', path)
112 If name specified, create it in path/name.
99 model = self.contents_manager.new_untitled(path=path, type=type, ext=ext)
113 """
114 self.log.info(u"Creating new file in %s/%s", path, name or '')
115 model = {}
116 if name:
117 model['name'] = name
118 model = self.contents_manager.create_file(model, path=path, ext=ext)
119 self.set_status(201)
100 self.set_status(201)
120 self._finish_model(model)
101 self._finish_model(model)
121
102
122 def _save(self, model, path, name):
103 def _save(self, model, path):
123 """Save an existing file."""
104 """Save an existing file."""
124 self.log.info(u"Saving file at %s/%s", path, name)
105 self.log.info(u"Saving file at %s", path)
125 model = self.contents_manager.save(model, name, path)
106 model = self.contents_manager.save(model, path)
126 if model['path'] != path.strip('/') or model['name'] != name:
107 self._finish_model(model)
127 # a rename happened, set Location header
128 location = True
129 else:
130 location = False
131 self._finish_model(model, location)
132
108
133 @web.authenticated
109 @web.authenticated
134 @json_errors
110 @json_errors
135 def post(self, path='', name=None):
111 def post(self, path=''):
136 """Create a new file or directory in the specified path.
112 """Create a new file in the specified path.
137
113
138 POST creates new files or directories. The server always decides on the name.
114 POST creates new files. The server always decides on the name.
139
115
140 POST /api/contents/path
116 POST /api/contents/path
141 New untitled notebook in path. If content specified, upload a
117 New untitled, empty file or directory.
142 notebook, otherwise start empty.
143 POST /api/contents/path
118 POST /api/contents/path
144 with body {"copy_from" : "OtherNotebook.ipynb"}
119 with body {"copy_from" : "/path/to/OtherNotebook.ipynb"}
145 New copy of OtherNotebook in path
120 New copy of OtherNotebook in path
146 """
121 """
147
122
148 if name is not None:
149 path = u'{}/{}'.format(path, name)
150
151 cm = self.contents_manager
123 cm = self.contents_manager
152
124
153 if cm.file_exists(path):
125 if cm.file_exists(path):
154 raise web.HTTPError(400, "Cannot POST to existing files, use PUT instead.")
126 raise web.HTTPError(400, "Cannot POST to files, use PUT instead.")
155
127
156 if not cm.path_exists(path):
128 if not cm.dir_exists(path):
157 raise web.HTTPError(404, "No such directory: %s" % path)
129 raise web.HTTPError(404, "No such directory: %s" % path)
158
130
159 model = self.get_json_body()
131 model = self.get_json_body()
160
132
161 if model is not None:
133 if model is not None:
162 copy_from = model.get('copy_from')
134 copy_from = model.get('copy_from')
163 ext = model.get('ext', '.ipynb')
135 ext = model.get('ext', '')
164 if model.get('content') is not None:
136 type = model.get('type', '')
165 if copy_from:
137 if copy_from:
166 raise web.HTTPError(400, "Can't upload and copy at the same time.")
167 self._upload(model, path)
168 elif copy_from:
169 self._copy(copy_from, path)
138 self._copy(copy_from, path)
170 else:
139 else:
171 self._create_empty_file(path, ext=ext)
140 self._new_untitled(path, type=type, ext=ext)
172 else:
141 else:
173 self._create_empty_file(path)
142 self._new_untitled(path)
174
143
175 @web.authenticated
144 @web.authenticated
176 @json_errors
145 @json_errors
177 def put(self, path='', name=None):
146 def put(self, path=''):
178 """Saves the file in the location specified by name and path.
147 """Saves the file in the location specified by name and path.
179
148
180 PUT is very similar to POST, but the requester specifies the name,
149 PUT is very similar to POST, but the requester specifies the name,
@@ -184,39 +153,25 b' class ContentsHandler(IPythonHandler):'
184 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
153 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
185 in `content` key of JSON request body. If content is not specified,
154 in `content` key of JSON request body. If content is not specified,
186 create a new empty notebook.
155 create a new empty notebook.
187 PUT /api/contents/path/Name.ipynb
188 with JSON body::
189
190 {
191 "copy_from" : "[path/to/]OtherNotebook.ipynb"
192 }
193
194 Copy OtherNotebook to Name
195 """
156 """
196 if name is None:
197 raise web.HTTPError(400, "name must be specified with PUT.")
198
199 model = self.get_json_body()
157 model = self.get_json_body()
200 if model:
158 if model:
201 copy_from = model.get('copy_from')
159 if model.get('copy_from'):
202 if copy_from:
160 raise web.HTTPError(400, "Cannot copy with PUT, only POST")
203 if model.get('content'):
161 if self.contents_manager.file_exists(path):
204 raise web.HTTPError(400, "Can't upload and copy at the same time.")
162 self._save(model, path)
205 self._copy(copy_from, path, name)
206 elif self.contents_manager.file_exists(name, path):
207 self._save(model, path, name)
208 else:
163 else:
209 self._upload(model, path, name)
164 self._upload(model, path)
210 else:
165 else:
211 self._create_empty_file(path, name)
166 self._new_untitled(path)
212
167
213 @web.authenticated
168 @web.authenticated
214 @json_errors
169 @json_errors
215 def delete(self, path='', name=None):
170 def delete(self, path=''):
216 """delete a file in the given path"""
171 """delete a file in the given path"""
217 cm = self.contents_manager
172 cm = self.contents_manager
218 self.log.warn('delete %s:%s', path, name)
173 self.log.warn('delete %s', path)
219 cm.delete(name, path)
174 cm.delete(path)
220 self.set_status(204)
175 self.set_status(204)
221 self.finish()
176 self.finish()
222
177
@@ -227,22 +182,22 b' class CheckpointsHandler(IPythonHandler):'
227
182
228 @web.authenticated
183 @web.authenticated
229 @json_errors
184 @json_errors
230 def get(self, path='', name=None):
185 def get(self, path=''):
231 """get lists checkpoints for a file"""
186 """get lists checkpoints for a file"""
232 cm = self.contents_manager
187 cm = self.contents_manager
233 checkpoints = cm.list_checkpoints(name, path)
188 checkpoints = cm.list_checkpoints(path)
234 data = json.dumps(checkpoints, default=date_default)
189 data = json.dumps(checkpoints, default=date_default)
235 self.finish(data)
190 self.finish(data)
236
191
237 @web.authenticated
192 @web.authenticated
238 @json_errors
193 @json_errors
239 def post(self, path='', name=None):
194 def post(self, path=''):
240 """post creates a new checkpoint"""
195 """post creates a new checkpoint"""
241 cm = self.contents_manager
196 cm = self.contents_manager
242 checkpoint = cm.create_checkpoint(name, path)
197 checkpoint = cm.create_checkpoint(path)
243 data = json.dumps(checkpoint, default=date_default)
198 data = json.dumps(checkpoint, default=date_default)
244 location = url_path_join(self.base_url, 'api/contents',
199 location = url_path_join(self.base_url, 'api/contents',
245 path, name, 'checkpoints', checkpoint['id'])
200 path, 'checkpoints', checkpoint['id'])
246 self.set_header('Location', url_escape(location))
201 self.set_header('Location', url_escape(location))
247 self.set_status(201)
202 self.set_status(201)
248 self.finish(data)
203 self.finish(data)
@@ -254,19 +209,19 b' class ModifyCheckpointsHandler(IPythonHandler):'
254
209
255 @web.authenticated
210 @web.authenticated
256 @json_errors
211 @json_errors
257 def post(self, path, name, checkpoint_id):
212 def post(self, path, checkpoint_id):
258 """post restores a file from a checkpoint"""
213 """post restores a file from a checkpoint"""
259 cm = self.contents_manager
214 cm = self.contents_manager
260 cm.restore_checkpoint(checkpoint_id, name, path)
215 cm.restore_checkpoint(checkpoint_id, path)
261 self.set_status(204)
216 self.set_status(204)
262 self.finish()
217 self.finish()
263
218
264 @web.authenticated
219 @web.authenticated
265 @json_errors
220 @json_errors
266 def delete(self, path, name, checkpoint_id):
221 def delete(self, path, checkpoint_id):
267 """delete clears a checkpoint for a given file"""
222 """delete clears a checkpoint for a given file"""
268 cm = self.contents_manager
223 cm = self.contents_manager
269 cm.delete_checkpoint(checkpoint_id, name, path)
224 cm.delete_checkpoint(checkpoint_id, path)
270 self.set_status(204)
225 self.set_status(204)
271 self.finish()
226 self.finish()
272
227
@@ -294,10 +249,9 b' class NotebooksRedirectHandler(IPythonHandler):'
294 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
249 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
295
250
296 default_handlers = [
251 default_handlers = [
297 (r"/api/contents%s/checkpoints" % file_path_regex, CheckpointsHandler),
252 (r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler),
298 (r"/api/contents%s/checkpoints/%s" % (file_path_regex, _checkpoint_id_regex),
253 (r"/api/contents%s/checkpoints/%s" % (path_regex, _checkpoint_id_regex),
299 ModifyCheckpointsHandler),
254 ModifyCheckpointsHandler),
300 (r"/api/contents%s" % file_path_regex, ContentsHandler),
301 (r"/api/contents%s" % path_regex, ContentsHandler),
255 (r"/api/contents%s" % path_regex, ContentsHandler),
302 (r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
256 (r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
303 ]
257 ]
@@ -33,14 +33,6 b' class ContentsManager(LoggingConfigurable):'
33 - if unspecified, path defaults to '',
33 - if unspecified, path defaults to '',
34 indicating the root path.
34 indicating the root path.
35
35
36 name is also unicode, and refers to a specfic target:
37
38 - unicode, not url-escaped
39 - must not contain '/'
40 - It refers to an individual filename
41 - It may refer to a directory name,
42 in the case of listing or creating directories.
43
44 """
36 """
45
37
46 notary = Instance(sign.NotebookNotary)
38 notary = Instance(sign.NotebookNotary)
@@ -69,7 +61,7 b' class ContentsManager(LoggingConfigurable):'
69 # ContentsManager API part 1: methods that must be
61 # ContentsManager API part 1: methods that must be
70 # implemented in subclasses.
62 # implemented in subclasses.
71
63
72 def path_exists(self, path):
64 def dir_exists(self, path):
73 """Does the API-style path (directory) actually exist?
65 """Does the API-style path (directory) actually exist?
74
66
75 Like os.path.isdir
67 Like os.path.isdir
@@ -105,8 +97,8 b' class ContentsManager(LoggingConfigurable):'
105 """
97 """
106 raise NotImplementedError
98 raise NotImplementedError
107
99
108 def file_exists(self, name, path=''):
100 def file_exists(self, path=''):
109 """Does a file exist at the given name and path?
101 """Does a file exist at the given path?
110
102
111 Like os.path.isfile
103 Like os.path.isfile
112
104
@@ -126,15 +118,13 b' class ContentsManager(LoggingConfigurable):'
126 """
118 """
127 raise NotImplementedError('must be implemented in a subclass')
119 raise NotImplementedError('must be implemented in a subclass')
128
120
129 def exists(self, name, path=''):
121 def exists(self, path):
130 """Does a file or directory exist at the given name and path?
122 """Does a file or directory exist at the given path?
131
123
132 Like os.path.exists
124 Like os.path.exists
133
125
134 Parameters
126 Parameters
135 ----------
127 ----------
136 name : string
137 The name of the file you are checking.
138 path : string
128 path : string
139 The relative path to the file's directory (with '/' as separator)
129 The relative path to the file's directory (with '/' as separator)
140
130
@@ -143,17 +133,17 b' class ContentsManager(LoggingConfigurable):'
143 exists : bool
133 exists : bool
144 Whether the target exists.
134 Whether the target exists.
145 """
135 """
146 return self.file_exists(name, path) or self.path_exists("%s/%s" % (path, name))
136 return self.file_exists(path) or self.dir_exists(path)
147
137
148 def get_model(self, name, path='', content=True):
138 def get_model(self, path, content=True):
149 """Get the model of a file or directory with or without content."""
139 """Get the model of a file or directory with or without content."""
150 raise NotImplementedError('must be implemented in a subclass')
140 raise NotImplementedError('must be implemented in a subclass')
151
141
152 def save(self, model, name, path=''):
142 def save(self, model, path):
153 """Save the file or directory and return the model with no content."""
143 """Save the file or directory and return the model with no content."""
154 raise NotImplementedError('must be implemented in a subclass')
144 raise NotImplementedError('must be implemented in a subclass')
155
145
156 def update(self, model, name, path=''):
146 def update(self, model, path):
157 """Update the file or directory and return the model with no content.
147 """Update the file or directory and return the model with no content.
158
148
159 For use in PATCH requests, to enable renaming a file without
149 For use in PATCH requests, to enable renaming a file without
@@ -161,26 +151,26 b' class ContentsManager(LoggingConfigurable):'
161 """
151 """
162 raise NotImplementedError('must be implemented in a subclass')
152 raise NotImplementedError('must be implemented in a subclass')
163
153
164 def delete(self, name, path=''):
154 def delete(self, path):
165 """Delete file or directory by name and path."""
155 """Delete file or directory by path."""
166 raise NotImplementedError('must be implemented in a subclass')
156 raise NotImplementedError('must be implemented in a subclass')
167
157
168 def create_checkpoint(self, name, path=''):
158 def create_checkpoint(self, path):
169 """Create a checkpoint of the current state of a file
159 """Create a checkpoint of the current state of a file
170
160
171 Returns a checkpoint_id for the new checkpoint.
161 Returns a checkpoint_id for the new checkpoint.
172 """
162 """
173 raise NotImplementedError("must be implemented in a subclass")
163 raise NotImplementedError("must be implemented in a subclass")
174
164
175 def list_checkpoints(self, name, path=''):
165 def list_checkpoints(self, path):
176 """Return a list of checkpoints for a given file"""
166 """Return a list of checkpoints for a given file"""
177 return []
167 return []
178
168
179 def restore_checkpoint(self, checkpoint_id, name, path=''):
169 def restore_checkpoint(self, checkpoint_id, path):
180 """Restore a file from one of its checkpoints"""
170 """Restore a file from one of its checkpoints"""
181 raise NotImplementedError("must be implemented in a subclass")
171 raise NotImplementedError("must be implemented in a subclass")
182
172
183 def delete_checkpoint(self, checkpoint_id, name, path=''):
173 def delete_checkpoint(self, checkpoint_id, path):
184 """delete a checkpoint for a file"""
174 """delete a checkpoint for a file"""
185 raise NotImplementedError("must be implemented in a subclass")
175 raise NotImplementedError("must be implemented in a subclass")
186
176
@@ -190,8 +180,12 b' class ContentsManager(LoggingConfigurable):'
190 def info_string(self):
180 def info_string(self):
191 return "Serving contents"
181 return "Serving contents"
192
182
193 def get_kernel_path(self, name, path='', model=None):
183 def get_kernel_path(self, path, model=None):
194 """ Return the path to start kernel in """
184 """Return the API path for the kernel
185
186 KernelManagers can turn this value into a filesystem path,
187 or ignore it altogether.
188 """
195 return path
189 return path
196
190
197 def increment_filename(self, filename, path=''):
191 def increment_filename(self, filename, path=''):
@@ -214,7 +208,7 b' class ContentsManager(LoggingConfigurable):'
214 for i in itertools.count():
208 for i in itertools.count():
215 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
209 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
216 ext=ext)
210 ext=ext)
217 if not self.file_exists(name, path):
211 if not self.exists(u'{}/{}'.format(path, name)):
218 break
212 break
219 return name
213 return name
220
214
@@ -223,85 +217,124 b' class ContentsManager(LoggingConfigurable):'
223 try:
217 try:
224 validate(model['content'])
218 validate(model['content'])
225 except ValidationError as e:
219 except ValidationError as e:
226 model['message'] = 'Notebook Validation failed: {}:\n{}'.format(
220 model['message'] = u'Notebook Validation failed: {}:\n{}'.format(
227 e.message, json.dumps(e.instance, indent=1, default=lambda obj: '<UNKNOWN>'),
221 e.message, json.dumps(e.instance, indent=1, default=lambda obj: '<UNKNOWN>'),
228 )
222 )
229 return model
223 return model
230
224
231 def create_file(self, model=None, path='', ext='.ipynb'):
225 def new_untitled(self, path='', type='', ext=''):
232 """Create a new file or directory and return its model with no content."""
226 """Create a new untitled file or directory in path
227
228 path must be a directory
229
230 File extension can be specified.
231
232 Use `new` to create files with a fully specified path (including filename).
233 """
234 path = path.strip('/')
235 if not self.dir_exists(path):
236 raise HTTPError(404, 'No such directory: %s' % path)
237
238 model = {}
239 if type:
240 model['type'] = type
241
242 if ext == '.ipynb':
243 model.setdefault('type', 'notebook')
244 else:
245 model.setdefault('type', 'file')
246
247 if model['type'] == 'directory':
248 untitled = self.untitled_directory
249 elif model['type'] == 'notebook':
250 untitled = self.untitled_notebook
251 ext = '.ipynb'
252 elif model['type'] == 'file':
253 untitled = self.untitled_file
254 else:
255 raise HTTPError(400, "Unexpected model type: %r" % model['type'])
256
257 name = self.increment_filename(untitled + ext, path)
258 path = u'{0}/{1}'.format(path, name)
259 return self.new(model, path)
260
261 def new(self, model=None, path=''):
262 """Create a new file or directory and return its model with no content.
263
264 To create a new untitled entity in a directory, use `new_untitled`.
265 """
233 path = path.strip('/')
266 path = path.strip('/')
234 if model is None:
267 if model is None:
235 model = {}
268 model = {}
236 if 'content' not in model and model.get('type', None) != 'directory':
269
237 if ext == '.ipynb':
270 if path.endswith('.ipynb'):
271 model.setdefault('type', 'notebook')
272 else:
273 model.setdefault('type', 'file')
274
275 # no content, not a directory, so fill out new-file model
276 if 'content' not in model and model['type'] != 'directory':
277 if model['type'] == 'notebook':
238 model['content'] = new_notebook()
278 model['content'] = new_notebook()
239 model['type'] = 'notebook'
240 model['format'] = 'json'
279 model['format'] = 'json'
241 else:
280 else:
242 model['content'] = ''
281 model['content'] = ''
243 model['type'] = 'file'
282 model['type'] = 'file'
244 model['format'] = 'text'
283 model['format'] = 'text'
245 if 'name' not in model:
284
246 if model['type'] == 'directory':
285 model = self.save(model, path)
247 untitled = self.untitled_directory
248 elif model['type'] == 'notebook':
249 untitled = self.untitled_notebook
250 elif model['type'] == 'file':
251 untitled = self.untitled_file
252 else:
253 raise HTTPError(400, "Unexpected model type: %r" % model['type'])
254 model['name'] = self.increment_filename(untitled + ext, path)
255
256 model['path'] = path
257 model = self.save(model, model['name'], model['path'])
258 return model
286 return model
259
287
260 def copy(self, from_name, to_name=None, path=''):
288 def copy(self, from_path, to_path=None):
261 """Copy an existing file and return its new model.
289 """Copy an existing file and return its new model.
262
290
263 If to_name not specified, increment `from_name-Copy#.ext`.
291 If to_path not specified, it will be the parent directory of from_path.
292 If to_path is a directory, filename will increment `from_path-Copy#.ext`.
264
293
265 copy_from can be a full path to a file,
294 from_path must be a full path to a file.
266 or just a base name. If a base name, `path` is used.
267 """
295 """
268 path = path.strip('/')
296 path = from_path.strip('/')
269 if '/' in from_name:
297 if '/' in path:
270 from_path, from_name = from_name.rsplit('/', 1)
298 from_dir, from_name = path.rsplit('/', 1)
271 else:
299 else:
272 from_path = path
300 from_dir = ''
273 model = self.get_model(from_name, from_path)
301 from_name = path
302
303 model = self.get_model(path)
304 model.pop('path', None)
305 model.pop('name', None)
274 if model['type'] == 'directory':
306 if model['type'] == 'directory':
275 raise HTTPError(400, "Can't copy directories")
307 raise HTTPError(400, "Can't copy directories")
276 if not to_name:
308
309 if not to_path:
310 to_path = from_dir
311 if self.dir_exists(to_path):
277 base, ext = os.path.splitext(from_name)
312 base, ext = os.path.splitext(from_name)
278 copy_name = u'{0}-Copy{1}'.format(base, ext)
313 copy_name = u'{0}-Copy{1}'.format(base, ext)
279 to_name = self.increment_filename(copy_name, path)
314 to_name = self.increment_filename(copy_name, to_path)
280 model['name'] = to_name
315 to_path = u'{0}/{1}'.format(to_path, to_name)
281 model['path'] = path
316
282 model = self.save(model, to_name, path)
317 model = self.save(model, to_path)
283 return model
318 return model
284
319
285 def log_info(self):
320 def log_info(self):
286 self.log.info(self.info_string())
321 self.log.info(self.info_string())
287
322
288 def trust_notebook(self, name, path=''):
323 def trust_notebook(self, path):
289 """Explicitly trust a notebook
324 """Explicitly trust a notebook
290
325
291 Parameters
326 Parameters
292 ----------
327 ----------
293 name : string
294 The filename of the notebook
295 path : string
328 path : string
296 The notebook's directory
329 The path of a notebook
297 """
330 """
298 model = self.get_model(name, path)
331 model = self.get_model(path)
299 nb = model['content']
332 nb = model['content']
300 self.log.warn("Trusting notebook %s/%s", path, name)
333 self.log.warn("Trusting notebook %s", path)
301 self.notary.mark_cells(nb, True)
334 self.notary.mark_cells(nb, True)
302 self.save(model, name, path)
335 self.save(model, path)
303
336
304 def check_and_sign(self, nb, name='', path=''):
337 def check_and_sign(self, nb, path=''):
305 """Check for trusted cells, and sign the notebook.
338 """Check for trusted cells, and sign the notebook.
306
339
307 Called as a part of saving notebooks.
340 Called as a part of saving notebooks.
@@ -310,17 +343,15 b' class ContentsManager(LoggingConfigurable):'
310 ----------
343 ----------
311 nb : dict
344 nb : dict
312 The notebook dict
345 The notebook dict
313 name : string
314 The filename of the notebook (for logging)
315 path : string
346 path : string
316 The notebook's directory (for logging)
347 The notebook's path (for logging)
317 """
348 """
318 if self.notary.check_cells(nb):
349 if self.notary.check_cells(nb):
319 self.notary.sign(nb)
350 self.notary.sign(nb)
320 else:
351 else:
321 self.log.warn("Saving untrusted notebook %s/%s", path, name)
352 self.log.warn("Saving untrusted notebook %s", path)
322
353
323 def mark_trusted_cells(self, nb, name='', path=''):
354 def mark_trusted_cells(self, nb, path=''):
324 """Mark cells as trusted if the notebook signature matches.
355 """Mark cells as trusted if the notebook signature matches.
325
356
326 Called as a part of loading notebooks.
357 Called as a part of loading notebooks.
@@ -329,14 +360,12 b' class ContentsManager(LoggingConfigurable):'
329 ----------
360 ----------
330 nb : dict
361 nb : dict
331 The notebook object (in current nbformat)
362 The notebook object (in current nbformat)
332 name : string
333 The filename of the notebook (for logging)
334 path : string
363 path : string
335 The notebook's directory (for logging)
364 The notebook's path (for logging)
336 """
365 """
337 trusted = self.notary.check_signature(nb)
366 trusted = self.notary.check_signature(nb)
338 if not trusted:
367 if not trusted:
339 self.log.warn("Notebook %s/%s is not trusted", path, name)
368 self.log.warn("Notebook %s is not trusted", path)
340 self.notary.mark_cells(nb, trusted)
369 self.notary.mark_cells(nb, trusted)
341
370
342 def should_list(self, name):
371 def should_list(self, name):
@@ -46,56 +46,59 b' class API(object):'
46 def list(self, path='/'):
46 def list(self, path='/'):
47 return self._req('GET', path)
47 return self._req('GET', path)
48
48
49 def read(self, name, path='/'):
49 def read(self, path):
50 return self._req('GET', url_path_join(path, name))
50 return self._req('GET', path)
51
51
52 def create_untitled(self, path='/', ext=None):
52 def create_untitled(self, path='/', ext='.ipynb'):
53 body = None
53 body = None
54 if ext:
54 if ext:
55 body = json.dumps({'ext': ext})
55 body = json.dumps({'ext': ext})
56 return self._req('POST', path, body)
56 return self._req('POST', path, body)
57
57
58 def upload_untitled(self, body, path='/'):
58 def mkdir_untitled(self, path='/'):
59 return self._req('POST', path, body)
59 return self._req('POST', path, json.dumps({'type': 'directory'}))
60
60
61 def copy_untitled(self, copy_from, path='/'):
61 def copy(self, copy_from, path='/'):
62 body = json.dumps({'copy_from':copy_from})
62 body = json.dumps({'copy_from':copy_from})
63 return self._req('POST', path, body)
63 return self._req('POST', path, body)
64
64
65 def create(self, name, path='/'):
65 def create(self, path='/'):
66 return self._req('PUT', url_path_join(path, name))
66 return self._req('PUT', path)
67
68 def upload(self, path, body):
69 return self._req('PUT', path, body)
67
70
68 def upload(self, name, body, path='/'):
71 def mkdir_untitled(self, path='/'):
69 return self._req('PUT', url_path_join(path, name), body)
72 return self._req('POST', path, json.dumps({'type': 'directory'}))
70
73
71 def mkdir(self, name, path='/'):
74 def mkdir(self, path='/'):
72 return self._req('PUT', url_path_join(path, name), json.dumps({'type': 'directory'}))
75 return self._req('PUT', path, json.dumps({'type': 'directory'}))
73
76
74 def copy(self, copy_from, copy_to, path='/'):
77 def copy_put(self, copy_from, path='/'):
75 body = json.dumps({'copy_from':copy_from})
78 body = json.dumps({'copy_from':copy_from})
76 return self._req('PUT', url_path_join(path, copy_to), body)
79 return self._req('PUT', path, body)
77
80
78 def save(self, name, body, path='/'):
81 def save(self, path, body):
79 return self._req('PUT', url_path_join(path, name), body)
82 return self._req('PUT', path, body)
80
83
81 def delete(self, name, path='/'):
84 def delete(self, path='/'):
82 return self._req('DELETE', url_path_join(path, name))
85 return self._req('DELETE', path)
83
86
84 def rename(self, name, path, new_name):
87 def rename(self, path, new_path):
85 body = json.dumps({'name': new_name})
88 body = json.dumps({'path': new_path})
86 return self._req('PATCH', url_path_join(path, name), body)
89 return self._req('PATCH', path, body)
87
90
88 def get_checkpoints(self, name, path):
91 def get_checkpoints(self, path):
89 return self._req('GET', url_path_join(path, name, 'checkpoints'))
92 return self._req('GET', url_path_join(path, 'checkpoints'))
90
93
91 def new_checkpoint(self, name, path):
94 def new_checkpoint(self, path):
92 return self._req('POST', url_path_join(path, name, 'checkpoints'))
95 return self._req('POST', url_path_join(path, 'checkpoints'))
93
96
94 def restore_checkpoint(self, name, path, checkpoint_id):
97 def restore_checkpoint(self, path, checkpoint_id):
95 return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
98 return self._req('POST', url_path_join(path, 'checkpoints', checkpoint_id))
96
99
97 def delete_checkpoint(self, name, path, checkpoint_id):
100 def delete_checkpoint(self, path, checkpoint_id):
98 return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
101 return self._req('DELETE', url_path_join(path, 'checkpoints', checkpoint_id))
99
102
100 class APITest(NotebookTestBase):
103 class APITest(NotebookTestBase):
101 """Test the kernels web service API"""
104 """Test the kernels web service API"""
@@ -131,8 +134,6 b' class APITest(NotebookTestBase):'
131 self.blob = os.urandom(100)
134 self.blob = os.urandom(100)
132 self.b64_blob = base64.encodestring(self.blob).decode('ascii')
135 self.b64_blob = base64.encodestring(self.blob).decode('ascii')
133
136
134
135
136 for d in (self.dirs + self.hidden_dirs):
137 for d in (self.dirs + self.hidden_dirs):
137 d.replace('/', os.sep)
138 d.replace('/', os.sep)
138 if not os.path.isdir(pjoin(nbdir, d)):
139 if not os.path.isdir(pjoin(nbdir, d)):
@@ -178,12 +179,12 b' class APITest(NotebookTestBase):'
178 nbs = notebooks_only(self.api.list(u'/unicodΓ©/').json())
179 nbs = notebooks_only(self.api.list(u'/unicodΓ©/').json())
179 self.assertEqual(len(nbs), 1)
180 self.assertEqual(len(nbs), 1)
180 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
181 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
181 self.assertEqual(nbs[0]['path'], u'unicodΓ©')
182 self.assertEqual(nbs[0]['path'], u'unicodΓ©/innonascii.ipynb')
182
183
183 nbs = notebooks_only(self.api.list('/foo/bar/').json())
184 nbs = notebooks_only(self.api.list('/foo/bar/').json())
184 self.assertEqual(len(nbs), 1)
185 self.assertEqual(len(nbs), 1)
185 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
186 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
186 self.assertEqual(nbs[0]['path'], 'foo/bar')
187 self.assertEqual(nbs[0]['path'], 'foo/bar/baz.ipynb')
187
188
188 nbs = notebooks_only(self.api.list('foo').json())
189 nbs = notebooks_only(self.api.list('foo').json())
189 self.assertEqual(len(nbs), 4)
190 self.assertEqual(len(nbs), 4)
@@ -198,8 +199,11 b' class APITest(NotebookTestBase):'
198 self.assertEqual(nbnames, expected)
199 self.assertEqual(nbnames, expected)
199
200
200 def test_list_dirs(self):
201 def test_list_dirs(self):
202 print(self.api.list().json())
201 dirs = dirs_only(self.api.list().json())
203 dirs = dirs_only(self.api.list().json())
202 dir_names = {normalize('NFC', d['name']) for d in dirs}
204 dir_names = {normalize('NFC', d['name']) for d in dirs}
205 print(dir_names)
206 print(self.top_level_dirs)
203 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
207 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
204
208
205 def test_list_nonexistant_dir(self):
209 def test_list_nonexistant_dir(self):
@@ -208,8 +212,10 b' class APITest(NotebookTestBase):'
208
212
209 def test_get_nb_contents(self):
213 def test_get_nb_contents(self):
210 for d, name in self.dirs_nbs:
214 for d, name in self.dirs_nbs:
211 nb = self.api.read('%s.ipynb' % name, d+'/').json()
215 path = url_path_join(d, name + '.ipynb')
216 nb = self.api.read(path).json()
212 self.assertEqual(nb['name'], u'%s.ipynb' % name)
217 self.assertEqual(nb['name'], u'%s.ipynb' % name)
218 self.assertEqual(nb['path'], path)
213 self.assertEqual(nb['type'], 'notebook')
219 self.assertEqual(nb['type'], 'notebook')
214 self.assertIn('content', nb)
220 self.assertIn('content', nb)
215 self.assertEqual(nb['format'], 'json')
221 self.assertEqual(nb['format'], 'json')
@@ -220,12 +226,14 b' class APITest(NotebookTestBase):'
220 def test_get_contents_no_such_file(self):
226 def test_get_contents_no_such_file(self):
221 # Name that doesn't exist - should be a 404
227 # Name that doesn't exist - should be a 404
222 with assert_http_error(404):
228 with assert_http_error(404):
223 self.api.read('q.ipynb', 'foo')
229 self.api.read('foo/q.ipynb')
224
230
225 def test_get_text_file_contents(self):
231 def test_get_text_file_contents(self):
226 for d, name in self.dirs_nbs:
232 for d, name in self.dirs_nbs:
227 model = self.api.read(u'%s.txt' % name, d+'/').json()
233 path = url_path_join(d, name + '.txt')
234 model = self.api.read(path).json()
228 self.assertEqual(model['name'], u'%s.txt' % name)
235 self.assertEqual(model['name'], u'%s.txt' % name)
236 self.assertEqual(model['path'], path)
229 self.assertIn('content', model)
237 self.assertIn('content', model)
230 self.assertEqual(model['format'], 'text')
238 self.assertEqual(model['format'], 'text')
231 self.assertEqual(model['type'], 'file')
239 self.assertEqual(model['type'], 'file')
@@ -233,12 +241,14 b' class APITest(NotebookTestBase):'
233
241
234 # Name that doesn't exist - should be a 404
242 # Name that doesn't exist - should be a 404
235 with assert_http_error(404):
243 with assert_http_error(404):
236 self.api.read('q.txt', 'foo')
244 self.api.read('foo/q.txt')
237
245
238 def test_get_binary_file_contents(self):
246 def test_get_binary_file_contents(self):
239 for d, name in self.dirs_nbs:
247 for d, name in self.dirs_nbs:
240 model = self.api.read(u'%s.blob' % name, d+'/').json()
248 path = url_path_join(d, name + '.blob')
249 model = self.api.read(path).json()
241 self.assertEqual(model['name'], u'%s.blob' % name)
250 self.assertEqual(model['name'], u'%s.blob' % name)
251 self.assertEqual(model['path'], path)
242 self.assertIn('content', model)
252 self.assertIn('content', model)
243 self.assertEqual(model['format'], 'base64')
253 self.assertEqual(model['format'], 'base64')
244 self.assertEqual(model['type'], 'file')
254 self.assertEqual(model['type'], 'file')
@@ -247,66 +257,71 b' class APITest(NotebookTestBase):'
247
257
248 # Name that doesn't exist - should be a 404
258 # Name that doesn't exist - should be a 404
249 with assert_http_error(404):
259 with assert_http_error(404):
250 self.api.read('q.txt', 'foo')
260 self.api.read('foo/q.txt')
251
261
252 def _check_created(self, resp, name, path, type='notebook'):
262 def _check_created(self, resp, path, type='notebook'):
253 self.assertEqual(resp.status_code, 201)
263 self.assertEqual(resp.status_code, 201)
254 location_header = py3compat.str_to_unicode(resp.headers['Location'])
264 location_header = py3compat.str_to_unicode(resp.headers['Location'])
255 self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path, name)))
265 self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path)))
256 rjson = resp.json()
266 rjson = resp.json()
257 self.assertEqual(rjson['name'], name)
267 self.assertEqual(rjson['name'], path.rsplit('/', 1)[-1])
258 self.assertEqual(rjson['path'], path)
268 self.assertEqual(rjson['path'], path)
259 self.assertEqual(rjson['type'], type)
269 self.assertEqual(rjson['type'], type)
260 isright = os.path.isdir if type == 'directory' else os.path.isfile
270 isright = os.path.isdir if type == 'directory' else os.path.isfile
261 assert isright(pjoin(
271 assert isright(pjoin(
262 self.notebook_dir.name,
272 self.notebook_dir.name,
263 path.replace('/', os.sep),
273 path.replace('/', os.sep),
264 name,
265 ))
274 ))
266
275
267 def test_create_untitled(self):
276 def test_create_untitled(self):
268 resp = self.api.create_untitled(path=u'Γ₯ b')
277 resp = self.api.create_untitled(path=u'Γ₯ b')
269 self._check_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
278 self._check_created(resp, u'Γ₯ b/Untitled0.ipynb')
270
279
271 # Second time
280 # Second time
272 resp = self.api.create_untitled(path=u'Γ₯ b')
281 resp = self.api.create_untitled(path=u'Γ₯ b')
273 self._check_created(resp, 'Untitled1.ipynb', u'Γ₯ b')
282 self._check_created(resp, u'Γ₯ b/Untitled1.ipynb')
274
283
275 # And two directories down
284 # And two directories down
276 resp = self.api.create_untitled(path='foo/bar')
285 resp = self.api.create_untitled(path='foo/bar')
277 self._check_created(resp, 'Untitled0.ipynb', 'foo/bar')
286 self._check_created(resp, 'foo/bar/Untitled0.ipynb')
278
287
279 def test_create_untitled_txt(self):
288 def test_create_untitled_txt(self):
280 resp = self.api.create_untitled(path='foo/bar', ext='.txt')
289 resp = self.api.create_untitled(path='foo/bar', ext='.txt')
281 self._check_created(resp, 'untitled0.txt', 'foo/bar', type='file')
290 self._check_created(resp, 'foo/bar/untitled0.txt', type='file')
282
291
283 resp = self.api.read(path='foo/bar', name='untitled0.txt')
292 resp = self.api.read(path='foo/bar/untitled0.txt')
284 model = resp.json()
293 model = resp.json()
285 self.assertEqual(model['type'], 'file')
294 self.assertEqual(model['type'], 'file')
286 self.assertEqual(model['format'], 'text')
295 self.assertEqual(model['format'], 'text')
287 self.assertEqual(model['content'], '')
296 self.assertEqual(model['content'], '')
288
297
289 def test_upload_untitled(self):
290 nb = new_notebook()
291 nbmodel = {'content': nb, 'type': 'notebook'}
292 resp = self.api.upload_untitled(path=u'Γ₯ b',
293 body=json.dumps(nbmodel))
294 self._check_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
295
296 def test_upload(self):
298 def test_upload(self):
297 nb = new_notebook()
299 nb = new_notebook()
298 nbmodel = {'content': nb, 'type': 'notebook'}
300 nbmodel = {'content': nb, 'type': 'notebook'}
299 resp = self.api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
301 path = u'Γ₯ b/Upload tΓ©st.ipynb'
300 body=json.dumps(nbmodel))
302 resp = self.api.upload(path, body=json.dumps(nbmodel))
301 self._check_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
303 self._check_created(resp, path)
304
305 def test_mkdir_untitled(self):
306 resp = self.api.mkdir_untitled(path=u'Γ₯ b')
307 self._check_created(resp, u'Γ₯ b/Untitled Folder0', type='directory')
308
309 # Second time
310 resp = self.api.mkdir_untitled(path=u'Γ₯ b')
311 self._check_created(resp, u'Γ₯ b/Untitled Folder1', type='directory')
312
313 # And two directories down
314 resp = self.api.mkdir_untitled(path='foo/bar')
315 self._check_created(resp, 'foo/bar/Untitled Folder0', type='directory')
302
316
303 def test_mkdir(self):
317 def test_mkdir(self):
304 resp = self.api.mkdir(u'New βˆ‚ir', path=u'Γ₯ b')
318 path = u'Γ₯ b/New βˆ‚ir'
305 self._check_created(resp, u'New βˆ‚ir', u'Γ₯ b', type='directory')
319 resp = self.api.mkdir(path)
320 self._check_created(resp, path, type='directory')
306
321
307 def test_mkdir_hidden_400(self):
322 def test_mkdir_hidden_400(self):
308 with assert_http_error(400):
323 with assert_http_error(400):
309 resp = self.api.mkdir(u'.hidden', path=u'Γ₯ b')
324 resp = self.api.mkdir(u'Γ₯ b/.hidden')
310
325
311 def test_upload_txt(self):
326 def test_upload_txt(self):
312 body = u'ΓΌnicode tΓ©xt'
327 body = u'ΓΌnicode tΓ©xt'
@@ -315,11 +330,11 b' class APITest(NotebookTestBase):'
315 'format' : 'text',
330 'format' : 'text',
316 'type' : 'file',
331 'type' : 'file',
317 }
332 }
318 resp = self.api.upload(u'Upload tΓ©st.txt', path=u'Γ₯ b',
333 path = u'Γ₯ b/Upload tΓ©st.txt'
319 body=json.dumps(model))
334 resp = self.api.upload(path, body=json.dumps(model))
320
335
321 # check roundtrip
336 # check roundtrip
322 resp = self.api.read(path=u'Γ₯ b', name=u'Upload tΓ©st.txt')
337 resp = self.api.read(path)
323 model = resp.json()
338 model = resp.json()
324 self.assertEqual(model['type'], 'file')
339 self.assertEqual(model['type'], 'file')
325 self.assertEqual(model['format'], 'text')
340 self.assertEqual(model['format'], 'text')
@@ -333,13 +348,14 b' class APITest(NotebookTestBase):'
333 'format' : 'base64',
348 'format' : 'base64',
334 'type' : 'file',
349 'type' : 'file',
335 }
350 }
336 resp = self.api.upload(u'Upload tΓ©st.blob', path=u'Γ₯ b',
351 path = u'Γ₯ b/Upload tΓ©st.blob'
337 body=json.dumps(model))
352 resp = self.api.upload(path, body=json.dumps(model))
338
353
339 # check roundtrip
354 # check roundtrip
340 resp = self.api.read(path=u'Γ₯ b', name=u'Upload tΓ©st.blob')
355 resp = self.api.read(path)
341 model = resp.json()
356 model = resp.json()
342 self.assertEqual(model['type'], 'file')
357 self.assertEqual(model['type'], 'file')
358 self.assertEqual(model['path'], path)
343 self.assertEqual(model['format'], 'base64')
359 self.assertEqual(model['format'], 'base64')
344 decoded = base64.decodestring(model['content'].encode('ascii'))
360 decoded = base64.decodestring(model['content'].encode('ascii'))
345 self.assertEqual(decoded, body)
361 self.assertEqual(decoded, body)
@@ -350,45 +366,52 b' class APITest(NotebookTestBase):'
350 nb.worksheets.append(ws)
366 nb.worksheets.append(ws)
351 ws.cells.append(v2.new_code_cell(input='print("hi")'))
367 ws.cells.append(v2.new_code_cell(input='print("hi")'))
352 nbmodel = {'content': nb, 'type': 'notebook'}
368 nbmodel = {'content': nb, 'type': 'notebook'}
353 resp = self.api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
369 path = u'Γ₯ b/Upload tΓ©st.ipynb'
354 body=json.dumps(nbmodel))
370 resp = self.api.upload(path, body=json.dumps(nbmodel))
355 self._check_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
371 self._check_created(resp, path)
356 resp = self.api.read(u'Upload tΓ©st.ipynb', u'Γ₯ b')
372 resp = self.api.read(path)
357 data = resp.json()
373 data = resp.json()
358 self.assertEqual(data['content']['nbformat'], 4)
374 self.assertEqual(data['content']['nbformat'], 4)
359
375
360 def test_copy_untitled(self):
361 resp = self.api.copy_untitled(u'Γ§ d.ipynb', path=u'Γ₯ b')
362 self._check_created(resp, u'Γ§ d-Copy0.ipynb', u'Γ₯ b')
363
364 def test_copy(self):
376 def test_copy(self):
365 resp = self.api.copy(u'Γ§ d.ipynb', u'cΓΈpy.ipynb', path=u'Γ₯ b')
377 resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'unicodΓ©')
366 self._check_created(resp, u'cΓΈpy.ipynb', u'Γ₯ b')
378 self._check_created(resp, u'unicodΓ©/Γ§ d-Copy0.ipynb')
379
380 resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b')
381 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy0.ipynb')
367
382
368 def test_copy_path(self):
383 def test_copy_path(self):
369 resp = self.api.copy(u'foo/a.ipynb', u'cΓΈpyfoo.ipynb', path=u'Γ₯ b')
384 resp = self.api.copy(u'foo/a.ipynb', u'Γ₯ b')
370 self._check_created(resp, u'cΓΈpyfoo.ipynb', u'Γ₯ b')
385 self._check_created(resp, u'Γ₯ b/a-Copy0.ipynb')
386
387 def test_copy_put_400(self):
388 with assert_http_error(400):
389 resp = self.api.copy_put(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b/cΓΈpy.ipynb')
371
390
372 def test_copy_dir_400(self):
391 def test_copy_dir_400(self):
373 # can't copy directories
392 # can't copy directories
374 with assert_http_error(400):
393 with assert_http_error(400):
375 resp = self.api.copy(u'Γ₯ b', u'Γ₯ c')
394 resp = self.api.copy(u'Γ₯ b', u'foo')
376
395
377 def test_delete(self):
396 def test_delete(self):
378 for d, name in self.dirs_nbs:
397 for d, name in self.dirs_nbs:
379 resp = self.api.delete('%s.ipynb' % name, d)
398 print('%r, %r' % (d, name))
399 resp = self.api.delete(url_path_join(d, name + '.ipynb'))
380 self.assertEqual(resp.status_code, 204)
400 self.assertEqual(resp.status_code, 204)
381
401
382 for d in self.dirs + ['/']:
402 for d in self.dirs + ['/']:
383 nbs = notebooks_only(self.api.list(d).json())
403 nbs = notebooks_only(self.api.list(d).json())
384 self.assertEqual(len(nbs), 0)
404 print('------')
405 print(d)
406 print(nbs)
407 self.assertEqual(nbs, [])
385
408
386 def test_delete_dirs(self):
409 def test_delete_dirs(self):
387 # depth-first delete everything, so we don't try to delete empty directories
410 # depth-first delete everything, so we don't try to delete empty directories
388 for name in sorted(self.dirs + ['/'], key=len, reverse=True):
411 for name in sorted(self.dirs + ['/'], key=len, reverse=True):
389 listing = self.api.list(name).json()['content']
412 listing = self.api.list(name).json()['content']
390 for model in listing:
413 for model in listing:
391 self.api.delete(model['name'], model['path'])
414 self.api.delete(model['path'])
392 listing = self.api.list('/').json()['content']
415 listing = self.api.list('/').json()['content']
393 self.assertEqual(listing, [])
416 self.assertEqual(listing, [])
394
417
@@ -398,9 +421,10 b' class APITest(NotebookTestBase):'
398 self.api.delete(u'Γ₯ b')
421 self.api.delete(u'Γ₯ b')
399
422
400 def test_rename(self):
423 def test_rename(self):
401 resp = self.api.rename('a.ipynb', 'foo', 'z.ipynb')
424 resp = self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
402 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
425 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
403 self.assertEqual(resp.json()['name'], 'z.ipynb')
426 self.assertEqual(resp.json()['name'], 'z.ipynb')
427 self.assertEqual(resp.json()['path'], 'foo/z.ipynb')
404 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
428 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
405
429
406 nbs = notebooks_only(self.api.list('foo').json())
430 nbs = notebooks_only(self.api.list('foo').json())
@@ -410,41 +434,31 b' class APITest(NotebookTestBase):'
410
434
411 def test_rename_existing(self):
435 def test_rename_existing(self):
412 with assert_http_error(409):
436 with assert_http_error(409):
413 self.api.rename('a.ipynb', 'foo', 'b.ipynb')
437 self.api.rename('foo/a.ipynb', 'foo/b.ipynb')
414
438
415 def test_save(self):
439 def test_save(self):
416 resp = self.api.read('a.ipynb', 'foo')
440 resp = self.api.read('foo/a.ipynb')
417 nbcontent = json.loads(resp.text)['content']
441 nbcontent = json.loads(resp.text)['content']
418 nb = from_dict(nbcontent)
442 nb = from_dict(nbcontent)
419 nb.cells.append(new_markdown_cell(u'Created by test Β³'))
443 nb.cells.append(new_markdown_cell(u'Created by test Β³'))
420
444
421 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'}
445 nbmodel= {'content': nb, 'type': 'notebook'}
422 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
446 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
423
447
424 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
448 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
425 with io.open(nbfile, 'r', encoding='utf-8') as f:
449 with io.open(nbfile, 'r', encoding='utf-8') as f:
426 newnb = read(f, as_version=4)
450 newnb = read(f, as_version=4)
427 self.assertEqual(newnb.cells[0].source,
451 self.assertEqual(newnb.cells[0].source,
428 u'Created by test Β³')
452 u'Created by test Β³')
429 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
453 nbcontent = self.api.read('foo/a.ipynb').json()['content']
430 newnb = from_dict(nbcontent)
454 newnb = from_dict(nbcontent)
431 self.assertEqual(newnb.cells[0].source,
455 self.assertEqual(newnb.cells[0].source,
432 u'Created by test Β³')
456 u'Created by test Β³')
433
457
434 # Save and rename
435 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb, 'type': 'notebook'}
436 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
437 saved = resp.json()
438 self.assertEqual(saved['name'], 'a2.ipynb')
439 self.assertEqual(saved['path'], 'foo/bar')
440 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
441 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
442 with assert_http_error(404):
443 self.api.read('a.ipynb', 'foo')
444
458
445 def test_checkpoints(self):
459 def test_checkpoints(self):
446 resp = self.api.read('a.ipynb', 'foo')
460 resp = self.api.read('foo/a.ipynb')
447 r = self.api.new_checkpoint('a.ipynb', 'foo')
461 r = self.api.new_checkpoint('foo/a.ipynb')
448 self.assertEqual(r.status_code, 201)
462 self.assertEqual(r.status_code, 201)
449 cp1 = r.json()
463 cp1 = r.json()
450 self.assertEqual(set(cp1), {'id', 'last_modified'})
464 self.assertEqual(set(cp1), {'id', 'last_modified'})
@@ -456,26 +470,26 b' class APITest(NotebookTestBase):'
456 hcell = new_markdown_cell('Created by test')
470 hcell = new_markdown_cell('Created by test')
457 nb.cells.append(hcell)
471 nb.cells.append(hcell)
458 # Save
472 # Save
459 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'}
473 nbmodel= {'content': nb, 'type': 'notebook'}
460 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
474 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
461
475
462 # List checkpoints
476 # List checkpoints
463 cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
477 cps = self.api.get_checkpoints('foo/a.ipynb').json()
464 self.assertEqual(cps, [cp1])
478 self.assertEqual(cps, [cp1])
465
479
466 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
480 nbcontent = self.api.read('foo/a.ipynb').json()['content']
467 nb = from_dict(nbcontent)
481 nb = from_dict(nbcontent)
468 self.assertEqual(nb.cells[0].source, 'Created by test')
482 self.assertEqual(nb.cells[0].source, 'Created by test')
469
483
470 # Restore cp1
484 # Restore cp1
471 r = self.api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
485 r = self.api.restore_checkpoint('foo/a.ipynb', cp1['id'])
472 self.assertEqual(r.status_code, 204)
486 self.assertEqual(r.status_code, 204)
473 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
487 nbcontent = self.api.read('foo/a.ipynb').json()['content']
474 nb = from_dict(nbcontent)
488 nb = from_dict(nbcontent)
475 self.assertEqual(nb.cells, [])
489 self.assertEqual(nb.cells, [])
476
490
477 # Delete cp1
491 # Delete cp1
478 r = self.api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
492 r = self.api.delete_checkpoint('foo/a.ipynb', cp1['id'])
479 self.assertEqual(r.status_code, 204)
493 self.assertEqual(r.status_code, 204)
480 cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
494 cps = self.api.get_checkpoints('foo/a.ipynb').json()
481 self.assertEqual(cps, [])
495 self.assertEqual(cps, [])
@@ -42,7 +42,7 b' class TestFileContentsManager(TestCase):'
42 with TemporaryDirectory() as td:
42 with TemporaryDirectory() as td:
43 root = td
43 root = td
44 fm = FileContentsManager(root_dir=root)
44 fm = FileContentsManager(root_dir=root)
45 path = fm._get_os_path('test.ipynb', '/path/to/notebook/')
45 path = fm._get_os_path('/path/to/notebook/test.ipynb')
46 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
46 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
47 fs_path = os.path.join(fm.root_dir, *rel_path_list)
47 fs_path = os.path.join(fm.root_dir, *rel_path_list)
48 self.assertEqual(path, fs_path)
48 self.assertEqual(path, fs_path)
@@ -53,7 +53,7 b' class TestFileContentsManager(TestCase):'
53 self.assertEqual(path, fs_path)
53 self.assertEqual(path, fs_path)
54
54
55 fm = FileContentsManager(root_dir=root)
55 fm = FileContentsManager(root_dir=root)
56 path = fm._get_os_path('test.ipynb', '////')
56 path = fm._get_os_path('////test.ipynb')
57 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
57 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
58 self.assertEqual(path, fs_path)
58 self.assertEqual(path, fs_path)
59
59
@@ -64,8 +64,8 b' class TestFileContentsManager(TestCase):'
64 root = td
64 root = td
65 os.mkdir(os.path.join(td, subd))
65 os.mkdir(os.path.join(td, subd))
66 fm = FileContentsManager(root_dir=root)
66 fm = FileContentsManager(root_dir=root)
67 cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb', '/')
67 cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb')
68 cp_subdir = fm.get_checkpoint_path('cp', 'test.ipynb', '/%s/' % subd)
68 cp_subdir = fm.get_checkpoint_path('cp', '/%s/test.ipynb' % subd)
69 self.assertNotEqual(cp_dir, cp_subdir)
69 self.assertNotEqual(cp_dir, cp_subdir)
70 self.assertEqual(cp_dir, os.path.join(root, fm.checkpoint_dir, cp_name))
70 self.assertEqual(cp_dir, os.path.join(root, fm.checkpoint_dir, cp_name))
71 self.assertEqual(cp_subdir, os.path.join(root, subd, fm.checkpoint_dir, cp_name))
71 self.assertEqual(cp_subdir, os.path.join(root, subd, fm.checkpoint_dir, cp_name))
@@ -101,46 +101,58 b' class TestContentsManager(TestCase):'
101
101
102 def new_notebook(self):
102 def new_notebook(self):
103 cm = self.contents_manager
103 cm = self.contents_manager
104 model = cm.create_file()
104 model = cm.new_untitled(type='notebook')
105 name = model['name']
105 name = model['name']
106 path = model['path']
106 path = model['path']
107
107
108 full_model = cm.get_model(name, path)
108 full_model = cm.get_model(path)
109 nb = full_model['content']
109 nb = full_model['content']
110 self.add_code_cell(nb)
110 self.add_code_cell(nb)
111
111
112 cm.save(full_model, name, path)
112 cm.save(full_model, path)
113 return nb, name, path
113 return nb, name, path
114
114
115 def test_create_file(self):
115 def test_new_untitled(self):
116 cm = self.contents_manager
116 cm = self.contents_manager
117 # Test in root directory
117 # Test in root directory
118 model = cm.create_file()
118 model = cm.new_untitled(type='notebook')
119 assert isinstance(model, dict)
119 assert isinstance(model, dict)
120 self.assertIn('name', model)
120 self.assertIn('name', model)
121 self.assertIn('path', model)
121 self.assertIn('path', model)
122 self.assertIn('type', model)
123 self.assertEqual(model['type'], 'notebook')
122 self.assertEqual(model['name'], 'Untitled0.ipynb')
124 self.assertEqual(model['name'], 'Untitled0.ipynb')
123 self.assertEqual(model['path'], '')
125 self.assertEqual(model['path'], 'Untitled0.ipynb')
124
126
125 # Test in sub-directory
127 # Test in sub-directory
126 sub_dir = '/foo/'
128 model = cm.new_untitled(type='directory')
127 self.make_dir(cm.root_dir, 'foo')
128 model = cm.create_file(None, sub_dir)
129 assert isinstance(model, dict)
129 assert isinstance(model, dict)
130 self.assertIn('name', model)
130 self.assertIn('name', model)
131 self.assertIn('path', model)
131 self.assertIn('path', model)
132 self.assertEqual(model['name'], 'Untitled0.ipynb')
132 self.assertIn('type', model)
133 self.assertEqual(model['path'], sub_dir.strip('/'))
133 self.assertEqual(model['type'], 'directory')
134 self.assertEqual(model['name'], 'Untitled Folder0')
135 self.assertEqual(model['path'], 'Untitled Folder0')
136 sub_dir = model['path']
137
138 model = cm.new_untitled(path=sub_dir)
139 assert isinstance(model, dict)
140 self.assertIn('name', model)
141 self.assertIn('path', model)
142 self.assertIn('type', model)
143 self.assertEqual(model['type'], 'file')
144 self.assertEqual(model['name'], 'untitled0')
145 self.assertEqual(model['path'], '%s/untitled0' % sub_dir)
134
146
135 def test_get(self):
147 def test_get(self):
136 cm = self.contents_manager
148 cm = self.contents_manager
137 # Create a notebook
149 # Create a notebook
138 model = cm.create_file()
150 model = cm.new_untitled(type='notebook')
139 name = model['name']
151 name = model['name']
140 path = model['path']
152 path = model['path']
141
153
142 # Check that we 'get' on the notebook we just created
154 # Check that we 'get' on the notebook we just created
143 model2 = cm.get_model(name, path)
155 model2 = cm.get_model(path)
144 assert isinstance(model2, dict)
156 assert isinstance(model2, dict)
145 self.assertIn('name', model2)
157 self.assertIn('name', model2)
146 self.assertIn('path', model2)
158 self.assertIn('path', model2)
@@ -150,14 +162,14 b' class TestContentsManager(TestCase):'
150 # Test in sub-directory
162 # Test in sub-directory
151 sub_dir = '/foo/'
163 sub_dir = '/foo/'
152 self.make_dir(cm.root_dir, 'foo')
164 self.make_dir(cm.root_dir, 'foo')
153 model = cm.create_file(None, sub_dir)
165 model = cm.new_untitled(path=sub_dir, ext='.ipynb')
154 model2 = cm.get_model(name, sub_dir)
166 model2 = cm.get_model(sub_dir + name)
155 assert isinstance(model2, dict)
167 assert isinstance(model2, dict)
156 self.assertIn('name', model2)
168 self.assertIn('name', model2)
157 self.assertIn('path', model2)
169 self.assertIn('path', model2)
158 self.assertIn('content', model2)
170 self.assertIn('content', model2)
159 self.assertEqual(model2['name'], 'Untitled0.ipynb')
171 self.assertEqual(model2['name'], 'Untitled0.ipynb')
160 self.assertEqual(model2['path'], sub_dir.strip('/'))
172 self.assertEqual(model2['path'], '{0}/{1}'.format(sub_dir.strip('/'), name))
161
173
162 @dec.skip_win32
174 @dec.skip_win32
163 def test_bad_symlink(self):
175 def test_bad_symlink(self):
@@ -165,7 +177,7 b' class TestContentsManager(TestCase):'
165 path = 'test bad symlink'
177 path = 'test bad symlink'
166 os_path = self.make_dir(cm.root_dir, path)
178 os_path = self.make_dir(cm.root_dir, path)
167
179
168 file_model = cm.create_file(path=path, ext='.txt')
180 file_model = cm.new_untitled(path=path, ext='.txt')
169
181
170 # create a broken symlink
182 # create a broken symlink
171 os.symlink("target", os.path.join(os_path, "bad symlink"))
183 os.symlink("target", os.path.join(os_path, "bad symlink"))
@@ -175,16 +187,17 b' class TestContentsManager(TestCase):'
175 @dec.skip_win32
187 @dec.skip_win32
176 def test_good_symlink(self):
188 def test_good_symlink(self):
177 cm = self.contents_manager
189 cm = self.contents_manager
178 path = 'test good symlink'
190 parent = 'test good symlink'
179 os_path = self.make_dir(cm.root_dir, path)
191 name = 'good symlink'
192 path = '{0}/{1}'.format(parent, name)
193 os_path = self.make_dir(cm.root_dir, parent)
180
194
181 file_model = cm.create_file(path=path, ext='.txt')
195 file_model = cm.new(path=parent + '/zfoo.txt')
182
196
183 # create a good symlink
197 # create a good symlink
184 os.symlink(file_model['name'], os.path.join(os_path, "good symlink"))
198 os.symlink(file_model['name'], os.path.join(os_path, name))
185 symlink_model = cm.get_model(name="good symlink", path=path, content=False)
199 symlink_model = cm.get_model(path, content=False)
186
200 dir_model = cm.get_model(parent)
187 dir_model = cm.get_model(path)
188 self.assertEqual(
201 self.assertEqual(
189 sorted(dir_model['content'], key=lambda x: x['name']),
202 sorted(dir_model['content'], key=lambda x: x['name']),
190 [symlink_model, file_model],
203 [symlink_model, file_model],
@@ -193,53 +206,54 b' class TestContentsManager(TestCase):'
193 def test_update(self):
206 def test_update(self):
194 cm = self.contents_manager
207 cm = self.contents_manager
195 # Create a notebook
208 # Create a notebook
196 model = cm.create_file()
209 model = cm.new_untitled(type='notebook')
197 name = model['name']
210 name = model['name']
198 path = model['path']
211 path = model['path']
199
212
200 # Change the name in the model for rename
213 # Change the name in the model for rename
201 model['name'] = 'test.ipynb'
214 model['path'] = 'test.ipynb'
202 model = cm.update(model, name, path)
215 model = cm.update(model, path)
203 assert isinstance(model, dict)
216 assert isinstance(model, dict)
204 self.assertIn('name', model)
217 self.assertIn('name', model)
205 self.assertIn('path', model)
218 self.assertIn('path', model)
206 self.assertEqual(model['name'], 'test.ipynb')
219 self.assertEqual(model['name'], 'test.ipynb')
207
220
208 # Make sure the old name is gone
221 # Make sure the old name is gone
209 self.assertRaises(HTTPError, cm.get_model, name, path)
222 self.assertRaises(HTTPError, cm.get_model, path)
210
223
211 # Test in sub-directory
224 # Test in sub-directory
212 # Create a directory and notebook in that directory
225 # Create a directory and notebook in that directory
213 sub_dir = '/foo/'
226 sub_dir = '/foo/'
214 self.make_dir(cm.root_dir, 'foo')
227 self.make_dir(cm.root_dir, 'foo')
215 model = cm.create_file(None, sub_dir)
228 model = cm.new_untitled(path=sub_dir, type='notebook')
216 name = model['name']
229 name = model['name']
217 path = model['path']
230 path = model['path']
218
231
219 # Change the name in the model for rename
232 # Change the name in the model for rename
220 model['name'] = 'test_in_sub.ipynb'
233 d = path.rsplit('/', 1)[0]
221 model = cm.update(model, name, path)
234 new_path = model['path'] = d + '/test_in_sub.ipynb'
235 model = cm.update(model, path)
222 assert isinstance(model, dict)
236 assert isinstance(model, dict)
223 self.assertIn('name', model)
237 self.assertIn('name', model)
224 self.assertIn('path', model)
238 self.assertIn('path', model)
225 self.assertEqual(model['name'], 'test_in_sub.ipynb')
239 self.assertEqual(model['name'], 'test_in_sub.ipynb')
226 self.assertEqual(model['path'], sub_dir.strip('/'))
240 self.assertEqual(model['path'], new_path)
227
241
228 # Make sure the old name is gone
242 # Make sure the old name is gone
229 self.assertRaises(HTTPError, cm.get_model, name, path)
243 self.assertRaises(HTTPError, cm.get_model, path)
230
244
231 def test_save(self):
245 def test_save(self):
232 cm = self.contents_manager
246 cm = self.contents_manager
233 # Create a notebook
247 # Create a notebook
234 model = cm.create_file()
248 model = cm.new_untitled(type='notebook')
235 name = model['name']
249 name = model['name']
236 path = model['path']
250 path = model['path']
237
251
238 # Get the model with 'content'
252 # Get the model with 'content'
239 full_model = cm.get_model(name, path)
253 full_model = cm.get_model(path)
240
254
241 # Save the notebook
255 # Save the notebook
242 model = cm.save(full_model, name, path)
256 model = cm.save(full_model, path)
243 assert isinstance(model, dict)
257 assert isinstance(model, dict)
244 self.assertIn('name', model)
258 self.assertIn('name', model)
245 self.assertIn('path', model)
259 self.assertIn('path', model)
@@ -250,18 +264,18 b' class TestContentsManager(TestCase):'
250 # Create a directory and notebook in that directory
264 # Create a directory and notebook in that directory
251 sub_dir = '/foo/'
265 sub_dir = '/foo/'
252 self.make_dir(cm.root_dir, 'foo')
266 self.make_dir(cm.root_dir, 'foo')
253 model = cm.create_file(None, sub_dir)
267 model = cm.new_untitled(path=sub_dir, type='notebook')
254 name = model['name']
268 name = model['name']
255 path = model['path']
269 path = model['path']
256 model = cm.get_model(name, path)
270 model = cm.get_model(path)
257
271
258 # Change the name in the model for rename
272 # Change the name in the model for rename
259 model = cm.save(model, name, path)
273 model = cm.save(model, path)
260 assert isinstance(model, dict)
274 assert isinstance(model, dict)
261 self.assertIn('name', model)
275 self.assertIn('name', model)
262 self.assertIn('path', model)
276 self.assertIn('path', model)
263 self.assertEqual(model['name'], 'Untitled0.ipynb')
277 self.assertEqual(model['name'], 'Untitled0.ipynb')
264 self.assertEqual(model['path'], sub_dir.strip('/'))
278 self.assertEqual(model['path'], 'foo/Untitled0.ipynb')
265
279
266 def test_delete(self):
280 def test_delete(self):
267 cm = self.contents_manager
281 cm = self.contents_manager
@@ -269,36 +283,38 b' class TestContentsManager(TestCase):'
269 nb, name, path = self.new_notebook()
283 nb, name, path = self.new_notebook()
270
284
271 # Delete the notebook
285 # Delete the notebook
272 cm.delete(name, path)
286 cm.delete(path)
273
287
274 # Check that a 'get' on the deleted notebook raises and error
288 # Check that a 'get' on the deleted notebook raises and error
275 self.assertRaises(HTTPError, cm.get_model, name, path)
289 self.assertRaises(HTTPError, cm.get_model, path)
276
290
277 def test_copy(self):
291 def test_copy(self):
278 cm = self.contents_manager
292 cm = self.contents_manager
279 path = u'Γ₯ b'
293 parent = u'Γ₯ b'
280 name = u'nb √.ipynb'
294 name = u'nb √.ipynb'
281 os.mkdir(os.path.join(cm.root_dir, path))
295 path = u'{0}/{1}'.format(parent, name)
282 orig = cm.create_file({'name' : name}, path=path)
296 os.mkdir(os.path.join(cm.root_dir, parent))
297 orig = cm.new(path=path)
283
298
284 # copy with unspecified name
299 # copy with unspecified name
285 copy = cm.copy(name, path=path)
300 copy = cm.copy(path)
286 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
301 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
287
302
288 # copy with specified name
303 # copy with specified name
289 copy2 = cm.copy(name, u'copy 2.ipynb', path=path)
304 copy2 = cm.copy(path, u'Γ₯ b/copy 2.ipynb')
290 self.assertEqual(copy2['name'], u'copy 2.ipynb')
305 self.assertEqual(copy2['name'], u'copy 2.ipynb')
306 self.assertEqual(copy2['path'], u'Γ₯ b/copy 2.ipynb')
291
307
292 def test_trust_notebook(self):
308 def test_trust_notebook(self):
293 cm = self.contents_manager
309 cm = self.contents_manager
294 nb, name, path = self.new_notebook()
310 nb, name, path = self.new_notebook()
295
311
296 untrusted = cm.get_model(name, path)['content']
312 untrusted = cm.get_model(path)['content']
297 assert not cm.notary.check_cells(untrusted)
313 assert not cm.notary.check_cells(untrusted)
298
314
299 # print(untrusted)
315 # print(untrusted)
300 cm.trust_notebook(name, path)
316 cm.trust_notebook(path)
301 trusted = cm.get_model(name, path)['content']
317 trusted = cm.get_model(path)['content']
302 # print(trusted)
318 # print(trusted)
303 assert cm.notary.check_cells(trusted)
319 assert cm.notary.check_cells(trusted)
304
320
@@ -306,13 +322,13 b' class TestContentsManager(TestCase):'
306 cm = self.contents_manager
322 cm = self.contents_manager
307 nb, name, path = self.new_notebook()
323 nb, name, path = self.new_notebook()
308
324
309 cm.mark_trusted_cells(nb, name, path)
325 cm.mark_trusted_cells(nb, path)
310 for cell in nb.cells:
326 for cell in nb.cells:
311 if cell.cell_type == 'code':
327 if cell.cell_type == 'code':
312 assert not cell.metadata.trusted
328 assert not cell.metadata.trusted
313
329
314 cm.trust_notebook(name, path)
330 cm.trust_notebook(path)
315 nb = cm.get_model(name, path)['content']
331 nb = cm.get_model(path)['content']
316 for cell in nb.cells:
332 for cell in nb.cells:
317 if cell.cell_type == 'code':
333 if cell.cell_type == 'code':
318 assert cell.metadata.trusted
334 assert cell.metadata.trusted
@@ -321,12 +337,12 b' class TestContentsManager(TestCase):'
321 cm = self.contents_manager
337 cm = self.contents_manager
322 nb, name, path = self.new_notebook()
338 nb, name, path = self.new_notebook()
323
339
324 cm.mark_trusted_cells(nb, name, path)
340 cm.mark_trusted_cells(nb, path)
325 cm.check_and_sign(nb, name, path)
341 cm.check_and_sign(nb, path)
326 assert not cm.notary.check_signature(nb)
342 assert not cm.notary.check_signature(nb)
327
343
328 cm.trust_notebook(name, path)
344 cm.trust_notebook(path)
329 nb = cm.get_model(name, path)['content']
345 nb = cm.get_model(path)['content']
330 cm.mark_trusted_cells(nb, name, path)
346 cm.mark_trusted_cells(nb, path)
331 cm.check_and_sign(nb, name, path)
347 cm.check_and_sign(nb, path)
332 assert cm.notary.check_signature(nb)
348 assert cm.notary.check_signature(nb)
@@ -36,10 +36,6 b' class SessionRootHandler(IPythonHandler):'
36 if model is None:
36 if model is None:
37 raise web.HTTPError(400, "No JSON data provided")
37 raise web.HTTPError(400, "No JSON data provided")
38 try:
38 try:
39 name = model['notebook']['name']
40 except KeyError:
41 raise web.HTTPError(400, "Missing field in JSON data: notebook.name")
42 try:
43 path = model['notebook']['path']
39 path = model['notebook']['path']
44 except KeyError:
40 except KeyError:
45 raise web.HTTPError(400, "Missing field in JSON data: notebook.path")
41 raise web.HTTPError(400, "Missing field in JSON data: notebook.path")
@@ -50,11 +46,11 b' class SessionRootHandler(IPythonHandler):'
50 kernel_name = None
46 kernel_name = None
51
47
52 # Check to see if session exists
48 # Check to see if session exists
53 if sm.session_exists(name=name, path=path):
49 if sm.session_exists(path=path):
54 model = sm.get_session(name=name, path=path)
50 model = sm.get_session(path=path)
55 else:
51 else:
56 try:
52 try:
57 model = sm.create_session(name=name, path=path, kernel_name=kernel_name)
53 model = sm.create_session(path=path, kernel_name=kernel_name)
58 except NoSuchKernel:
54 except NoSuchKernel:
59 msg = ("The '%s' kernel is not available. Please pick another "
55 msg = ("The '%s' kernel is not available. Please pick another "
60 "suitable kernel instead, or install that kernel." % kernel_name)
56 "suitable kernel instead, or install that kernel." % kernel_name)
@@ -92,8 +88,6 b' class SessionHandler(IPythonHandler):'
92 changes = {}
88 changes = {}
93 if 'notebook' in model:
89 if 'notebook' in model:
94 notebook = model['notebook']
90 notebook = model['notebook']
95 if 'name' in notebook:
96 changes['name'] = notebook['name']
97 if 'path' in notebook:
91 if 'path' in notebook:
98 changes['path'] = notebook['path']
92 changes['path'] = notebook['path']
99
93
@@ -21,7 +21,7 b' class SessionManager(LoggingConfigurable):'
21 # Session database initialized below
21 # Session database initialized below
22 _cursor = None
22 _cursor = None
23 _connection = None
23 _connection = None
24 _columns = {'session_id', 'name', 'path', 'kernel_id'}
24 _columns = {'session_id', 'path', 'kernel_id'}
25
25
26 @property
26 @property
27 def cursor(self):
27 def cursor(self):
@@ -29,7 +29,7 b' class SessionManager(LoggingConfigurable):'
29 if self._cursor is None:
29 if self._cursor is None:
30 self._cursor = self.connection.cursor()
30 self._cursor = self.connection.cursor()
31 self._cursor.execute("""CREATE TABLE session
31 self._cursor.execute("""CREATE TABLE session
32 (session_id, name, path, kernel_id)""")
32 (session_id, path, kernel_id)""")
33 return self._cursor
33 return self._cursor
34
34
35 @property
35 @property
@@ -44,9 +44,9 b' class SessionManager(LoggingConfigurable):'
44 """Close connection once SessionManager closes"""
44 """Close connection once SessionManager closes"""
45 self.cursor.close()
45 self.cursor.close()
46
46
47 def session_exists(self, name, path):
47 def session_exists(self, path):
48 """Check to see if the session for a given notebook exists"""
48 """Check to see if the session for a given notebook exists"""
49 self.cursor.execute("SELECT * FROM session WHERE name=? AND path=?", (name, path))
49 self.cursor.execute("SELECT * FROM session WHERE path=?", (path,))
50 reply = self.cursor.fetchone()
50 reply = self.cursor.fetchone()
51 if reply is None:
51 if reply is None:
52 return False
52 return False
@@ -57,17 +57,17 b' class SessionManager(LoggingConfigurable):'
57 "Create a uuid for a new session"
57 "Create a uuid for a new session"
58 return unicode_type(uuid.uuid4())
58 return unicode_type(uuid.uuid4())
59
59
60 def create_session(self, name=None, path=None, kernel_name=None):
60 def create_session(self, path=None, kernel_name=None):
61 """Creates a session and returns its model"""
61 """Creates a session and returns its model"""
62 session_id = self.new_session_id()
62 session_id = self.new_session_id()
63 # allow nbm to specify kernels cwd
63 # allow nbm to specify kernels cwd
64 kernel_path = self.contents_manager.get_kernel_path(name=name, path=path)
64 kernel_path = self.contents_manager.get_kernel_path(path=path)
65 kernel_id = self.kernel_manager.start_kernel(path=kernel_path,
65 kernel_id = self.kernel_manager.start_kernel(path=kernel_path,
66 kernel_name=kernel_name)
66 kernel_name=kernel_name)
67 return self.save_session(session_id, name=name, path=path,
67 return self.save_session(session_id, path=path,
68 kernel_id=kernel_id)
68 kernel_id=kernel_id)
69
69
70 def save_session(self, session_id, name=None, path=None, kernel_id=None):
70 def save_session(self, session_id, path=None, kernel_id=None):
71 """Saves the items for the session with the given session_id
71 """Saves the items for the session with the given session_id
72
72
73 Given a session_id (and any other of the arguments), this method
73 Given a session_id (and any other of the arguments), this method
@@ -78,10 +78,8 b' class SessionManager(LoggingConfigurable):'
78 ----------
78 ----------
79 session_id : str
79 session_id : str
80 uuid for the session; this method must be given a session_id
80 uuid for the session; this method must be given a session_id
81 name : str
82 the .ipynb notebook name that started the session
83 path : str
81 path : str
84 the path to the named notebook
82 the path for the given notebook
85 kernel_id : str
83 kernel_id : str
86 a uuid for the kernel associated with this session
84 a uuid for the kernel associated with this session
87
85
@@ -90,8 +88,8 b' class SessionManager(LoggingConfigurable):'
90 model : dict
88 model : dict
91 a dictionary of the session model
89 a dictionary of the session model
92 """
90 """
93 self.cursor.execute("INSERT INTO session VALUES (?,?,?,?)",
91 self.cursor.execute("INSERT INTO session VALUES (?,?,?)",
94 (session_id, name, path, kernel_id)
92 (session_id, path, kernel_id)
95 )
93 )
96 return self.get_session(session_id=session_id)
94 return self.get_session(session_id=session_id)
97
95
@@ -105,7 +103,7 b' class SessionManager(LoggingConfigurable):'
105 ----------
103 ----------
106 **kwargs : keyword argument
104 **kwargs : keyword argument
107 must be given one of the keywords and values from the session database
105 must be given one of the keywords and values from the session database
108 (i.e. session_id, name, path, kernel_id)
106 (i.e. session_id, path, kernel_id)
109
107
110 Returns
108 Returns
111 -------
109 -------
@@ -182,7 +180,6 b' class SessionManager(LoggingConfigurable):'
182 model = {
180 model = {
183 'id': row['session_id'],
181 'id': row['session_id'],
184 'notebook': {
182 'notebook': {
185 'name': row['name'],
186 'path': row['path']
183 'path': row['path']
187 },
184 },
188 'kernel': self.kernel_manager.kernel_model(row['kernel_id'])
185 'kernel': self.kernel_manager.kernel_model(row['kernel_id'])
@@ -32,24 +32,24 b' class TestSessionManager(TestCase):'
32
32
33 def test_get_session(self):
33 def test_get_session(self):
34 sm = SessionManager(kernel_manager=DummyMKM())
34 sm = SessionManager(kernel_manager=DummyMKM())
35 session_id = sm.create_session(name='test.ipynb', path='/path/to/',
35 session_id = sm.create_session(path='/path/to/test.ipynb',
36 kernel_name='bar')['id']
36 kernel_name='bar')['id']
37 model = sm.get_session(session_id=session_id)
37 model = sm.get_session(session_id=session_id)
38 expected = {'id':session_id,
38 expected = {'id':session_id,
39 'notebook':{'name':u'test.ipynb', 'path': u'/path/to/'},
39 'notebook':{'path': u'/path/to/test.ipynb'},
40 'kernel': {'id':u'A', 'name': 'bar'}}
40 'kernel': {'id':u'A', 'name': 'bar'}}
41 self.assertEqual(model, expected)
41 self.assertEqual(model, expected)
42
42
43 def test_bad_get_session(self):
43 def test_bad_get_session(self):
44 # Should raise error if a bad key is passed to the database.
44 # Should raise error if a bad key is passed to the database.
45 sm = SessionManager(kernel_manager=DummyMKM())
45 sm = SessionManager(kernel_manager=DummyMKM())
46 session_id = sm.create_session(name='test.ipynb', path='/path/to/',
46 session_id = sm.create_session(path='/path/to/test.ipynb',
47 kernel_name='foo')['id']
47 kernel_name='foo')['id']
48 self.assertRaises(TypeError, sm.get_session, bad_id=session_id) # Bad keyword
48 self.assertRaises(TypeError, sm.get_session, bad_id=session_id) # Bad keyword
49
49
50 def test_get_session_dead_kernel(self):
50 def test_get_session_dead_kernel(self):
51 sm = SessionManager(kernel_manager=DummyMKM())
51 sm = SessionManager(kernel_manager=DummyMKM())
52 session = sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python')
52 session = sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python')
53 # kill the kernel
53 # kill the kernel
54 sm.kernel_manager.shutdown_kernel(session['kernel']['id'])
54 sm.kernel_manager.shutdown_kernel(session['kernel']['id'])
55 with self.assertRaises(KeyError):
55 with self.assertRaises(KeyError):
@@ -61,24 +61,33 b' class TestSessionManager(TestCase):'
61 def test_list_sessions(self):
61 def test_list_sessions(self):
62 sm = SessionManager(kernel_manager=DummyMKM())
62 sm = SessionManager(kernel_manager=DummyMKM())
63 sessions = [
63 sessions = [
64 sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
64 sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'),
65 sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
65 sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'),
66 sm.create_session(name='test3.ipynb', path='/path/to/3/', kernel_name='python'),
66 sm.create_session(path='/path/to/3/test3.ipynb', kernel_name='python'),
67 ]
67 ]
68 sessions = sm.list_sessions()
68 sessions = sm.list_sessions()
69 expected = [{'id':sessions[0]['id'], 'notebook':{'name':u'test1.ipynb',
69 expected = [
70 'path': u'/path/to/1/'}, 'kernel':{'id':u'A', 'name':'python'}},
70 {
71 {'id':sessions[1]['id'], 'notebook': {'name':u'test2.ipynb',
71 'id':sessions[0]['id'],
72 'path': u'/path/to/2/'}, 'kernel':{'id':u'B', 'name':'python'}},
72 'notebook':{'path': u'/path/to/1/test1.ipynb'},
73 {'id':sessions[2]['id'], 'notebook':{'name':u'test3.ipynb',
73 'kernel':{'id':u'A', 'name':'python'}
74 'path': u'/path/to/3/'}, 'kernel':{'id':u'C', 'name':'python'}}]
74 }, {
75 'id':sessions[1]['id'],
76 'notebook': {'path': u'/path/to/2/test2.ipynb'},
77 'kernel':{'id':u'B', 'name':'python'}
78 }, {
79 'id':sessions[2]['id'],
80 'notebook':{'path': u'/path/to/3/test3.ipynb'},
81 'kernel':{'id':u'C', 'name':'python'}
82 }
83 ]
75 self.assertEqual(sessions, expected)
84 self.assertEqual(sessions, expected)
76
85
77 def test_list_sessions_dead_kernel(self):
86 def test_list_sessions_dead_kernel(self):
78 sm = SessionManager(kernel_manager=DummyMKM())
87 sm = SessionManager(kernel_manager=DummyMKM())
79 sessions = [
88 sessions = [
80 sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
89 sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'),
81 sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
90 sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'),
82 ]
91 ]
83 # kill one of the kernels
92 # kill one of the kernels
84 sm.kernel_manager.shutdown_kernel(sessions[0]['kernel']['id'])
93 sm.kernel_manager.shutdown_kernel(sessions[0]['kernel']['id'])
@@ -87,8 +96,7 b' class TestSessionManager(TestCase):'
87 {
96 {
88 'id': sessions[1]['id'],
97 'id': sessions[1]['id'],
89 'notebook': {
98 'notebook': {
90 'name': u'test2.ipynb',
99 'path': u'/path/to/2/test2.ipynb',
91 'path': u'/path/to/2/',
92 },
100 },
93 'kernel': {
101 'kernel': {
94 'id': u'B',
102 'id': u'B',
@@ -100,41 +108,47 b' class TestSessionManager(TestCase):'
100
108
101 def test_update_session(self):
109 def test_update_session(self):
102 sm = SessionManager(kernel_manager=DummyMKM())
110 sm = SessionManager(kernel_manager=DummyMKM())
103 session_id = sm.create_session(name='test.ipynb', path='/path/to/',
111 session_id = sm.create_session(path='/path/to/test.ipynb',
104 kernel_name='julia')['id']
112 kernel_name='julia')['id']
105 sm.update_session(session_id, name='new_name.ipynb')
113 sm.update_session(session_id, path='/path/to/new_name.ipynb')
106 model = sm.get_session(session_id=session_id)
114 model = sm.get_session(session_id=session_id)
107 expected = {'id':session_id,
115 expected = {'id':session_id,
108 'notebook':{'name':u'new_name.ipynb', 'path': u'/path/to/'},
116 'notebook':{'path': u'/path/to/new_name.ipynb'},
109 'kernel':{'id':u'A', 'name':'julia'}}
117 'kernel':{'id':u'A', 'name':'julia'}}
110 self.assertEqual(model, expected)
118 self.assertEqual(model, expected)
111
119
112 def test_bad_update_session(self):
120 def test_bad_update_session(self):
113 # try to update a session with a bad keyword ~ raise error
121 # try to update a session with a bad keyword ~ raise error
114 sm = SessionManager(kernel_manager=DummyMKM())
122 sm = SessionManager(kernel_manager=DummyMKM())
115 session_id = sm.create_session(name='test.ipynb', path='/path/to/',
123 session_id = sm.create_session(path='/path/to/test.ipynb',
116 kernel_name='ir')['id']
124 kernel_name='ir')['id']
117 self.assertRaises(TypeError, sm.update_session, session_id=session_id, bad_kw='test.ipynb') # Bad keyword
125 self.assertRaises(TypeError, sm.update_session, session_id=session_id, bad_kw='test.ipynb') # Bad keyword
118
126
119 def test_delete_session(self):
127 def test_delete_session(self):
120 sm = SessionManager(kernel_manager=DummyMKM())
128 sm = SessionManager(kernel_manager=DummyMKM())
121 sessions = [
129 sessions = [
122 sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
130 sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'),
123 sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
131 sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'),
124 sm.create_session(name='test3.ipynb', path='/path/to/3/', kernel_name='python'),
132 sm.create_session(path='/path/to/3/test3.ipynb', kernel_name='python'),
125 ]
133 ]
126 sm.delete_session(sessions[1]['id'])
134 sm.delete_session(sessions[1]['id'])
127 new_sessions = sm.list_sessions()
135 new_sessions = sm.list_sessions()
128 expected = [{'id':sessions[0]['id'], 'notebook':{'name':u'test1.ipynb',
136 expected = [{
129 'path': u'/path/to/1/'}, 'kernel':{'id':u'A', 'name':'python'}},
137 'id': sessions[0]['id'],
130 {'id':sessions[2]['id'], 'notebook':{'name':u'test3.ipynb',
138 'notebook': {'path': u'/path/to/1/test1.ipynb'},
131 'path': u'/path/to/3/'}, 'kernel':{'id':u'C', 'name':'python'}}]
139 'kernel': {'id':u'A', 'name':'python'}
140 }, {
141 'id': sessions[2]['id'],
142 'notebook': {'path': u'/path/to/3/test3.ipynb'},
143 'kernel': {'id':u'C', 'name':'python'}
144 }
145 ]
132 self.assertEqual(new_sessions, expected)
146 self.assertEqual(new_sessions, expected)
133
147
134 def test_bad_delete_session(self):
148 def test_bad_delete_session(self):
135 # try to delete a session that doesn't exist ~ raise error
149 # try to delete a session that doesn't exist ~ raise error
136 sm = SessionManager(kernel_manager=DummyMKM())
150 sm = SessionManager(kernel_manager=DummyMKM())
137 sm.create_session(name='test.ipynb', path='/path/to/', kernel_name='python')
151 sm.create_session(path='/path/to/test.ipynb', kernel_name='python')
138 self.assertRaises(TypeError, sm.delete_session, bad_kwarg='23424') # Bad keyword
152 self.assertRaises(TypeError, sm.delete_session, bad_kwarg='23424') # Bad keyword
139 self.assertRaises(web.HTTPError, sm.delete_session, session_id='23424') # nonexistant
153 self.assertRaises(web.HTTPError, sm.delete_session, session_id='23424') # nonexistant
140
154
@@ -38,13 +38,13 b' class SessionAPI(object):'
38 def get(self, id):
38 def get(self, id):
39 return self._req('GET', id)
39 return self._req('GET', id)
40
40
41 def create(self, name, path, kernel_name='python'):
41 def create(self, path, kernel_name='python'):
42 body = json.dumps({'notebook': {'name':name, 'path':path},
42 body = json.dumps({'notebook': {'path':path},
43 'kernel': {'name': kernel_name}})
43 'kernel': {'name': kernel_name}})
44 return self._req('POST', '', body)
44 return self._req('POST', '', body)
45
45
46 def modify(self, id, name, path):
46 def modify(self, id, path):
47 body = json.dumps({'notebook': {'name':name, 'path':path}})
47 body = json.dumps({'notebook': {'path':path}})
48 return self._req('PATCH', id, body)
48 return self._req('PATCH', id, body)
49
49
50 def delete(self, id):
50 def delete(self, id):
@@ -78,12 +78,11 b' class SessionAPITest(NotebookTestBase):'
78 sessions = self.sess_api.list().json()
78 sessions = self.sess_api.list().json()
79 self.assertEqual(len(sessions), 0)
79 self.assertEqual(len(sessions), 0)
80
80
81 resp = self.sess_api.create('nb1.ipynb', 'foo')
81 resp = self.sess_api.create('foo/nb1.ipynb')
82 self.assertEqual(resp.status_code, 201)
82 self.assertEqual(resp.status_code, 201)
83 newsession = resp.json()
83 newsession = resp.json()
84 self.assertIn('id', newsession)
84 self.assertIn('id', newsession)
85 self.assertEqual(newsession['notebook']['name'], 'nb1.ipynb')
85 self.assertEqual(newsession['notebook']['path'], 'foo/nb1.ipynb')
86 self.assertEqual(newsession['notebook']['path'], 'foo')
87 self.assertEqual(resp.headers['Location'], '/api/sessions/{0}'.format(newsession['id']))
86 self.assertEqual(resp.headers['Location'], '/api/sessions/{0}'.format(newsession['id']))
88
87
89 sessions = self.sess_api.list().json()
88 sessions = self.sess_api.list().json()
@@ -95,7 +94,7 b' class SessionAPITest(NotebookTestBase):'
95 self.assertEqual(got, newsession)
94 self.assertEqual(got, newsession)
96
95
97 def test_delete(self):
96 def test_delete(self):
98 newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
97 newsession = self.sess_api.create('foo/nb1.ipynb').json()
99 sid = newsession['id']
98 sid = newsession['id']
100
99
101 resp = self.sess_api.delete(sid)
100 resp = self.sess_api.delete(sid)
@@ -108,10 +107,9 b' class SessionAPITest(NotebookTestBase):'
108 self.sess_api.get(sid)
107 self.sess_api.get(sid)
109
108
110 def test_modify(self):
109 def test_modify(self):
111 newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
110 newsession = self.sess_api.create('foo/nb1.ipynb').json()
112 sid = newsession['id']
111 sid = newsession['id']
113
112
114 changed = self.sess_api.modify(sid, 'nb2.ipynb', '').json()
113 changed = self.sess_api.modify(sid, 'nb2.ipynb').json()
115 self.assertEqual(changed['id'], sid)
114 self.assertEqual(changed['id'], sid)
116 self.assertEqual(changed['notebook']['name'], 'nb2.ipynb')
115 self.assertEqual(changed['notebook']['path'], 'nb2.ipynb')
117 self.assertEqual(changed['notebook']['path'], '')
@@ -272,11 +272,11 b' define(['
272 } else {
272 } else {
273 line = "background-color: ";
273 line = "background-color: ";
274 }
274 }
275 line = line + "rgb(" + r + "," + g + "," + b + ");"
275 line = line + "rgb(" + r + "," + g + "," + b + ");";
276 if ( !attrs["style"] ) {
276 if ( !attrs.style ) {
277 attrs["style"] = line;
277 attrs.style = line;
278 } else {
278 } else {
279 attrs["style"] += " " + line;
279 attrs.style += " " + line;
280 }
280 }
281 }
281 }
282 }
282 }
@@ -285,7 +285,7 b' define(['
285 function ansispan(str) {
285 function ansispan(str) {
286 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
286 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
287 // regular ansi escapes (using the table above)
287 // regular ansi escapes (using the table above)
288 var is_open = false
288 var is_open = false;
289 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
289 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
290 if (!pattern) {
290 if (!pattern) {
291 // [(01|22|39|)m close spans
291 // [(01|22|39|)m close spans
@@ -313,7 +313,7 b' define(['
313 return span + ">";
313 return span + ">";
314 }
314 }
315 });
315 });
316 };
316 }
317
317
318 // Transform ANSI color escape codes into HTML <span> tags with css
318 // Transform ANSI color escape codes into HTML <span> tags with css
319 // classes listed in the above ansi_colormap object. The actual color used
319 // classes listed in the above ansi_colormap object. The actual color used
@@ -392,6 +392,18 b' define(['
392 return url;
392 return url;
393 };
393 };
394
394
395 var url_path_split = function (path) {
396 // Like os.path.split for URLs.
397 // Always returns two strings, the directory path and the base filename
398
399 var idx = path.lastIndexOf('/');
400 if (idx === -1) {
401 return ['', path];
402 } else {
403 return [ path.slice(0, idx), path.slice(idx + 1) ];
404 }
405 };
406
395 var parse_url = function (url) {
407 var parse_url = function (url) {
396 // an `a` element with an href allows attr-access to the parsed segments of a URL
408 // an `a` element with an href allows attr-access to the parsed segments of a URL
397 // a = parse_url("http://localhost:8888/path/name#hash")
409 // a = parse_url("http://localhost:8888/path/name#hash")
@@ -577,7 +589,7 b' define(['
577 wrapped_error.xhr_status = status;
589 wrapped_error.xhr_status = status;
578 wrapped_error.xhr_error = error;
590 wrapped_error.xhr_error = error;
579 return wrapped_error;
591 return wrapped_error;
580 }
592 };
581
593
582 var utils = {
594 var utils = {
583 regex_split : regex_split,
595 regex_split : regex_split,
@@ -588,6 +600,7 b' define(['
588 points_to_pixels : points_to_pixels,
600 points_to_pixels : points_to_pixels,
589 get_body_data : get_body_data,
601 get_body_data : get_body_data,
590 parse_url : parse_url,
602 parse_url : parse_url,
603 url_path_split : url_path_split,
591 url_path_join : url_path_join,
604 url_path_join : url_path_join,
592 url_join_encode : url_join_encode,
605 url_join_encode : url_join_encode,
593 encode_uri_components : encode_uri_components,
606 encode_uri_components : encode_uri_components,
@@ -151,6 +151,6 b' require(['
151 IPython.tooltip = notebook.tooltip;
151 IPython.tooltip = notebook.tooltip;
152
152
153 events.trigger('app_initialized.NotebookApp');
153 events.trigger('app_initialized.NotebookApp');
154 notebook.load_notebook(common_options.notebook_name, common_options.notebook_path);
154 notebook.load_notebook(common_options.notebook_path);
155
155
156 });
156 });
@@ -2,13 +2,14 b''
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 'base/js/namespace',
6 'jquery',
5 'jquery',
6 'base/js/namespace',
7 'base/js/dialog',
7 'base/js/utils',
8 'base/js/utils',
8 'notebook/js/tour',
9 'notebook/js/tour',
9 'bootstrap',
10 'bootstrap',
10 'moment',
11 'moment',
11 ], function(IPython, $, utils, tour, bootstrap, moment) {
12 ], function($, IPython, dialog, utils, tour, bootstrap, moment) {
12 "use strict";
13 "use strict";
13
14
14 var MenuBar = function (selector, options) {
15 var MenuBar = function (selector, options) {
@@ -89,14 +90,14 b' define(['
89 this.element.find('#new_notebook').click(function () {
90 this.element.find('#new_notebook').click(function () {
90 // Create a new notebook in the same path as the current
91 // Create a new notebook in the same path as the current
91 // notebook's path.
92 // notebook's path.
92 that.contents.new(that.notebook.notebook_path, null, {
93 var parent = utils.url_path_split(that.notebook.notebook_path)[0];
93 ext: ".ipynb",
94 that.contents.new_untitled(parent, {
95 type: "notebook",
94 extra_settings: {async: false}, // So we can open a new window afterwards
96 extra_settings: {async: false}, // So we can open a new window afterwards
95 success: function (data) {
97 success: function (data) {
96 window.open(
98 window.open(
97 utils.url_join_encode(
99 utils.url_join_encode(
98 that.base_url, 'notebooks',
100 that.base_url, 'notebooks', data.path
99 data.path, data.name
100 ), '_blank');
101 ), '_blank');
101 },
102 },
102 error: function(error) {
103 error: function(error) {
@@ -212,13 +212,13 b' define(['
212 });
212 });
213
213
214 this.events.on('kernel_ready.Kernel', function(event, data) {
214 this.events.on('kernel_ready.Kernel', function(event, data) {
215 var kinfo = data.kernel.info_reply
215 var kinfo = data.kernel.info_reply;
216 var langinfo = kinfo.language_info || {};
216 var langinfo = kinfo.language_info || {};
217 if (!langinfo.name) langinfo.name = kinfo.language;
217 if (!langinfo.name) langinfo.name = kinfo.language;
218
218
219 that.metadata.language_info = langinfo;
219 that.metadata.language_info = langinfo;
220 // Mode 'null' should be plain, unhighlighted text.
220 // Mode 'null' should be plain, unhighlighted text.
221 var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null'
221 var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null';
222 that.set_codemirror_mode(cm_mode);
222 that.set_codemirror_mode(cm_mode);
223 });
223 });
224
224
@@ -1029,7 +1029,7 b' define(['
1029 text = '';
1029 text = '';
1030 }
1030 }
1031 // metadata
1031 // metadata
1032 target_cell.metadata = source_cell.metadata
1032 target_cell.metadata = source_cell.metadata;
1033 // We must show the editor before setting its contents
1033 // We must show the editor before setting its contents
1034 target_cell.unrender();
1034 target_cell.unrender();
1035 target_cell.set_text(text);
1035 target_cell.set_text(text);
@@ -1231,8 +1231,6 b' define(['
1231 * @method split_cell
1231 * @method split_cell
1232 */
1232 */
1233 Notebook.prototype.split_cell = function () {
1233 Notebook.prototype.split_cell = function () {
1234 var mdc = textcell.MarkdownCell;
1235 var rc = textcell.RawCell;
1236 var cell = this.get_selected_cell();
1234 var cell = this.get_selected_cell();
1237 if (cell.is_splittable()) {
1235 if (cell.is_splittable()) {
1238 var texta = cell.get_pre_cursor();
1236 var texta = cell.get_pre_cursor();
@@ -1251,8 +1249,6 b' define(['
1251 * @method merge_cell_above
1249 * @method merge_cell_above
1252 */
1250 */
1253 Notebook.prototype.merge_cell_above = function () {
1251 Notebook.prototype.merge_cell_above = function () {
1254 var mdc = textcell.MarkdownCell;
1255 var rc = textcell.RawCell;
1256 var index = this.get_selected_index();
1252 var index = this.get_selected_index();
1257 var cell = this.get_cell(index);
1253 var cell = this.get_cell(index);
1258 var render = cell.rendered;
1254 var render = cell.rendered;
@@ -1288,8 +1284,6 b' define(['
1288 * @method merge_cell_below
1284 * @method merge_cell_below
1289 */
1285 */
1290 Notebook.prototype.merge_cell_below = function () {
1286 Notebook.prototype.merge_cell_below = function () {
1291 var mdc = textcell.MarkdownCell;
1292 var rc = textcell.RawCell;
1293 var index = this.get_selected_index();
1287 var index = this.get_selected_index();
1294 var cell = this.get_cell(index);
1288 var cell = this.get_cell(index);
1295 var render = cell.rendered;
1289 var render = cell.rendered;
@@ -1523,9 +1517,9 b' define(['
1523 }
1517 }
1524 this.codemirror_mode = newmode;
1518 this.codemirror_mode = newmode;
1525 codecell.CodeCell.options_default.cm_config.mode = newmode;
1519 codecell.CodeCell.options_default.cm_config.mode = newmode;
1526 modename = newmode.mode || newmode.name || newmode;
1520 var modename = newmode.mode || newmode.name || newmode;
1527
1521
1528 that = this;
1522 var that = this;
1529 utils.requireCodeMirrorMode(modename, function () {
1523 utils.requireCodeMirrorMode(modename, function () {
1530 $.map(that.get_cells(), function(cell, i) {
1524 $.map(that.get_cells(), function(cell, i) {
1531 if (cell.cell_type === 'code'){
1525 if (cell.cell_type === 'code'){
@@ -1547,7 +1541,6 b' define(['
1547 * @method start_session
1541 * @method start_session
1548 */
1542 */
1549 Notebook.prototype.start_session = function (kernel_name) {
1543 Notebook.prototype.start_session = function (kernel_name) {
1550 var that = this;
1551 if (this._session_starting) {
1544 if (this._session_starting) {
1552 throw new session.SessionAlreadyStarting();
1545 throw new session.SessionAlreadyStarting();
1553 }
1546 }
@@ -1629,7 +1622,6 b' define(['
1629 Notebook.prototype.execute_cell = function () {
1622 Notebook.prototype.execute_cell = function () {
1630 // mode = shift, ctrl, alt
1623 // mode = shift, ctrl, alt
1631 var cell = this.get_selected_cell();
1624 var cell = this.get_selected_cell();
1632 var cell_index = this.find_cell_index(cell);
1633
1625
1634 cell.execute();
1626 cell.execute();
1635 this.command_mode();
1627 this.command_mode();
@@ -1758,7 +1750,9 b' define(['
1758 * @param {String} name A new name for this notebook
1750 * @param {String} name A new name for this notebook
1759 */
1751 */
1760 Notebook.prototype.set_notebook_name = function (name) {
1752 Notebook.prototype.set_notebook_name = function (name) {
1753 var parent = utils.url_path_split(this.notebook_path)[0];
1761 this.notebook_name = name;
1754 this.notebook_name = name;
1755 this.notebook_path = utils.url_path_join(parent, name);
1762 };
1756 };
1763
1757
1764 /**
1758 /**
@@ -1795,6 +1789,7 b' define(['
1795 // Save the metadata and name.
1789 // Save the metadata and name.
1796 this.metadata = content.metadata;
1790 this.metadata = content.metadata;
1797 this.notebook_name = data.name;
1791 this.notebook_name = data.name;
1792 this.notebook_path = data.path;
1798 var trusted = true;
1793 var trusted = true;
1799
1794
1800 // Trigger an event changing the kernel spec - this will set the default
1795 // Trigger an event changing the kernel spec - this will set the default
@@ -1807,7 +1802,7 b' define(['
1807 if (this.metadata.language_info !== undefined) {
1802 if (this.metadata.language_info !== undefined) {
1808 var langinfo = this.metadata.language_info;
1803 var langinfo = this.metadata.language_info;
1809 // Mode 'null' should be plain, unhighlighted text.
1804 // Mode 'null' should be plain, unhighlighted text.
1810 var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null'
1805 var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null';
1811 this.set_codemirror_mode(cm_mode);
1806 this.set_codemirror_mode(cm_mode);
1812 }
1807 }
1813
1808
@@ -1900,8 +1895,6 b' define(['
1900 Notebook.prototype.save_notebook = function (extra_settings) {
1895 Notebook.prototype.save_notebook = function (extra_settings) {
1901 // Create a JSON model to be sent to the server.
1896 // Create a JSON model to be sent to the server.
1902 var model = {
1897 var model = {
1903 name : this.notebook_name,
1904 path : this.notebook_path,
1905 type : "notebook",
1898 type : "notebook",
1906 content : this.toJSON()
1899 content : this.toJSON()
1907 };
1900 };
@@ -1909,11 +1902,11 b' define(['
1909 var start = new Date().getTime();
1902 var start = new Date().getTime();
1910
1903
1911 var that = this;
1904 var that = this;
1912 this.contents.save(this.notebook_path, this.notebook_name, model, {
1905 this.contents.save(this.notebook_path, model, {
1913 extra_settings: extra_settings,
1906 extra_settings: extra_settings,
1914 success: $.proxy(this.save_notebook_success, this, start),
1907 success: $.proxy(this.save_notebook_success, this, start),
1915 error: function (error) {
1908 error: function (error) {
1916 that.events.trigger('notebook_save_failed.Notebook');
1909 that.events.trigger('notebook_save_failed.Notebook', error);
1917 }
1910 }
1918 });
1911 });
1919 };
1912 };
@@ -2031,15 +2024,15 b' define(['
2031
2024
2032 Notebook.prototype.copy_notebook = function(){
2025 Notebook.prototype.copy_notebook = function(){
2033 var base_url = this.base_url;
2026 var base_url = this.base_url;
2034 this.contents.copy(this.notebook_path, null, this.notebook_name, {
2027 var parent = utils.url_path_split(this.notebook_path)[0];
2028 this.contents.copy(this.notebook_path, parent, {
2035 // synchronous so we can open a new window on success
2029 // synchronous so we can open a new window on success
2036 extra_settings: {async: false},
2030 extra_settings: {async: false},
2037 success: function (data) {
2031 success: function (data) {
2038 window.open(utils.url_join_encode(
2032 window.open(utils.url_join_encode(
2039 base_url, 'notebooks', data.path, data.name
2033 base_url, 'notebooks', data.path
2040 ), '_blank');
2034 ), '_blank');
2041 },
2035 }
2042 error : utils.log_ajax_error
2043 });
2036 });
2044 };
2037 };
2045
2038
@@ -2049,11 +2042,13 b' define(['
2049 }
2042 }
2050
2043
2051 var that = this;
2044 var that = this;
2052 this.contents.rename(this.notebook_path, this.notebook_name,
2045 var parent = utils.url_path_split(this.notebook_path)[0];
2053 this.notebook_path, new_name, {
2046 var new_path = utils.url_path_join(parent, new_name);
2047 this.contents.rename(this.notebook_path, new_path, {
2054 success: function (json) {
2048 success: function (json) {
2055 var name = that.notebook_name = json.name;
2049 that.notebook_name = json.name;
2056 that.session.rename_notebook(name, json.path);
2050 that.notebook_path = json.path;
2051 that.session.rename_notebook(json.path);
2057 that.events.trigger('notebook_renamed.Notebook', json);
2052 that.events.trigger('notebook_renamed.Notebook', json);
2058 },
2053 },
2059 error: $.proxy(this.rename_error, this)
2054 error: $.proxy(this.rename_error, this)
@@ -2061,7 +2056,7 b' define(['
2061 };
2056 };
2062
2057
2063 Notebook.prototype.delete = function () {
2058 Notebook.prototype.delete = function () {
2064 this.contents.delete(this.notebook_name, this.notebook_path);
2059 this.contents.delete(this.notebook_path);
2065 };
2060 };
2066
2061
2067 Notebook.prototype.rename_error = function (error) {
2062 Notebook.prototype.rename_error = function (error) {
@@ -2100,13 +2095,13 b' define(['
2100 * Request a notebook's data from the server.
2095 * Request a notebook's data from the server.
2101 *
2096 *
2102 * @method load_notebook
2097 * @method load_notebook
2103 * @param {String} notebook_name and path A notebook to load
2098 * @param {String} notebook_path A notebook to load
2104 */
2099 */
2105 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
2100 Notebook.prototype.load_notebook = function (notebook_path) {
2106 this.notebook_name = notebook_name;
2107 this.notebook_path = notebook_path;
2101 this.notebook_path = notebook_path;
2102 this.notebook_name = utils.url_path_split(this.notebook_path)[1];
2108 this.events.trigger('notebook_loading.Notebook');
2103 this.events.trigger('notebook_loading.Notebook');
2109 this.contents.load(notebook_path, notebook_name, {
2104 this.contents.get(notebook_path, {
2110 success: $.proxy(this.load_notebook_success, this),
2105 success: $.proxy(this.load_notebook_success, this),
2111 error: $.proxy(this.load_notebook_error, this)
2106 error: $.proxy(this.load_notebook_error, this)
2112 });
2107 });
@@ -2121,7 +2116,7 b' define(['
2121 * @param {Object} data JSON representation of a notebook
2116 * @param {Object} data JSON representation of a notebook
2122 */
2117 */
2123 Notebook.prototype.load_notebook_success = function (data) {
2118 Notebook.prototype.load_notebook_success = function (data) {
2124 var failed;
2119 var failed, msg;
2125 try {
2120 try {
2126 this.fromJSON(data);
2121 this.fromJSON(data);
2127 } catch (e) {
2122 } catch (e) {
@@ -2146,12 +2141,11 b' define(['
2146 }
2141 }
2147
2142
2148 if (data.message) {
2143 if (data.message) {
2149 var msg;
2150 if (failed) {
2144 if (failed) {
2151 msg = "The notebook also failed validation:"
2145 msg = "The notebook also failed validation:";
2152 } else {
2146 } else {
2153 msg = "An invalid notebook may not function properly." +
2147 msg = "An invalid notebook may not function properly." +
2154 " The validation error was:"
2148 " The validation error was:";
2155 }
2149 }
2156 body.append($("<p>").text(
2150 body.append($("<p>").text(
2157 msg
2151 msg
@@ -2192,7 +2186,7 b' define(['
2192 src = " a newer notebook format ";
2186 src = " a newer notebook format ";
2193 }
2187 }
2194
2188
2195 var msg = "This notebook has been converted from" + src +
2189 msg = "This notebook has been converted from" + src +
2196 "(v"+orig_nbformat+") to the current notebook " +
2190 "(v"+orig_nbformat+") to the current notebook " +
2197 "format (v"+nbmodel.nbformat+"). The next time you save this notebook, the " +
2191 "format (v"+nbmodel.nbformat+"). The next time you save this notebook, the " +
2198 "current notebook format will be used.";
2192 "current notebook format will be used.";
@@ -2219,7 +2213,7 b' define(['
2219 var that = this;
2213 var that = this;
2220 var orig_vs = 'v' + nbmodel.nbformat + '.' + orig_nbformat_minor;
2214 var orig_vs = 'v' + nbmodel.nbformat + '.' + orig_nbformat_minor;
2221 var this_vs = 'v' + nbmodel.nbformat + '.' + this.nbformat_minor;
2215 var this_vs = 'v' + nbmodel.nbformat + '.' + this.nbformat_minor;
2222 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2216 msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2223 this_vs + ". You can still work with this notebook, but some features " +
2217 this_vs + ". You can still work with this notebook, but some features " +
2224 "introduced in later notebook versions may not be available.";
2218 "introduced in later notebook versions may not be available.";
2225
2219
@@ -2270,7 +2264,7 b' define(['
2270 Notebook.prototype.load_notebook_error = function (error) {
2264 Notebook.prototype.load_notebook_error = function (error) {
2271 this.events.trigger('notebook_load_failed.Notebook', error);
2265 this.events.trigger('notebook_load_failed.Notebook', error);
2272 var msg;
2266 var msg;
2273 if (error.name = utils.XHR_ERROR && error.xhr.status === 500) {
2267 if (error.name === utils.XHR_ERROR && error.xhr.status === 500) {
2274 utils.log_ajax_error(error.xhr, error.xhr_status, error.xhr_error);
2268 utils.log_ajax_error(error.xhr, error.xhr_status, error.xhr_error);
2275 msg = "An unknown error occurred while loading this notebook. " +
2269 msg = "An unknown error occurred while loading this notebook. " +
2276 "This version can load notebook formats " +
2270 "This version can load notebook formats " +
@@ -2330,10 +2324,10 b' define(['
2330 */
2324 */
2331 Notebook.prototype.list_checkpoints = function () {
2325 Notebook.prototype.list_checkpoints = function () {
2332 var that = this;
2326 var that = this;
2333 this.contents.list_checkpoints(this.notebook_path, this.notebook_name, {
2327 this.contents.list_checkpoints(this.notebook_path, {
2334 success: $.proxy(this.list_checkpoints_success, this),
2328 success: $.proxy(this.list_checkpoints_success, this),
2335 error: function(error) {
2329 error: function(error) {
2336 that.events.trigger('list_checkpoints_failed.Notebook');
2330 that.events.trigger('list_checkpoints_failed.Notebook', error);
2337 }
2331 }
2338 });
2332 });
2339 };
2333 };
@@ -2362,10 +2356,10 b' define(['
2362 */
2356 */
2363 Notebook.prototype.create_checkpoint = function () {
2357 Notebook.prototype.create_checkpoint = function () {
2364 var that = this;
2358 var that = this;
2365 this.contents.create_checkpoint(this.notebook_path, this.notebook_name, {
2359 this.contents.create_checkpoint(this.notebook_path, {
2366 success: $.proxy(this.create_checkpoint_success, this),
2360 success: $.proxy(this.create_checkpoint_success, this),
2367 error: function (error) {
2361 error: function (error) {
2368 that.events.trigger('checkpoint_failed.Notebook');
2362 that.events.trigger('checkpoint_failed.Notebook', error);
2369 }
2363 }
2370 });
2364 });
2371 };
2365 };
@@ -2432,11 +2426,11 b' define(['
2432 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2426 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2433 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2427 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2434 var that = this;
2428 var that = this;
2435 this.contents.restore_checkpoint(this.notebook_path, this.notebook_name,
2429 this.contents.restore_checkpoint(this.notebook_path,
2436 checkpoint, {
2430 checkpoint, {
2437 success: $.proxy(this.restore_checkpoint_success, this),
2431 success: $.proxy(this.restore_checkpoint_success, this),
2438 error: function (error) {
2432 error: function (error) {
2439 that.events.trigger('checkpoint_restore_failed.Notebook');
2433 that.events.trigger('checkpoint_restore_failed.Notebook', error);
2440 }
2434 }
2441 });
2435 });
2442 };
2436 };
@@ -2448,7 +2442,7 b' define(['
2448 */
2442 */
2449 Notebook.prototype.restore_checkpoint_success = function () {
2443 Notebook.prototype.restore_checkpoint_success = function () {
2450 this.events.trigger('checkpoint_restored.Notebook');
2444 this.events.trigger('checkpoint_restored.Notebook');
2451 this.load_notebook(this.notebook_name, this.notebook_path);
2445 this.load_notebook(this.notebook_path);
2452 };
2446 };
2453
2447
2454 /**
2448 /**
@@ -2460,7 +2454,7 b' define(['
2460 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2454 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2461 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2455 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2462 var that = this;
2456 var that = this;
2463 this.contents.delete_checkpoint(this.notebook_path, this.notebook_name,
2457 this.contents.delete_checkpoint(this.notebook_path,
2464 checkpoint, {
2458 checkpoint, {
2465 success: $.proxy(this.delete_checkpoint_success, this),
2459 success: $.proxy(this.delete_checkpoint_success, this),
2466 error: function (error) {
2460 error: function (error) {
@@ -2476,7 +2470,7 b' define(['
2476 */
2470 */
2477 Notebook.prototype.delete_checkpoint_success = function () {
2471 Notebook.prototype.delete_checkpoint_success = function () {
2478 this.events.trigger('checkpoint_deleted.Notebook');
2472 this.events.trigger('checkpoint_deleted.Notebook');
2479 this.load_notebook(this.notebook_name, this.notebook_path);
2473 this.load_notebook(this.notebook_path);
2480 };
2474 };
2481
2475
2482
2476
@@ -122,14 +122,12 b' define(['
122
122
123 SaveWidget.prototype.update_address_bar = function(){
123 SaveWidget.prototype.update_address_bar = function(){
124 var base_url = this.notebook.base_url;
124 var base_url = this.notebook.base_url;
125 var nbname = this.notebook.notebook_name;
126 var path = this.notebook.notebook_path;
125 var path = this.notebook.notebook_path;
127 var state = {path : path, name: nbname};
126 var state = {path : path};
128 window.history.replaceState(state, "", utils.url_join_encode(
127 window.history.replaceState(state, "", utils.url_join_encode(
129 base_url,
128 base_url,
130 "notebooks",
129 "notebooks",
131 path,
130 path)
132 nbname)
133 );
131 );
134 };
132 };
135
133
@@ -199,7 +197,7 b' define(['
199 $.proxy(that._regularly_update_checkpoint_date, that),
197 $.proxy(that._regularly_update_checkpoint_date, that),
200 t + 1000
198 t + 1000
201 );
199 );
202 }
200 };
203 var tdelta = Math.ceil(new Date()-this._checkpoint_date);
201 var tdelta = Math.ceil(new Date()-this._checkpoint_date);
204
202
205 // update regularly for the first 6hours and show
203 // update regularly for the first 6hours and show
@@ -29,8 +29,9 b' define(['
29 // An error representing the result of attempting to delete a non-empty
29 // An error representing the result of attempting to delete a non-empty
30 // directory.
30 // directory.
31 this.message = 'A directory must be empty before being deleted.';
31 this.message = 'A directory must be empty before being deleted.';
32 }
32 };
33 Contents.DirectoryNotEmptyError.prototype = new Error;
33
34 Contents.DirectoryNotEmptyError.prototype = Object.create(Error.prototype);
34 Contents.DirectoryNotEmptyError.prototype.name =
35 Contents.DirectoryNotEmptyError.prototype.name =
35 Contents.DIRECTORY_NOT_EMPTY_ERROR;
36 Contents.DIRECTORY_NOT_EMPTY_ERROR;
36
37
@@ -54,29 +55,28 b' define(['
54 */
55 */
55 Contents.prototype.create_basic_error_handler = function(callback) {
56 Contents.prototype.create_basic_error_handler = function(callback) {
56 if (!callback) {
57 if (!callback) {
57 return function(xhr, status, error) { };
58 return utils.log_ajax_error;
58 }
59 }
59 return function(xhr, status, error) {
60 return function(xhr, status, error) {
60 callback(utils.wrap_ajax_error(xhr, status, error));
61 callback(utils.wrap_ajax_error(xhr, status, error));
61 };
62 };
62 }
63 };
63
64
64 /**
65 /**
65 * File Functions (including notebook operations)
66 * File Functions (including notebook operations)
66 */
67 */
67
68
68 /**
69 /**
69 * Load a file.
70 * Get a file.
70 *
71 *
71 * Calls success with file JSON model, or error with error.
72 * Calls success with file JSON model, or error with error.
72 *
73 *
73 * @method load_notebook
74 * @method get
74 * @param {String} path
75 * @param {String} path
75 * @param {String} name
76 * @param {Function} success
76 * @param {Function} success
77 * @param {Function} error
77 * @param {Function} error
78 */
78 */
79 Contents.prototype.load = function (path, name, options) {
79 Contents.prototype.get = function (path, options) {
80 // We do the call with settings so we can set cache to false.
80 // We do the call with settings so we can set cache to false.
81 var settings = {
81 var settings = {
82 processData : false,
82 processData : false,
@@ -86,32 +86,29 b' define(['
86 success : options.success,
86 success : options.success,
87 error : this.create_basic_error_handler(options.error)
87 error : this.create_basic_error_handler(options.error)
88 };
88 };
89 var url = this.api_url(path, name);
89 var url = this.api_url(path);
90 $.ajax(url, settings);
90 $.ajax(url, settings);
91 };
91 };
92
92
93
93
94 /**
94 /**
95 * Creates a new notebook file at the specified directory path.
95 * Creates a new untitled file or directory in the specified directory path.
96 *
96 *
97 * @method scroll_to_cell
97 * @method new
98 * @param {String} path The path to create the new notebook at
98 * @param {String} path: the directory in which to create the new file/directory
99 * @param {String} name Name for new file. Chosen by server if unspecified.
100 * @param {Object} options:
99 * @param {Object} options:
101 * ext: file extension to use if name unspecified
100 * ext: file extension to use
101 * type: model type to create ('notebook', 'file', or 'directory')
102 */
102 */
103 Contents.prototype.new = function(path, name, options) {
103 Contents.prototype.new_untitled = function(path, options) {
104 var method, data;
104 var data = JSON.stringify({
105 if (name) {
105 ext: options.ext,
106 method = "PUT";
106 type: options.type
107 } else {
107 });
108 method = "POST";
109 data = JSON.stringify({ext: options.ext || ".ipynb"});
110 }
111
108
112 var settings = {
109 var settings = {
113 processData : false,
110 processData : false,
114 type : method,
111 type : "POST",
115 data: data,
112 data: data,
116 dataType : "json",
113 dataType : "json",
117 success : options.success || function() {},
114 success : options.success || function() {},
@@ -123,9 +120,8 b' define(['
123 $.ajax(this.api_url(path), settings);
120 $.ajax(this.api_url(path), settings);
124 };
121 };
125
122
126 Contents.prototype.delete = function(name, path, options) {
123 Contents.prototype.delete = function(path, options) {
127 var error_callback = options.error || function() {};
124 var error_callback = options.error || function() {};
128 var that = this;
129 var settings = {
125 var settings = {
130 processData : false,
126 processData : false,
131 type : "DELETE",
127 type : "DELETE",
@@ -140,12 +136,12 b' define(['
140 error_callback(utils.wrap_ajax_error(xhr, status, error));
136 error_callback(utils.wrap_ajax_error(xhr, status, error));
141 }
137 }
142 };
138 };
143 var url = this.api_url(path, name);
139 var url = this.api_url(path);
144 $.ajax(url, settings);
140 $.ajax(url, settings);
145 };
141 };
146
142
147 Contents.prototype.rename = function(path, name, new_path, new_name, options) {
143 Contents.prototype.rename = function(path, new_path, options) {
148 var data = {name: new_name, path: new_path};
144 var data = {path: new_path};
149 var settings = {
145 var settings = {
150 processData : false,
146 processData : false,
151 type : "PATCH",
147 type : "PATCH",
@@ -155,11 +151,11 b' define(['
155 success : options.success || function() {},
151 success : options.success || function() {},
156 error : this.create_basic_error_handler(options.error)
152 error : this.create_basic_error_handler(options.error)
157 };
153 };
158 var url = this.api_url(path, name);
154 var url = this.api_url(path);
159 $.ajax(url, settings);
155 $.ajax(url, settings);
160 };
156 };
161
157
162 Contents.prototype.save = function(path, name, model, options) {
158 Contents.prototype.save = function(path, model, options) {
163 // We do the call with settings so we can set cache to false.
159 // We do the call with settings so we can set cache to false.
164 var settings = {
160 var settings = {
165 processData : false,
161 processData : false,
@@ -172,24 +168,19 b' define(['
172 if (options.extra_settings) {
168 if (options.extra_settings) {
173 $.extend(settings, options.extra_settings);
169 $.extend(settings, options.extra_settings);
174 }
170 }
175 var url = this.api_url(path, name);
171 var url = this.api_url(path);
176 $.ajax(url, settings);
172 $.ajax(url, settings);
177 };
173 };
178
174
179 Contents.prototype.copy = function(to_path, to_name, from, options) {
175 Contents.prototype.copy = function(from_file, to_dir, options) {
180 var url, method;
176 // Copy a file into a given directory via POST
181 if (to_name) {
177 // The server will select the name of the copied file
182 url = this.api_url(to_path, to_name);
178 var url = this.api_url(to_dir);
183 method = "PUT";
184 } else {
185 url = this.api_url(to_path);
186 method = "POST";
187 }
188
179
189 var settings = {
180 var settings = {
190 processData : false,
181 processData : false,
191 type: method,
182 type: "POST",
192 data: JSON.stringify({copy_from: from}),
183 data: JSON.stringify({copy_from: from_file}),
193 dataType : "json",
184 dataType : "json",
194 success: options.success || function() {},
185 success: options.success || function() {},
195 error: this.create_basic_error_handler(options.error)
186 error: this.create_basic_error_handler(options.error)
@@ -204,8 +195,8 b' define(['
204 * Checkpointing Functions
195 * Checkpointing Functions
205 */
196 */
206
197
207 Contents.prototype.create_checkpoint = function(path, name, options) {
198 Contents.prototype.create_checkpoint = function(path, options) {
208 var url = this.api_url(path, name, 'checkpoints');
199 var url = this.api_url(path, 'checkpoints');
209 var settings = {
200 var settings = {
210 type : "POST",
201 type : "POST",
211 success: options.success || function() {},
202 success: options.success || function() {},
@@ -214,8 +205,8 b' define(['
214 $.ajax(url, settings);
205 $.ajax(url, settings);
215 };
206 };
216
207
217 Contents.prototype.list_checkpoints = function(path, name, options) {
208 Contents.prototype.list_checkpoints = function(path, options) {
218 var url = this.api_url(path, name, 'checkpoints');
209 var url = this.api_url(path, 'checkpoints');
219 var settings = {
210 var settings = {
220 type : "GET",
211 type : "GET",
221 success: options.success,
212 success: options.success,
@@ -224,8 +215,8 b' define(['
224 $.ajax(url, settings);
215 $.ajax(url, settings);
225 };
216 };
226
217
227 Contents.prototype.restore_checkpoint = function(path, name, checkpoint_id, options) {
218 Contents.prototype.restore_checkpoint = function(path, checkpoint_id, options) {
228 var url = this.api_url(path, name, 'checkpoints', checkpoint_id);
219 var url = this.api_url(path, 'checkpoints', checkpoint_id);
229 var settings = {
220 var settings = {
230 type : "POST",
221 type : "POST",
231 success: options.success || function() {},
222 success: options.success || function() {},
@@ -234,8 +225,8 b' define(['
234 $.ajax(url, settings);
225 $.ajax(url, settings);
235 };
226 };
236
227
237 Contents.prototype.delete_checkpoint = function(path, name, checkpoint_id, options) {
228 Contents.prototype.delete_checkpoint = function(path, checkpoint_id, options) {
238 var url = this.api_url(path, name, 'checkpoints', checkpoint_id);
229 var url = this.api_url(path, 'checkpoints', checkpoint_id);
239 var settings = {
230 var settings = {
240 type : "DELETE",
231 type : "DELETE",
241 success: options.success || function() {},
232 success: options.success || function() {},
@@ -255,10 +246,8 b' define(['
255 * representing individual files or directories. Each dictionary has
246 * representing individual files or directories. Each dictionary has
256 * the keys:
247 * the keys:
257 * type: "notebook" or "directory"
248 * type: "notebook" or "directory"
258 * name: the name of the file or directory
259 * created: created date
249 * created: created date
260 * last_modified: last modified dat
250 * last_modified: last modified dat
261 * path: the path
262 * @method list_notebooks
251 * @method list_notebooks
263 * @param {String} path The path to list notebooks in
252 * @param {String} path The path to list notebooks in
264 * @param {Function} load_callback called with list of notebooks on success
253 * @param {Function} load_callback called with list of notebooks on success
@@ -15,7 +15,6 b' define(['
15 * all other operations, the kernel object should be used.
15 * all other operations, the kernel object should be used.
16 *
16 *
17 * Options should include:
17 * Options should include:
18 * - notebook_name: the notebook name
19 * - notebook_path: the path (not including name) to the notebook
18 * - notebook_path: the path (not including name) to the notebook
20 * - kernel_name: the type of kernel (e.g. python3)
19 * - kernel_name: the type of kernel (e.g. python3)
21 * - base_url: the root url of the notebook server
20 * - base_url: the root url of the notebook server
@@ -28,7 +27,6 b' define(['
28 var Session = function (options) {
27 var Session = function (options) {
29 this.id = null;
28 this.id = null;
30 this.notebook_model = {
29 this.notebook_model = {
31 name: options.notebook_name,
32 path: options.notebook_path
30 path: options.notebook_path
33 };
31 };
34 this.kernel_model = {
32 this.kernel_model = {
@@ -154,15 +152,11 b' define(['
154 * undefined, then they will not be changed.
152 * undefined, then they will not be changed.
155 *
153 *
156 * @function rename_notebook
154 * @function rename_notebook
157 * @param {string} [name] - new notebook name
155 * @param {string} [path] - new notebook path
158 * @param {string} [path] - new path to notebook
159 * @param {function} [success] - function executed on ajax success
156 * @param {function} [success] - function executed on ajax success
160 * @param {function} [error] - functon executed on ajax error
157 * @param {function} [error] - functon executed on ajax error
161 */
158 */
162 Session.prototype.rename_notebook = function (name, path, success, error) {
159 Session.prototype.rename_notebook = function (path, success, error) {
163 if (name !== undefined) {
164 this.notebook_model.name = name;
165 }
166 if (path !== undefined) {
160 if (path !== undefined) {
167 this.notebook_model.path = path;
161 this.notebook_model.path = path;
168 }
162 }
@@ -208,7 +202,6 b' define(['
208 * fresh. If options are given, they can include any of the
202 * fresh. If options are given, they can include any of the
209 * following:
203 * following:
210 *
204 *
211 * - notebook_name - the name of the notebook
212 * - notebook_path - the path to the notebook
205 * - notebook_path - the path to the notebook
213 * - kernel_name - the name (type) of the kernel
206 * - kernel_name - the name (type) of the kernel
214 *
207 *
@@ -220,9 +213,6 b' define(['
220 Session.prototype.restart = function (options, success, error) {
213 Session.prototype.restart = function (options, success, error) {
221 var that = this;
214 var that = this;
222 var start = function () {
215 var start = function () {
223 if (options && options.notebook_name) {
224 that.notebook_model.name = options.notebook_name;
225 }
226 if (options && options.notebook_path) {
216 if (options && options.notebook_path) {
227 that.notebook_model.path = options.notebook_path;
217 that.notebook_model.path = options.notebook_path;
228 }
218 }
@@ -238,8 +228,8 b' define(['
238 // Helper functions
228 // Helper functions
239
229
240 /**
230 /**
241 * Get the data model for the session, which includes the notebook
231 * Get the data model for the session, which includes the notebook path
242 * (name and path) and kernel (name and id).
232 * and kernel (name and id).
243 *
233 *
244 * @function _get_model
234 * @function _get_model
245 * @returns {Object} - the data model
235 * @returns {Object} - the data model
@@ -266,7 +256,6 b' define(['
266 this.session_url = utils.url_join_encode(this.session_service_url, this.id);
256 this.session_url = utils.url_join_encode(this.session_service_url, this.id);
267 }
257 }
268 if (data && data.notebook) {
258 if (data && data.notebook) {
269 this.notebook_model.name = data.notebook.name;
270 this.notebook_model.path = data.notebook.path;
259 this.notebook_model.path = data.notebook.path;
271 }
260 }
272 if (data && data.kernel) {
261 if (data && data.kernel) {
@@ -2,8 +2,9 b''
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 require([
4 require([
5 'base/js/namespace',
6 'jquery',
5 'jquery',
6 'base/js/namespace',
7 'base/js/dialog',
7 'base/js/events',
8 'base/js/events',
8 'base/js/page',
9 'base/js/page',
9 'base/js/utils',
10 'base/js/utils',
@@ -19,18 +20,20 b' require(['
19 'bootstrap',
20 'bootstrap',
20 'custom/custom',
21 'custom/custom',
21 ], function(
22 ], function(
22 IPython,
23 $,
23 $,
24 IPython,
25 dialog,
24 events,
26 events,
25 page,
27 page,
26 utils,
28 utils,
27 contents,
29 contents_service,
28 notebooklist,
30 notebooklist,
29 clusterlist,
31 clusterlist,
30 sesssionlist,
32 sesssionlist,
31 kernellist,
33 kernellist,
32 terminallist,
34 terminallist,
33 loginwidget){
35 loginwidget){
36 "use strict";
34
37
35 page = new page.Page();
38 page = new page.Page();
36
39
@@ -38,36 +41,37 b' require(['
38 base_url: utils.get_body_data("baseUrl"),
41 base_url: utils.get_body_data("baseUrl"),
39 notebook_path: utils.get_body_data("notebookPath"),
42 notebook_path: utils.get_body_data("notebookPath"),
40 };
43 };
41 session_list = new sesssionlist.SesssionList($.extend({
44 var session_list = new sesssionlist.SesssionList($.extend({
42 events: events},
45 events: events},
43 common_options));
46 common_options));
44 contents = new contents.Contents($.extend({
47 var contents = new contents_service.Contents($.extend({
45 events: events},
48 events: events},
46 common_options));
49 common_options));
47 notebook_list = new notebooklist.NotebookList('#notebook_list', $.extend({
50 var notebook_list = new notebooklist.NotebookList('#notebook_list', $.extend({
48 contents: contents,
51 contents: contents,
49 session_list: session_list},
52 session_list: session_list},
50 common_options));
53 common_options));
51 cluster_list = new clusterlist.ClusterList('#cluster_list', common_options);
54 var cluster_list = new clusterlist.ClusterList('#cluster_list', common_options);
52 kernel_list = new kernellist.KernelList('#running_list', $.extend({
55 var kernel_list = new kernellist.KernelList('#running_list', $.extend({
53 session_list: session_list},
56 session_list: session_list},
54 common_options));
57 common_options));
55
58
59 var terminal_list;
56 if (utils.get_body_data("terminalsAvailable") === "True") {
60 if (utils.get_body_data("terminalsAvailable") === "True") {
57 terminal_list = new terminallist.TerminalList('#terminal_list', common_options);
61 terminal_list = new terminallist.TerminalList('#terminal_list', common_options);
58 }
62 }
59
63
60 login_widget = new loginwidget.LoginWidget('#login_widget', common_options);
64 var login_widget = new loginwidget.LoginWidget('#login_widget', common_options);
61
65
62 $('#new_notebook').click(function (e) {
66 $('#new_notebook').click(function (e) {
63 contents.new(common_options.notebook_path, null, {
67 contents.new_untitled(common_options.notebook_path, {
64 ext: ".ipynb",
68 type: "notebook",
65 extra_settings: {async: false}, // So we can open a new window afterwards
69 extra_settings: {async: false}, // So we can open a new window afterwards
66 success: function (data) {
70 success: function (data) {
67 window.open(
71 window.open(
68 utils.url_join_encode(
72 utils.url_join_encode(
69 common_options.base_url, 'notebooks',
73 common_options.base_url, 'notebooks',
70 data.path, data.name
74 data.path
71 ), '_blank');
75 ), '_blank');
72 },
76 },
73 error: function(error) {
77 error: function(error) {
@@ -100,7 +100,7 b' define(['
100 };
100 };
101 reader.onerror = function (event) {
101 reader.onerror = function (event) {
102 var item = $(event.target).data('item');
102 var item = $(event.target).data('item');
103 var name = item.data('name')
103 var name = item.data('name');
104 item.remove();
104 item.remove();
105 dialog.modal({
105 dialog.modal({
106 title : 'Failed to read file',
106 title : 'Failed to read file',
@@ -141,7 +141,7 b' define(['
141 };
141 };
142
142
143 NotebookList.prototype.load_list = function () {
143 NotebookList.prototype.load_list = function () {
144 var that = this
144 var that = this;
145 this.contents.list_contents(that.notebook_path, {
145 this.contents.list_contents(that.notebook_path, {
146 success: $.proxy(this.draw_notebook_list, this),
146 success: $.proxy(this.draw_notebook_list, this),
147 error: function(error) {
147 error: function(error) {
@@ -177,7 +177,7 b' define(['
177 model = {
177 model = {
178 type: 'directory',
178 type: 'directory',
179 name: '..',
179 name: '..',
180 path: path,
180 path: utils.url_path_split(path)[0],
181 };
181 };
182 this.add_link(model, item);
182 this.add_link(model, item);
183 offset += 1;
183 offset += 1;
@@ -240,8 +240,7 b' define(['
240 utils.url_join_encode(
240 utils.url_join_encode(
241 this.base_url,
241 this.base_url,
242 uri_prefix,
242 uri_prefix,
243 path,
243 path
244 name
245 )
244 )
246 );
245 );
247 // directory nav doesn't open new tabs
246 // directory nav doesn't open new tabs
@@ -311,7 +310,6 b' define(['
311 };
310 };
312
311
313 NotebookList.prototype.add_delete_button = function (item) {
312 NotebookList.prototype.add_delete_button = function (item) {
314 var new_buttons = $('<span/>').addClass("btn-group pull-right");
315 var notebooklist = this;
313 var notebooklist = this;
316 var delete_button = $("<button/>").text("Delete").addClass("btn btn-default btn-xs").
314 var delete_button = $("<button/>").text("Delete").addClass("btn btn-default btn-xs").
317 click(function (e) {
315 click(function (e) {
@@ -322,7 +320,7 b' define(['
322 var parent_item = that.parents('div.list_item');
320 var parent_item = that.parents('div.list_item');
323 var name = parent_item.data('nbname');
321 var name = parent_item.data('nbname');
324 var path = parent_item.data('path');
322 var path = parent_item.data('path');
325 var message = 'Are you sure you want to permanently delete the file: ' + nbname + '?';
323 var message = 'Are you sure you want to permanently delete the file: ' + name + '?';
326 dialog.modal({
324 dialog.modal({
327 title : "Delete file",
325 title : "Delete file",
328 body : message,
326 body : message,
@@ -330,9 +328,9 b' define(['
330 Delete : {
328 Delete : {
331 class: "btn-danger",
329 class: "btn-danger",
332 click: function() {
330 click: function() {
333 notebooklist.contents.delete(name, path, {
331 notebooklist.contents.delete(path, {
334 success: function() {
332 success: function() {
335 notebooklist.notebook_deleted(path, name);
333 notebooklist.notebook_deleted(path);
336 }
334 }
337 });
335 });
338 }
336 }
@@ -345,25 +343,24 b' define(['
345 item.find(".item_buttons").text("").append(delete_button);
343 item.find(".item_buttons").text("").append(delete_button);
346 };
344 };
347
345
348 NotebookList.prototype.notebook_deleted = function(path, name) {
346 NotebookList.prototype.notebook_deleted = function(path) {
349 // Remove the deleted notebook.
347 // Remove the deleted notebook.
350 $( ":data(nbname)" ).each(function() {
348 $( ":data(nbname)" ).each(function() {
351 var element = $( this );
349 var element = $(this);
352 if (element.data( "nbname" ) == d.name &&
350 if (element.data("path") == path) {
353 element.data( "path" ) == d.path) {
354 element.remove();
351 element.remove();
355 }
352 }
356 });
353 });
357 }
354 };
358
355
359
356
360 NotebookList.prototype.add_upload_button = function (item, type) {
357 NotebookList.prototype.add_upload_button = function (item) {
361 var that = this;
358 var that = this;
362 var upload_button = $('<button/>').text("Upload")
359 var upload_button = $('<button/>').text("Upload")
363 .addClass('btn btn-primary btn-xs upload_button')
360 .addClass('btn btn-primary btn-xs upload_button')
364 .click(function (e) {
361 .click(function (e) {
365 var path = that.notebook_path;
366 var filename = item.find('.item_name > input').val();
362 var filename = item.find('.item_name > input').val();
363 var path = utils.url_path_join(that.notebook_path, filename);
367 var filedata = item.data('filedata');
364 var filedata = item.data('filedata');
368 var format = 'text';
365 var format = 'text';
369 if (filename.length === 0 || filename[0] === '.') {
366 if (filename.length === 0 || filename[0] === '.') {
@@ -385,10 +382,7 b' define(['
385 filedata = btoa(bytes);
382 filedata = btoa(bytes);
386 format = 'base64';
383 format = 'base64';
387 }
384 }
388 var model = {
385 var model = {};
389 path: path,
390 name: filename
391 };
392
386
393 var name_and_ext = utils.splitext(filename);
387 var name_and_ext = utils.splitext(filename);
394 var file_ext = name_and_ext[1];
388 var file_ext = name_and_ext[1];
@@ -418,34 +412,22 b' define(['
418 model.content = filedata;
412 model.content = filedata;
419 content_type = 'application/octet-stream';
413 content_type = 'application/octet-stream';
420 }
414 }
421 var filedata = item.data('filedata');
415 filedata = item.data('filedata');
422
416
423 var settings = {
417 var settings = {
424 processData : false,
418 success : function () {
425 cache : false,
426 type : 'PUT',
427 data : JSON.stringify(model),
428 contentType: content_type,
429 success : function (data, status, xhr) {
430 item.removeClass('new-file');
419 item.removeClass('new-file');
431 that.add_link(model, item);
420 that.add_link(model, item);
432 that.add_delete_button(item);
421 that.add_delete_button(item);
433 that.session_list.load_sessions();
422 that.session_list.load_sessions();
434 },
423 },
435 error : utils.log_ajax_error,
436 };
424 };
437
438 var url = utils.url_join_encode(
439 that.base_url,
440 'api/contents',
441 that.notebook_path,
442 filename
443 );
444
425
445 var exists = false;
426 var exists = false;
446 $.each(that.element.find('.list_item:not(.new-file)'), function(k,v){
427 $.each(that.element.find('.list_item:not(.new-file)'), function(k,v){
447 if ($(v).data('name') === filename) { exists = true; return false; }
428 if ($(v).data('name') === filename) { exists = true; return false; }
448 });
429 });
430
449 if (exists) {
431 if (exists) {
450 dialog.modal({
432 dialog.modal({
451 title : "Replace file",
433 title : "Replace file",
@@ -453,7 +435,9 b' define(['
453 buttons : {
435 buttons : {
454 Overwrite : {
436 Overwrite : {
455 class: "btn-danger",
437 class: "btn-danger",
456 click: function() { $.ajax(url, settings); }
438 click: function () {
439 that.contents.save(path, model, settings);
440 }
457 },
441 },
458 Cancel : {
442 Cancel : {
459 click: function() { item.remove(); }
443 click: function() { item.remove(); }
@@ -461,7 +445,7 b' define(['
461 }
445 }
462 });
446 });
463 } else {
447 } else {
464 $.ajax(url, settings);
448 that.contents.save(path, model, settings);
465 }
449 }
466
450
467 return false;
451 return false;
@@ -478,7 +462,7 b' define(['
478 };
462 };
479
463
480
464
481 // Backwards compatability.
465 // Backwards compatability.
482 IPython.NotebookList = NotebookList;
466 IPython.NotebookList = NotebookList;
483
467
484 return {'NotebookList': NotebookList};
468 return {'NotebookList': NotebookList};
@@ -12,14 +12,14 b' casper.notebook_test(function () {'
12
12
13 this.thenEvaluate(function (nbname) {
13 this.thenEvaluate(function (nbname) {
14 require(['base/js/events'], function (events) {
14 require(['base/js/events'], function (events) {
15 IPython.notebook.notebook_name = nbname;
15 IPython.notebook.set_notebook_name(nbname);
16 IPython._save_success = IPython._save_failed = false;
16 IPython._save_success = IPython._save_failed = false;
17 events.on('notebook_saved.Notebook', function () {
17 events.on('notebook_saved.Notebook', function () {
18 IPython._save_success = true;
18 IPython._save_success = true;
19 });
19 });
20 events.on('notebook_save_failed.Notebook',
20 events.on('notebook_save_failed.Notebook',
21 function (event, xhr, status, error) {
21 function (event, error) {
22 IPython._save_failed = "save failed with " + xhr.status + xhr.responseText;
22 IPython._save_failed = "save failed with " + error;
23 });
23 });
24 IPython.notebook.save_notebook();
24 IPython.notebook.save_notebook();
25 });
25 });
@@ -42,6 +42,10 b' casper.notebook_test(function () {'
42 return IPython.notebook.notebook_name;
42 return IPython.notebook.notebook_name;
43 });
43 });
44 this.test.assertEquals(current_name, nbname, "Save with complicated name");
44 this.test.assertEquals(current_name, nbname, "Save with complicated name");
45 var current_path = this.evaluate(function(){
46 return IPython.notebook.notebook_path;
47 });
48 this.test.assertEquals(current_path, nbname, "path OK");
45 });
49 });
46
50
47 this.thenEvaluate(function(){
51 this.thenEvaluate(function(){
@@ -68,11 +72,8 b' casper.notebook_test(function () {'
68 });
72 });
69
73
70 this.then(function(){
74 this.then(function(){
71 var baseUrl = this.get_notebook_server();
75 this.open_dashboard();
72 this.open(baseUrl);
73 });
76 });
74
75 this.waitForSelector('.list_item');
76
77
77 this.then(function(){
78 this.then(function(){
78 var notebook_url = this.evaluate(function(nbname){
79 var notebook_url = this.evaluate(function(nbname){
@@ -92,11 +93,11 b' casper.notebook_test(function () {'
92 });
93 });
93
94
94 // wait for the notebook
95 // wait for the notebook
95 this.waitForSelector("#notebook");
96 this.waitFor(this.kernel_running);
96
97
97 this.waitFor(function(){
98 this.waitFor(function() {
98 return this.evaluate(function(){
99 return this.evaluate(function () {
99 return IPython.notebook || false;
100 return IPython && IPython.notebook && true;
100 });
101 });
101 });
102 });
102
103
@@ -75,22 +75,22 b' class TestInstallNBExtension(TestCase):'
75 td.cleanup()
75 td.cleanup()
76 nbextensions.get_ipython_dir = self.save_get_ipython_dir
76 nbextensions.get_ipython_dir = self.save_get_ipython_dir
77
77
78 def assert_path_exists(self, path):
78 def assert_dir_exists(self, path):
79 if not os.path.exists(path):
79 if not os.path.exists(path):
80 do_exist = os.listdir(os.path.dirname(path))
80 do_exist = os.listdir(os.path.dirname(path))
81 self.fail(u"%s should exist (found %s)" % (path, do_exist))
81 self.fail(u"%s should exist (found %s)" % (path, do_exist))
82
82
83 def assert_not_path_exists(self, path):
83 def assert_not_dir_exists(self, path):
84 if os.path.exists(path):
84 if os.path.exists(path):
85 self.fail(u"%s should not exist" % path)
85 self.fail(u"%s should not exist" % path)
86
86
87 def assert_installed(self, relative_path, ipdir=None):
87 def assert_installed(self, relative_path, ipdir=None):
88 self.assert_path_exists(
88 self.assert_dir_exists(
89 pjoin(ipdir or self.ipdir, u'nbextensions', relative_path)
89 pjoin(ipdir or self.ipdir, u'nbextensions', relative_path)
90 )
90 )
91
91
92 def assert_not_installed(self, relative_path, ipdir=None):
92 def assert_not_installed(self, relative_path, ipdir=None):
93 self.assert_not_path_exists(
93 self.assert_not_dir_exists(
94 pjoin(ipdir or self.ipdir, u'nbextensions', relative_path)
94 pjoin(ipdir or self.ipdir, u'nbextensions', relative_path)
95 )
95 )
96
96
@@ -99,7 +99,7 b' class TestInstallNBExtension(TestCase):'
99 with TemporaryDirectory() as td:
99 with TemporaryDirectory() as td:
100 ipdir = pjoin(td, u'ipython')
100 ipdir = pjoin(td, u'ipython')
101 install_nbextension(self.src, ipython_dir=ipdir)
101 install_nbextension(self.src, ipython_dir=ipdir)
102 self.assert_path_exists(ipdir)
102 self.assert_dir_exists(ipdir)
103 for file in self.files:
103 for file in self.files:
104 self.assert_installed(
104 self.assert_installed(
105 pjoin(basename(self.src), file),
105 pjoin(basename(self.src), file),
@@ -11,13 +11,15 b' casper.get_list_items = function () {'
11 });
11 });
12 };
12 };
13
13
14 casper.test_items = function (baseUrl) {
14 casper.test_items = function (baseUrl, visited) {
15 visited = visited || {};
15 casper.then(function () {
16 casper.then(function () {
16 var items = casper.get_list_items();
17 var items = casper.get_list_items();
17 casper.each(items, function (self, item) {
18 casper.each(items, function (self, item) {
18 if (!item.label.match(/\.ipynb$/)) {
19 if (item.link.match(/^\/tree\//)) {
19 var followed_url = baseUrl+item.link;
20 var followed_url = baseUrl+item.link;
20 if (!followed_url.match(/\/\.\.$/)) {
21 if (!visited[followed_url]) {
22 visited[followed_url] = true;
21 casper.thenOpen(followed_url, function () {
23 casper.thenOpen(followed_url, function () {
22 this.waitFor(this.page_loaded);
24 this.waitFor(this.page_loaded);
23 casper.wait_for_dashboard();
25 casper.wait_for_dashboard();
@@ -25,7 +27,7 b' casper.test_items = function (baseUrl) {'
25 // but item.link is without host, and url-encoded
27 // but item.link is without host, and url-encoded
26 var expected = baseUrl + decodeURIComponent(item.link);
28 var expected = baseUrl + decodeURIComponent(item.link);
27 this.test.assertEquals(this.getCurrentUrl(), expected, 'Testing dashboard link: ' + expected);
29 this.test.assertEquals(this.getCurrentUrl(), expected, 'Testing dashboard link: ' + expected);
28 casper.test_items(baseUrl);
30 casper.test_items(baseUrl, visited);
29 this.back();
31 this.back();
30 });
32 });
31 }
33 }
@@ -4,7 +4,7 b''
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 from tornado import web
6 from tornado import web
7 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
7 from ..base.handlers import IPythonHandler, path_regex
8 from ..utils import url_path_join, url_escape
8 from ..utils import url_path_join, url_escape
9
9
10
10
@@ -33,18 +33,21 b' class TreeHandler(IPythonHandler):'
33 return 'Home'
33 return 'Home'
34
34
35 @web.authenticated
35 @web.authenticated
36 def get(self, path='', name=None):
36 def get(self, path=''):
37 path = path.strip('/')
37 path = path.strip('/')
38 cm = self.contents_manager
38 cm = self.contents_manager
39 if name is not None:
39 if cm.file_exists(path):
40 # is a notebook, redirect to notebook handler
40 # it's not a directory, we have redirecting to do
41 model = cm.get(path, content=False)
42 # redirect to /api/notebooks if it's a notebook, otherwise /api/files
43 service = 'notebooks' if model['type'] == 'notebook' else 'files'
41 url = url_escape(url_path_join(
44 url = url_escape(url_path_join(
42 self.base_url, 'notebooks', path, name
45 self.base_url, service, path,
43 ))
46 ))
44 self.log.debug("Redirecting %s to %s", self.request.path, url)
47 self.log.debug("Redirecting %s to %s", self.request.path, url)
45 self.redirect(url)
48 self.redirect(url)
46 else:
49 else:
47 if not cm.path_exists(path=path):
50 if not cm.dir_exists(path=path):
48 # Directory is hidden or does not exist.
51 # Directory is hidden or does not exist.
49 raise web.HTTPError(404)
52 raise web.HTTPError(404)
50 elif cm.is_hidden(path):
53 elif cm.is_hidden(path):
@@ -66,7 +69,6 b' class TreeHandler(IPythonHandler):'
66
69
67
70
68 default_handlers = [
71 default_handlers = [
69 (r"/tree%s" % notebook_path_regex, TreeHandler),
70 (r"/tree%s" % path_regex, TreeHandler),
72 (r"/tree%s" % path_regex, TreeHandler),
71 (r"/tree", TreeHandler),
73 (r"/tree", TreeHandler),
72 ]
74 ]
General Comments 0
You need to be logged in to leave comments. Login now