Show More
@@ -0,0 +1,61 | |||||
|
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 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 = |
|
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 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. |
|
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( |
|
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 |
|
451 | path = '/'.join(parts) | |
453 |
|
452 | |||
454 |
if not cm.file_exists( |
|
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 |
|
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 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 class FilesHandler(IPythonHandler): | |||||
17 |
|
17 | |||
18 | @web.authenticated |
|
18 | @web.authenticated | |
19 | def get(self, path): |
|
19 | def get(self, path): | |
20 |
cm = self. |
|
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 |
|
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 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 |
|
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( |
|
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 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 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 |
|
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( |
|
27 | if not cm.file_exists(path): | |
30 |
raise web.HTTPError(404, u'Notebook does not exist: %s |
|
28 | raise web.HTTPError(404, u'Notebook does not exist: %s' % path) | |
31 |
name = url_escape( |
|
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 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 |
|
|
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 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 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, |
|
64 | def _get_os_path(self, path): | |
65 |
"""Given a |
|
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 |
|
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 class FileContentsManager(ContentsManager): | |||||
112 |
|
107 | |||
113 | Returns |
|
108 | Returns | |
114 | ------- |
|
109 | ------- | |
115 |
|
|
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, |
|
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 |
|
125 | The relative path to the file (with '/' as separator) | |
134 |
|
126 | |||
135 | Returns |
|
127 | Returns | |
136 | ------- |
|
128 | ------- | |
@@ -138,20 +130,18 class FileContentsManager(ContentsManager): | |||||
138 | Whether the file exists. |
|
130 | Whether the file exists. | |
139 | """ |
|
131 | """ | |
140 | path = path.strip('/') |
|
132 | path = path.strip('/') | |
141 |
|
|
133 | os_path = self._get_os_path(path) | |
142 |
return os.path.isfile( |
|
134 | return os.path.isfile(os_path) | |
143 |
|
135 | |||
144 |
def exists(self, |
|
136 | def exists(self, path): | |
145 |
"""Returns True if the path |
|
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 |
|
144 | The API path to the file (with '/' as separator) | |
155 |
|
145 | |||
156 | Returns |
|
146 | Returns | |
157 | ------- |
|
147 | ------- | |
@@ -159,32 +149,31 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( |
|
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, |
|
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( |
|
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'] = |
|
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, |
|
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( |
|
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 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 |
|
|
192 | os_dir = self._get_os_path(path) | |
210 |
|
|
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( |
|
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, |
|
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( |
|
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( |
|
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 class FileContentsManager(ContentsManager): | |||||
241 | return model |
|
234 | return model | |
242 |
|
235 | |||
243 |
|
236 | |||
244 |
def _notebook_model(self, |
|
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( |
|
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( |
|
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, |
|
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, |
|
258 | def get_model(self, path, content=True): | |
266 |
""" Takes a path |
|
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 class FileContentsManager(ContentsManager): | |||||
280 | """ |
|
271 | """ | |
281 | path = path.strip('/') |
|
272 | path = path.strip('/') | |
282 |
|
273 | |||
283 |
if not self.exists( |
|
274 | if not self.exists(path): | |
284 |
raise web.HTTPError(404, u'No such file or directory: %s |
|
275 | raise web.HTTPError(404, u'No such file or directory: %s' % path) | |
285 |
|
276 | |||
286 |
os_path = self._get_os_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( |
|
279 | model = self._dir_model(path, content=content) | |
289 |
elif |
|
280 | elif path.endswith('.ipynb'): | |
290 |
model = self._notebook_model( |
|
281 | model = self._notebook_model(path, content=content) | |
291 | else: |
|
282 | else: | |
292 |
model = self._file_model( |
|
283 | model = self._file_model(path, content=content) | |
293 | return model |
|
284 | return model | |
294 |
|
285 | |||
295 |
def _save_notebook(self, os_path, model, |
|
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, |
|
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, |
|
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 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, |
|
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 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, |
|
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 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( |
|
334 | if self.file_exists(path) and not self.list_checkpoints(path): | |
347 |
self.create_checkpoint( |
|
335 | self.create_checkpoint(path) | |
348 |
|
336 | |||
349 |
|
|
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, |
|
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, |
|
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, |
|
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 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( |
|
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, |
|
363 | def update(self, model, path): | |
382 |
"""Update the file's path |
|
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 |
|
371 | if path != new_path: | |
391 |
self.rename( |
|
372 | self.rename(path, new_path) | |
392 |
model = self.get_model( |
|
373 | model = self.get_model(new_path, content=False) | |
393 | return model |
|
374 | return model | |
394 |
|
375 | |||
395 |
def delete(self, |
|
376 | def delete(self, path): | |
396 |
"""Delete file |
|
377 | """Delete file at path.""" | |
397 | path = path.strip('/') |
|
378 | path = path.strip('/') | |
398 |
os_path = self._get_os_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 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( |
|
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, |
|
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 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, |
|
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 |
|
408 | if new_path == old_path: | |
428 | return |
|
409 | return | |
429 |
|
410 | |||
430 |
new_os_path = self._get_os_path( |
|
411 | new_os_path = self._get_os_path(new_path) | |
431 |
old_os_path = self._get_os_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. |
|
415 | if os.path.exists(new_os_path): | |
435 |
raise web.HTTPError(409, u'File |
|
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_ |
|
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( |
|
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, |
|
428 | old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path) | |
448 |
new_cp_path = self.get_checkpoint_path(checkpoint_id, |
|
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, |
|
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=pat |
|
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, |
|
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, |
|
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 class FileContentsManager(ContentsManager): | |||||
481 |
|
464 | |||
482 | # public checkpoint API |
|
465 | # public checkpoint API | |
483 |
|
466 | |||
484 |
def create_checkpoint(self, |
|
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, |
|
475 | cp_path = self.get_checkpoint_path(checkpoint_id, path) | |
491 |
self.log.debug("creating checkpoint for %s", |
|
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, |
|
480 | return self.get_checkpoint_model(checkpoint_id, path) | |
496 |
|
481 | |||
497 |
def list_checkpoints(self, |
|
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, |
|
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, |
|
493 | return [self.get_checkpoint_model(checkpoint_id, path)] | |
509 |
|
494 | |||
510 |
|
495 | |||
511 |
def restore_checkpoint(self, checkpoint_id, |
|
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", |
|
499 | self.log.info("restoring %s from checkpoint %s", path, checkpoint_id) | |
515 |
nb_path = self._get_os_path( |
|
500 | nb_path = self._get_os_path(path) | |
516 |
cp_path = self.get_checkpoint_path(checkpoint_id, |
|
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 |
|
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 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, |
|
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, |
|
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 |
|
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 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, |
|
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 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 ( |
|
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 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, |
|
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 |
|
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[' |
|
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='' |
|
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( |
|
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 class ContentsHandler(IPythonHandler): | |||||
69 |
|
67 | |||
70 | @web.authenticated |
|
68 | @web.authenticated | |
71 | @json_errors |
|
69 | @json_errors | |
72 |
def patch(self, path='' |
|
70 | def patch(self, path=''): | |
73 |
"""PATCH renames a |
|
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, |
|
76 | model = cm.update(model, path) | |
81 | self._finish_model(model) |
|
77 | self._finish_model(model) | |
82 |
|
78 | |||
83 |
def _copy(self, copy_from, |
|
79 | def _copy(self, copy_from, copy_to=None): | |
84 |
"""Copy a file, optionally specifying |
|
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 |
|
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 |
|
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 _ |
|
96 | def _new_untitled(self, path, type='', ext=''): | |
110 |
"""Create an empty |
|
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 |
|
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 |
|
105 | self.log.info(u"Saving file at %s", path) | |
125 |
model = self.contents_manager.save(model, |
|
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='' |
|
111 | def post(self, path=''): | |
136 |
"""Create a new file |
|
112 | """Create a new file in the specified path. | |
137 |
|
113 | |||
138 |
POST creates new files |
|
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 |
|
126 | raise web.HTTPError(400, "Cannot POST to files, use PUT instead.") | |
155 |
|
127 | |||
156 |
if not cm. |
|
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', ' |
|
135 | ext = model.get('ext', '') | |
164 |
|
|
136 | type = model.get('type', '') | |
165 |
|
|
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._ |
|
140 | self._new_untitled(path, type=type, ext=ext) | |
172 | else: |
|
141 | else: | |
173 |
self._ |
|
142 | self._new_untitled(path) | |
174 |
|
143 | |||
175 | @web.authenticated |
|
144 | @web.authenticated | |
176 | @json_errors |
|
145 | @json_errors | |
177 |
def put(self, path='' |
|
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 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 |
|
|
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 |
|
164 | self._upload(model, path) | |
210 | else: |
|
165 | else: | |
211 |
self._ |
|
166 | self._new_untitled(path) | |
212 |
|
167 | |||
213 | @web.authenticated |
|
168 | @web.authenticated | |
214 | @json_errors |
|
169 | @json_errors | |
215 |
def delete(self, path='' |
|
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 |
|
173 | self.log.warn('delete %s', path) | |
219 |
cm.delete( |
|
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 class CheckpointsHandler(IPythonHandler): | |||||
227 |
|
182 | |||
228 | @web.authenticated |
|
183 | @web.authenticated | |
229 | @json_errors |
|
184 | @json_errors | |
230 |
def get(self, path='' |
|
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( |
|
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='' |
|
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( |
|
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 |
|
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 class ModifyCheckpointsHandler(IPythonHandler): | |||||
254 |
|
209 | |||
255 | @web.authenticated |
|
210 | @web.authenticated | |
256 | @json_errors |
|
211 | @json_errors | |
257 |
def post(self, path, |
|
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, |
|
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, |
|
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, |
|
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 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" % |
|
252 | (r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler), | |
298 |
(r"/api/contents%s/checkpoints/%s" % ( |
|
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 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 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 |
|
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 class ContentsManager(LoggingConfigurable): | |||||
105 | """ |
|
97 | """ | |
106 | raise NotImplementedError |
|
98 | raise NotImplementedError | |
107 |
|
99 | |||
108 |
def file_exists(self, |
|
100 | def file_exists(self, path=''): | |
109 |
"""Does a file exist at the given |
|
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 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, |
|
121 | def exists(self, path): | |
130 |
"""Does a file or directory exist at the given |
|
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 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( |
|
136 | return self.file_exists(path) or self.dir_exists(path) | |
147 |
|
137 | |||
148 |
def get_model(self, |
|
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, |
|
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, |
|
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 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, |
|
154 | def delete(self, path): | |
165 |
"""Delete file or directory by |
|
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, |
|
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, |
|
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, |
|
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, |
|
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 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, |
|
183 | def get_kernel_path(self, path, model=None): | |
194 |
""" |
|
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 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. |
|
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 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 |
|
225 | def new_untitled(self, path='', type='', ext=''): | |
232 |
"""Create a new file or directory |
|
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 |
|
|
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_ |
|
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_ |
|
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 |
|
|
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 |
|
297 | if '/' in path: | |
270 |
from_ |
|
298 | from_dir, from_name = path.rsplit('/', 1) | |
271 | else: |
|
299 | else: | |
272 |
from_ |
|
300 | from_dir = '' | |
273 |
|
|
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_ |
|
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, |
|
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 |
|
329 | The path of a notebook | |
297 | """ |
|
330 | """ | |
298 |
model = self.get_model( |
|
331 | model = self.get_model(path) | |
299 | nb = model['content'] |
|
332 | nb = model['content'] | |
300 |
self.log.warn("Trusting notebook %s |
|
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, |
|
335 | self.save(model, path) | |
303 |
|
336 | |||
304 |
def check_and_sign(self, nb, |
|
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 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 |
|
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 |
|
352 | self.log.warn("Saving untrusted notebook %s", path) | |
322 |
|
353 | |||
323 |
def mark_trusted_cells(self, nb, |
|
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 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 |
|
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 |
|
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 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, |
|
49 | def read(self, path): | |
50 |
return self._req('GET', |
|
50 | return self._req('GET', path) | |
51 |
|
51 | |||
52 |
def create_untitled(self, path='/', ext= |
|
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 |
|
58 | def mkdir_untitled(self, path='/'): | |
59 |
return self._req('POST', path, |
|
59 | return self._req('POST', path, json.dumps({'type': 'directory'})) | |
60 |
|
60 | |||
61 |
def copy |
|
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 |
|
65 | def create(self, path='/'): | |
66 |
return self._req('PUT', |
|
66 | return self._req('PUT', path) | |
|
67 | ||||
|
68 | def upload(self, path, body): | |||
|
69 | return self._req('PUT', path, body) | |||
67 |
|
70 | |||
68 |
def |
|
71 | def mkdir_untitled(self, path='/'): | |
69 |
return self._req('P |
|
72 | return self._req('POST', path, json.dumps({'type': 'directory'})) | |
70 |
|
73 | |||
71 |
def mkdir(self |
|
74 | def mkdir(self, path='/'): | |
72 |
return self._req('PUT', |
|
75 | return self._req('PUT', path, json.dumps({'type': 'directory'})) | |
73 |
|
76 | |||
74 |
def copy(self, copy_from |
|
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', |
|
79 | return self._req('PUT', path, body) | |
77 |
|
80 | |||
78 |
def save(self, |
|
81 | def save(self, path, body): | |
79 |
return self._req('PUT', |
|
82 | return self._req('PUT', path, body) | |
80 |
|
83 | |||
81 |
def delete(self |
|
84 | def delete(self, path='/'): | |
82 |
return self._req('DELETE', |
|
85 | return self._req('DELETE', path) | |
83 |
|
86 | |||
84 |
def rename(self, |
|
87 | def rename(self, path, new_path): | |
85 |
body = json.dumps({' |
|
88 | body = json.dumps({'path': new_path}) | |
86 |
return self._req('PATCH', |
|
89 | return self._req('PATCH', path, body) | |
87 |
|
90 | |||
88 |
def get_checkpoints(self, |
|
91 | def get_checkpoints(self, path): | |
89 |
return self._req('GET', url_path_join(path, |
|
92 | return self._req('GET', url_path_join(path, 'checkpoints')) | |
90 |
|
93 | |||
91 |
def new_checkpoint(self, |
|
94 | def new_checkpoint(self, path): | |
92 |
return self._req('POST', url_path_join(path, |
|
95 | return self._req('POST', url_path_join(path, 'checkpoints')) | |
93 |
|
96 | |||
94 |
def restore_checkpoint(self, |
|
97 | def restore_checkpoint(self, path, checkpoint_id): | |
95 |
return self._req('POST', url_path_join(path, |
|
98 | return self._req('POST', url_path_join(path, 'checkpoints', checkpoint_id)) | |
96 |
|
99 | |||
97 |
def delete_checkpoint(self, |
|
100 | def delete_checkpoint(self, path, checkpoint_id): | |
98 |
return self._req('DELETE', url_path_join(path, |
|
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 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 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 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 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 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 |
|
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 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 |
|
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 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 |
|
260 | self.api.read('foo/q.txt') | |
251 |
|
261 | |||
252 |
def _check_created(self, resp |
|
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 |
|
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'], |
|
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, |
|
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, |
|
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 |
|
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 |
|
290 | self._check_created(resp, 'foo/bar/untitled0.txt', type='file') | |
282 |
|
291 | |||
283 |
resp = self.api.read(path='foo/bar |
|
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 |
|
|
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, |
|
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 |
|
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 class APITest(NotebookTestBase): | |||||
315 | 'format' : 'text', |
|
330 | 'format' : 'text', | |
316 | 'type' : 'file', |
|
331 | 'type' : 'file', | |
317 | } |
|
332 | } | |
318 |
|
|
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 |
|
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 class APITest(NotebookTestBase): | |||||
333 | 'format' : 'base64', |
|
348 | 'format' : 'base64', | |
334 | 'type' : 'file', |
|
349 | 'type' : 'file', | |
335 | } |
|
350 | } | |
336 |
|
|
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 |
|
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 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 |
|
|
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, |
|
371 | self._check_created(resp, path) | |
356 |
resp = self.api.read( |
|
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' |
|
377 | resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'unicodΓ©') | |
366 |
self._check_created(resp, u' |
|
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' |
|
384 | resp = self.api.copy(u'foo/a.ipynb', u'Γ₯ b') | |
370 |
self._check_created(resp, u' |
|
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' |
|
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[' |
|
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 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 |
|
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 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 |
|
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 |
|
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= { |
|
445 | nbmodel= {'content': nb, 'type': 'notebook'} | |
422 |
resp = self.api.save('a.ipynb |
|
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 |
|
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 |
|
460 | resp = self.api.read('foo/a.ipynb') | |
447 |
r = self.api.new_checkpoint('a.ipynb |
|
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 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= { |
|
473 | nbmodel= {'content': nb, 'type': 'notebook'} | |
460 |
resp = self.api.save('a.ipynb |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
494 | cps = self.api.get_checkpoints('foo/a.ipynb').json() | |
481 | self.assertEqual(cps, []) |
|
495 | self.assertEqual(cps, []) |
@@ -42,7 +42,7 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(' |
|
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 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 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', ' |
|
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 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( |
|
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, |
|
112 | cm.save(full_model, path) | |
113 | return nb, name, path |
|
113 | return nb, name, path | |
114 |
|
114 | |||
115 |
def test_ |
|
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[' |
|
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( |
|
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 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 |
|
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 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. |
|
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 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 |
pat |
|
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. |
|
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, |
|
198 | os.symlink(file_model['name'], os.path.join(os_path, name)) | |
185 |
symlink_model = cm.get_model( |
|
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 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[' |
|
214 | model['path'] = 'test.ipynb' | |
202 |
model = cm.update(model, |
|
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, |
|
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'], |
|
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, |
|
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( |
|
253 | full_model = cm.get_model(path) | |
240 |
|
254 | |||
241 | # Save the notebook |
|
255 | # Save the notebook | |
242 |
model = cm.save(full_model, |
|
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 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( |
|
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, |
|
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'], |
|
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 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( |
|
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, |
|
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 |
pat |
|
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( |
|
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( |
|
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( |
|
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( |
|
316 | cm.trust_notebook(path) | |
301 |
trusted = cm.get_model( |
|
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 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, |
|
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( |
|
330 | cm.trust_notebook(path) | |
315 |
nb = cm.get_model( |
|
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 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, |
|
340 | cm.mark_trusted_cells(nb, path) | |
325 |
cm.check_and_sign(nb, |
|
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( |
|
344 | cm.trust_notebook(path) | |
329 |
nb = cm.get_model( |
|
345 | nb = cm.get_model(path)['content'] | |
330 |
cm.mark_trusted_cells(nb, |
|
346 | cm.mark_trusted_cells(nb, path) | |
331 |
cm.check_and_sign(nb, |
|
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 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 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( |
|
49 | if sm.session_exists(path=path): | |
54 |
model = sm.get_session( |
|
50 | model = sm.get_session(path=path) | |
55 | else: |
|
51 | else: | |
56 | try: |
|
52 | try: | |
57 |
model = sm.create_session( |
|
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 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 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', ' |
|
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 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, |
|
32 | (session_id, path, kernel_id)""") | |
33 | return self._cursor |
|
33 | return self._cursor | |
34 |
|
34 | |||
35 | @property |
|
35 | @property | |
@@ -44,9 +44,9 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, |
|
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 |
|
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 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 |
|
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( |
|
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, |
|
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 |
|
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 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 |
|
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 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 |
|
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 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, |
|
106 | (i.e. session_id, path, kernel_id) | |
109 |
|
107 | |||
110 | Returns |
|
108 | Returns | |
111 | ------- |
|
109 | ------- | |
@@ -182,7 +180,6 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 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( |
|
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':{' |
|
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( |
|
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( |
|
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 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( |
|
64 | sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'), | |
65 |
sm.create_session( |
|
65 | sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'), | |
66 |
sm.create_session( |
|
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 |
|
|
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( |
|
89 | sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'), | |
81 |
sm.create_session( |
|
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 class TestSessionManager(TestCase): | |||||
87 | { |
|
96 | { | |
88 | 'id': sessions[1]['id'], |
|
97 | 'id': sessions[1]['id'], | |
89 | 'notebook': { |
|
98 | 'notebook': { | |
90 |
' |
|
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 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( |
|
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, |
|
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':{' |
|
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( |
|
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( |
|
130 | sm.create_session(path='/path/to/1/test1.ipynb', kernel_name='python'), | |
123 |
sm.create_session( |
|
131 | sm.create_session(path='/path/to/2/test2.ipynb', kernel_name='python'), | |
124 |
sm.create_session( |
|
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 |
|
|
138 | 'notebook': {'path': u'/path/to/1/test1.ipynb'}, | |
131 |
|
|
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( |
|
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 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 |
|
41 | def create(self, path, kernel_name='python'): | |
42 |
body = json.dumps({'notebook': {' |
|
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, |
|
46 | def modify(self, id, path): | |
47 |
body = json.dumps({'notebook': {' |
|
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 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 |
|
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'][' |
|
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 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 |
|
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 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 |
|
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' |
|
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'][' |
|
115 | self.assertEqual(changed['notebook']['path'], 'nb2.ipynb') | |
117 | self.assertEqual(changed['notebook']['path'], '') |
|
@@ -272,11 +272,11 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 |
|
276 | if ( !attrs.style ) { | |
277 |
attrs |
|
277 | attrs.style = line; | |
278 | } else { |
|
278 | } else { | |
279 |
attrs |
|
279 | attrs.style += " " + line; | |
280 | } |
|
280 | } | |
281 | } |
|
281 | } | |
282 | } |
|
282 | } | |
@@ -285,7 +285,7 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 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 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 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 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 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. |
|
154 | notebook.load_notebook(common_options.notebook_path); | |
155 |
|
155 | |||
156 | }); |
|
156 | }); |
@@ -2,13 +2,14 | |||||
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, |
|
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 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 |
|
|
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 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 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 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 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 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 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 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 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 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 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 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 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 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, |
|
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 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 |
|
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 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 |
|
|
2049 | that.notebook_name = json.name; | |
2056 |
that. |
|
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 define([ | |||||
2061 | }; |
|
2056 | }; | |
2062 |
|
2057 | |||
2063 | Notebook.prototype.delete = function () { |
|
2058 | Notebook.prototype.delete = function () { | |
2064 |
this.contents.delete(this. |
|
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 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_ |
|
2098 | * @param {String} notebook_path A notebook to load | |
2104 | */ |
|
2099 | */ | |
2105 |
Notebook.prototype.load_notebook = function ( |
|
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. |
|
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 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 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 define([ | |||||
2192 | src = " a newer notebook format "; |
|
2186 | src = " a newer notebook format "; | |
2193 | } |
|
2187 | } | |
2194 |
|
2188 | |||
2195 |
|
|
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 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 |
|
|
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 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 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, |
|
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 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, |
|
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 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, |
|
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 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. |
|
2445 | this.load_notebook(this.notebook_path); | |
2452 | }; |
|
2446 | }; | |
2453 |
|
2447 | |||
2454 | /** |
|
2448 | /** | |
@@ -2460,7 +2454,7 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, |
|
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 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. |
|
2473 | this.load_notebook(this.notebook_path); | |
2480 | }; |
|
2474 | }; | |
2481 |
|
2475 | |||
2482 |
|
2476 |
@@ -122,14 +122,12 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 |
|
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 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 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 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 |
|
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 |
* |
|
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 |
|
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. |
|
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 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 |
|
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 |
|
95 | * Creates a new untitled file or directory in the specified directory path. | |
96 | * |
|
96 | * | |
97 |
* @method |
|
97 | * @method new | |
98 |
* @param {String} path |
|
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 |
|
100 | * ext: file extension to use | |
|
101 | * type: model type to create ('notebook', 'file', or 'directory') | |||
102 | */ |
|
102 | */ | |
103 |
Contents.prototype.new = function(path, |
|
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 |
} |
|
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 : |
|
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 define([ | |||||
123 | $.ajax(this.api_url(path), settings); |
|
120 | $.ajax(this.api_url(path), settings); | |
124 | }; |
|
121 | }; | |
125 |
|
122 | |||
126 |
Contents.prototype.delete = function( |
|
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 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 |
|
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, |
|
143 | Contents.prototype.rename = function(path, new_path, options) { | |
148 |
var data = { |
|
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 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 |
|
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, |
|
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 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 |
|
171 | var url = this.api_url(path); | |
176 | $.ajax(url, settings); |
|
172 | $.ajax(url, settings); | |
177 | }; |
|
173 | }; | |
178 |
|
174 | |||
179 |
Contents.prototype.copy = function( |
|
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 |
|
|
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: |
|
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 define([ | |||||
204 | * Checkpointing Functions |
|
195 | * Checkpointing Functions | |
205 | */ |
|
196 | */ | |
206 |
|
197 | |||
207 |
Contents.prototype.create_checkpoint = function(path, |
|
198 | Contents.prototype.create_checkpoint = function(path, options) { | |
208 |
var url = this.api_url(path, |
|
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 define([ | |||||
214 | $.ajax(url, settings); |
|
205 | $.ajax(url, settings); | |
215 | }; |
|
206 | }; | |
216 |
|
207 | |||
217 |
Contents.prototype.list_checkpoints = function(path, |
|
208 | Contents.prototype.list_checkpoints = function(path, options) { | |
218 |
var url = this.api_url(path, |
|
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 define([ | |||||
224 | $.ajax(url, settings); |
|
215 | $.ajax(url, settings); | |
225 | }; |
|
216 | }; | |
226 |
|
217 | |||
227 |
Contents.prototype.restore_checkpoint = function(path, |
|
218 | Contents.prototype.restore_checkpoint = function(path, checkpoint_id, options) { | |
228 |
var url = this.api_url(path, |
|
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 define([ | |||||
234 | $.ajax(url, settings); |
|
225 | $.ajax(url, settings); | |
235 | }; |
|
226 | }; | |
236 |
|
227 | |||
237 |
Contents.prototype.delete_checkpoint = function(path, |
|
228 | Contents.prototype.delete_checkpoint = function(path, checkpoint_id, options) { | |
238 |
var url = this.api_url(path, |
|
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 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 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 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 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} [ |
|
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 ( |
|
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 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 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 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 |
* |
|
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 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 | |||||
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 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 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, |
|
67 | contents.new_untitled(common_options.notebook_path, { | |
64 |
e |
|
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 |
|
74 | data.path | |
71 | ), '_blank'); |
|
75 | ), '_blank'); | |
72 | }, |
|
76 | }, | |
73 | error: function(error) { |
|
77 | error: function(error) { |
@@ -100,7 +100,7 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 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 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 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 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 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: ' + |
|
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 define([ | |||||
330 | Delete : { |
|
328 | Delete : { | |
331 | class: "btn-danger", |
|
329 | class: "btn-danger", | |
332 | click: function() { |
|
330 | click: function() { | |
333 |
notebooklist.contents.delete( |
|
331 | notebooklist.contents.delete(path, { | |
334 | success: function() { |
|
332 | success: function() { | |
335 |
notebooklist.notebook_deleted(path |
|
333 | notebooklist.notebook_deleted(path); | |
336 | } |
|
334 | } | |
337 | }); |
|
335 | }); | |
338 | } |
|
336 | } | |
@@ -345,25 +343,24 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 |
|
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 = $( |
|
349 | var element = $(this); | |
352 |
if (element.data( |
|
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 |
|
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 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 define([ | |||||
418 | model.content = filedata; |
|
412 | model.content = filedata; | |
419 | content_type = 'application/octet-stream'; |
|
413 | content_type = 'application/octet-stream'; | |
420 | } |
|
414 | } | |
421 |
|
|
415 | filedata = item.data('filedata'); | |
422 |
|
416 | |||
423 | var settings = { |
|
417 | var settings = { | |
424 |
|
|
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 define([ | |||||
453 | buttons : { |
|
435 | buttons : { | |
454 | Overwrite : { |
|
436 | Overwrite : { | |
455 | class: "btn-danger", |
|
437 | class: "btn-danger", | |
456 |
click: function() { |
|
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 define([ | |||||
461 | } |
|
445 | } | |
462 | }); |
|
446 | }); | |
463 | } else { |
|
447 | } else { | |
464 |
|
|
448 | that.contents.save(path, model, settings); | |
465 | } |
|
449 | } | |
466 |
|
450 | |||
467 | return false; |
|
451 | return false; | |
@@ -478,7 +462,7 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 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 |
|
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, |
|
21 | function (event, error) { | |
22 |
IPython._save_failed = "save failed with " + |
|
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 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 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 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 |
|
100 | return IPython && IPython.notebook && true; | |
100 | }); |
|
101 | }); | |
101 | }); |
|
102 | }); | |
102 |
|
103 |
@@ -75,22 +75,22 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_ |
|
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_ |
|
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_ |
|
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_ |
|
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 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_ |
|
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 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 ( |
|
19 | if (item.link.match(/^\/tree\//)) { | |
19 | var followed_url = baseUrl+item.link; |
|
20 | var followed_url = baseUrl+item.link; | |
20 |
if (!followed_url |
|
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 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 | |||||
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, |
|
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 class TreeHandler(IPythonHandler): | |||||
33 | return 'Home' |
|
33 | return 'Home' | |
34 |
|
34 | |||
35 | @web.authenticated |
|
35 | @web.authenticated | |
36 |
def get(self, path='' |
|
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 |
# i |
|
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, |
|
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. |
|
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 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