Show More
@@ -54,7 +54,7 b' class FileContentsManager(ContentsManager):' | |||||
54 | self.log.debug("copystat on %s failed", dest, exc_info=True) |
|
54 | self.log.debug("copystat on %s failed", dest, exc_info=True) | |
55 |
|
55 | |||
56 | def _get_os_path(self, name=None, path=''): |
|
56 | def _get_os_path(self, name=None, path=''): | |
57 |
"""Given a filename and |
|
57 | """Given a filename and API path, return its file system | |
58 | path. |
|
58 | path. | |
59 |
|
59 | |||
60 | Parameters |
|
60 | Parameters | |
@@ -62,8 +62,7 b' class FileContentsManager(ContentsManager):' | |||||
62 | name : string |
|
62 | name : string | |
63 | A filename |
|
63 | A filename | |
64 | path : string |
|
64 | path : string | |
65 |
The relative |
|
65 | The relative API path to the named file. | |
66 | file. |
|
|||
67 |
|
66 | |||
68 | Returns |
|
67 | Returns | |
69 | ------- |
|
68 | ------- | |
@@ -77,6 +76,8 b' class FileContentsManager(ContentsManager):' | |||||
77 | def path_exists(self, path): |
|
76 | def path_exists(self, path): | |
78 | """Does the API-style path refer to an extant directory? |
|
77 | """Does the API-style path refer to an extant directory? | |
79 |
|
78 | |||
|
79 | API-style wrapper for os.path.isdir | |||
|
80 | ||||
80 | Parameters |
|
81 | Parameters | |
81 | ---------- |
|
82 | ---------- | |
82 | path : string |
|
83 | path : string | |
@@ -114,6 +115,8 b' class FileContentsManager(ContentsManager):' | |||||
114 | def file_exists(self, name, path=''): |
|
115 | def file_exists(self, name, path=''): | |
115 | """Returns True if the file exists, else returns False. |
|
116 | """Returns True if the file exists, else returns False. | |
116 |
|
117 | |||
|
118 | API-style wrapper for os.path.isfile | |||
|
119 | ||||
117 | Parameters |
|
120 | Parameters | |
118 | ---------- |
|
121 | ---------- | |
119 | name : string |
|
122 | name : string | |
@@ -123,7 +126,8 b' class FileContentsManager(ContentsManager):' | |||||
123 |
|
126 | |||
124 | Returns |
|
127 | Returns | |
125 | ------- |
|
128 | ------- | |
126 | bool |
|
129 | exists : bool | |
|
130 | Whether the file exists. | |||
127 | """ |
|
131 | """ | |
128 | path = path.strip('/') |
|
132 | path = path.strip('/') | |
129 | nbpath = self._get_os_path(name, path=path) |
|
133 | nbpath = self._get_os_path(name, path=path) | |
@@ -132,6 +136,8 b' class FileContentsManager(ContentsManager):' | |||||
132 | def exists(self, name=None, path=''): |
|
136 | def exists(self, name=None, path=''): | |
133 | """Returns True if the path [and name] exists, else returns False. |
|
137 | """Returns True if the path [and name] exists, else returns False. | |
134 |
|
138 | |||
|
139 | API-style wrapper for os.path.exists | |||
|
140 | ||||
135 | Parameters |
|
141 | Parameters | |
136 | ---------- |
|
142 | ---------- | |
137 | name : string |
|
143 | name : string | |
@@ -141,7 +147,8 b' class FileContentsManager(ContentsManager):' | |||||
141 |
|
147 | |||
142 | Returns |
|
148 | Returns | |
143 | ------- |
|
149 | ------- | |
144 | bool |
|
150 | exists : bool | |
|
151 | Whether the target exists. | |||
145 | """ |
|
152 | """ | |
146 | path = path.strip('/') |
|
153 | path = path.strip('/') | |
147 | os_path = self._get_os_path(name, path=path) |
|
154 | os_path = self._get_os_path(name, path=path) | |
@@ -246,7 +253,7 b' class FileContentsManager(ContentsManager):' | |||||
246 | name : str |
|
253 | name : str | |
247 | the name of the target |
|
254 | the name of the target | |
248 | path : str |
|
255 | path : str | |
249 |
the |
|
256 | the API path that describes the relative path for the target | |
250 |
|
257 | |||
251 | Returns |
|
258 | Returns | |
252 | ------- |
|
259 | ------- | |
@@ -344,7 +351,11 b' class FileContentsManager(ContentsManager):' | |||||
344 | return model |
|
351 | return model | |
345 |
|
352 | |||
346 | def update(self, model, name, path=''): |
|
353 | def update(self, model, name, path=''): | |
347 |
"""Update the file's path and/or name |
|
354 | """Update the file's path and/or name | |
|
355 | ||||
|
356 | For use in PATCH requests, to enable renaming a file without | |||
|
357 | re-uploading its contents. Only used for renaming at the moment. | |||
|
358 | """ | |||
348 | path = path.strip('/') |
|
359 | path = path.strip('/') | |
349 | new_name = model.get('name', name) |
|
360 | new_name = model.get('name', name) | |
350 | new_path = model.get('path', path).strip('/') |
|
361 | new_path = model.get('path', path).strip('/') | |
@@ -393,7 +404,7 b' class FileContentsManager(ContentsManager):' | |||||
393 |
|
404 | |||
394 | # Should we proceed with the move? |
|
405 | # Should we proceed with the move? | |
395 | if os.path.isfile(new_os_path): |
|
406 | if os.path.isfile(new_os_path): | |
396 |
raise web.HTTPError(409, u' |
|
407 | raise web.HTTPError(409, u'File with name already exists: %s' % new_os_path) | |
397 |
|
408 | |||
398 | # Move the file |
|
409 | # Move the file | |
399 | try: |
|
410 | try: |
@@ -54,16 +54,16 b' class ContentsHandler(IPythonHandler):' | |||||
54 | @web.authenticated |
|
54 | @web.authenticated | |
55 | @json_errors |
|
55 | @json_errors | |
56 | def get(self, path='', name=None): |
|
56 | def get(self, path='', name=None): | |
57 |
"""Return a |
|
57 | """Return a model for a file or directory. | |
58 |
|
58 | |||
59 | * GET with path and no filename lists files in a directory |
|
59 | A directory model contains a list of models (without content) | |
60 | * GET with path and filename returns file contents model |
|
60 | of the files and directories it contains. | |
61 | """ |
|
61 | """ | |
62 | path = path or '' |
|
62 | path = path or '' | |
63 | model = self.contents_manager.get_model(name=name, path=path) |
|
63 | model = self.contents_manager.get_model(name=name, path=path) | |
64 | if model['type'] == 'directory': |
|
64 | if model['type'] == 'directory': | |
65 | # group listing by type, then by name (case-insensitive) |
|
65 | # group listing by type, then by name (case-insensitive) | |
66 |
# FIXME: |
|
66 | # FIXME: sorting should be done in the frontends | |
67 | model['content'].sort(key=sort_key) |
|
67 | model['content'].sort(key=sort_key) | |
68 | self._finish_model(model, location=False) |
|
68 | self._finish_model(model, location=False) | |
69 |
|
69 | |||
@@ -81,22 +81,22 b' class ContentsHandler(IPythonHandler):' | |||||
81 | self._finish_model(model) |
|
81 | self._finish_model(model) | |
82 |
|
82 | |||
83 | def _copy(self, copy_from, path, copy_to=None): |
|
83 | def _copy(self, copy_from, path, copy_to=None): | |
84 |
"""Copy a file |
|
84 | """Copy a file, optionally specifying the new name. | |
85 |
|
||||
86 | Only support copying within the same directory. |
|
|||
87 | """ |
|
85 | """ | |
88 |
self.log.info(u"Copying |
|
86 | self.log.info(u"Copying {copy_from} to {path}/{copy_to}".format( | |
89 |
|
|
87 | copy_from=copy_from, | |
90 |
path |
|
88 | path=path, | |
91 | ) |
|
89 | copy_to=copy_to or '', | |
|
90 | )) | |||
92 | model = self.contents_manager.copy(copy_from, copy_to, path) |
|
91 | model = self.contents_manager.copy(copy_from, copy_to, path) | |
93 | self.set_status(201) |
|
92 | self.set_status(201) | |
94 | self._finish_model(model) |
|
93 | self._finish_model(model) | |
95 |
|
94 | |||
96 | def _upload(self, model, path, name=None): |
|
95 | def _upload(self, model, path, name=None): | |
97 |
""" |
|
96 | """Handle upload of a new file | |
98 |
|
97 | |||
99 |
If name specified, create it in path/name |
|
98 | If name specified, create it in path/name, | |
|
99 | otherwise create a new untitled file in path. | |||
100 | """ |
|
100 | """ | |
101 | self.log.info(u"Uploading file to %s/%s", path, name or '') |
|
101 | self.log.info(u"Uploading file to %s/%s", path, name or '') | |
102 | if name: |
|
102 | if name: | |
@@ -151,7 +151,7 b' class ContentsHandler(IPythonHandler):' | |||||
151 | cm = self.contents_manager |
|
151 | cm = self.contents_manager | |
152 |
|
152 | |||
153 | if cm.file_exists(path): |
|
153 | if cm.file_exists(path): | |
154 |
raise web.HTTPError(400, " |
|
154 | raise web.HTTPError(400, "Cannot POST to existing files, use PUT instead.") | |
155 |
|
155 | |||
156 | if not cm.path_exists(path): |
|
156 | if not cm.path_exists(path): | |
157 | raise web.HTTPError(404, "No such directory: %s" % path) |
|
157 | raise web.HTTPError(404, "No such directory: %s" % path) | |
@@ -184,11 +184,17 b' class ContentsHandler(IPythonHandler):' | |||||
184 | Save notebook at ``path/Name.ipynb``. Notebook structure is specified |
|
184 | Save notebook at ``path/Name.ipynb``. Notebook structure is specified | |
185 | in `content` key of JSON request body. If content is not specified, |
|
185 | in `content` key of JSON request body. If content is not specified, | |
186 | create a new empty notebook. |
|
186 | create a new empty notebook. | |
187 |
PUT /api/contents/path/Name.ipynb |
|
187 | PUT /api/contents/path/Name.ipynb | |
|
188 | with JSON body:: | |||
|
189 | ||||
|
190 | { | |||
|
191 | "copy_from" : "[path/to/]OtherNotebook.ipynb" | |||
|
192 | } | |||
|
193 | ||||
188 | Copy OtherNotebook to Name |
|
194 | Copy OtherNotebook to Name | |
189 | """ |
|
195 | """ | |
190 | if name is None: |
|
196 | if name is None: | |
191 | raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.") |
|
197 | raise web.HTTPError(400, "name must be specified with PUT.") | |
192 |
|
198 | |||
193 | model = self.get_json_body() |
|
199 | model = self.get_json_body() | |
194 | if model: |
|
200 | if model: |
@@ -15,6 +15,31 b' from IPython.utils.traitlets import Instance, Unicode, List' | |||||
15 |
|
15 | |||
16 |
|
16 | |||
17 | class ContentsManager(LoggingConfigurable): |
|
17 | class ContentsManager(LoggingConfigurable): | |
|
18 | """Base class for serving files and directories. | |||
|
19 | ||||
|
20 | This serves any text or binary file, | |||
|
21 | as well as directories, | |||
|
22 | with special handling for JSON notebook documents. | |||
|
23 | ||||
|
24 | Most APIs take a path argument, | |||
|
25 | which is always an API-style unicode path, | |||
|
26 | and always refers to a directory. | |||
|
27 | ||||
|
28 | - unicode, not url-escaped | |||
|
29 | - '/'-separated | |||
|
30 | - leading and trailing '/' will be stripped | |||
|
31 | - if unspecified, path defaults to '', | |||
|
32 | indicating the root path. | |||
|
33 | ||||
|
34 | name is also unicode, and refers to a specfic target: | |||
|
35 | ||||
|
36 | - unicode, not url-escaped | |||
|
37 | - must not contain '/' | |||
|
38 | - It refers to an individual filename | |||
|
39 | - It may refer to a directory name, | |||
|
40 | in the case of listing or creating directories. | |||
|
41 | ||||
|
42 | """ | |||
18 |
|
43 | |||
19 | notary = Instance(sign.NotebookNotary) |
|
44 | notary = Instance(sign.NotebookNotary) | |
20 | def _notary_default(self): |
|
45 | def _notary_default(self): | |
@@ -27,12 +52,26 b' class ContentsManager(LoggingConfigurable):' | |||||
27 | Glob patterns to hide in file and directory listings. |
|
52 | Glob patterns to hide in file and directory listings. | |
28 | """) |
|
53 | """) | |
29 |
|
54 | |||
|
55 | untitled_notebook = Unicode("Untitled", config=True, | |||
|
56 | help="The base name used when creating untitled notebooks." | |||
|
57 | ) | |||
|
58 | ||||
|
59 | untitled_file = Unicode("untitled", config=True, | |||
|
60 | help="The base name used when creating untitled files." | |||
|
61 | ) | |||
|
62 | ||||
|
63 | untitled_directory = Unicode("Untitled Folder", config=True, | |||
|
64 | help="The base name used when creating untitled directories." | |||
|
65 | ) | |||
|
66 | ||||
30 | # ContentsManager API part 1: methods that must be |
|
67 | # ContentsManager API part 1: methods that must be | |
31 | # implemented in subclasses. |
|
68 | # implemented in subclasses. | |
32 |
|
69 | |||
33 | def path_exists(self, path): |
|
70 | def path_exists(self, path): | |
34 | """Does the API-style path (directory) actually exist? |
|
71 | """Does the API-style path (directory) actually exist? | |
35 |
|
72 | |||
|
73 | Like os.path.isdir | |||
|
74 | ||||
36 | Override this method in subclasses. |
|
75 | Override this method in subclasses. | |
37 |
|
76 | |||
38 | Parameters |
|
77 | Parameters | |
@@ -58,14 +97,18 b' class ContentsManager(LoggingConfigurable):' | |||||
58 |
|
97 | |||
59 | Returns |
|
98 | Returns | |
60 | ------- |
|
99 | ------- | |
61 |
|
|
100 | hidden : bool | |
62 | Whether the path is hidden. |
|
101 | Whether the path is hidden. | |
63 |
|
102 | |||
64 | """ |
|
103 | """ | |
65 | raise NotImplementedError |
|
104 | raise NotImplementedError | |
66 |
|
105 | |||
67 | def file_exists(self, name, path=''): |
|
106 | def file_exists(self, name, path=''): | |
68 | """Returns a True if the file exists. Else, returns False. |
|
107 | """Does a file exist at the given name and path? | |
|
108 | ||||
|
109 | Like os.path.isfile | |||
|
110 | ||||
|
111 | Override this method in subclasses. | |||
69 |
|
112 | |||
70 | Parameters |
|
113 | Parameters | |
71 | ---------- |
|
114 | ---------- | |
@@ -76,20 +119,29 b' class ContentsManager(LoggingConfigurable):' | |||||
76 |
|
119 | |||
77 | Returns |
|
120 | Returns | |
78 | ------- |
|
121 | ------- | |
79 | bool |
|
122 | exists : bool | |
|
123 | Whether the file exists. | |||
80 | """ |
|
124 | """ | |
81 | raise NotImplementedError('must be implemented in a subclass') |
|
125 | raise NotImplementedError('must be implemented in a subclass') | |
82 |
|
126 | |||
83 |
def |
|
127 | def exists(self, name, path=''): | |
84 | """Return a list of contents dicts without content. |
|
128 | """Does a file or directory exist at the given name and path? | |
85 |
|
129 | |||
86 | This returns a list of dicts |
|
130 | Like os.path.exists | |
87 |
|
131 | |||
88 | This list of dicts should be sorted by name:: |
|
132 | Parameters | |
|
133 | ---------- | |||
|
134 | name : string | |||
|
135 | The name of the file you are checking. | |||
|
136 | path : string | |||
|
137 | The relative path to the file's directory (with '/' as separator) | |||
89 |
|
138 | |||
90 | data = sorted(data, key=lambda item: item['name']) |
|
139 | Returns | |
|
140 | ------- | |||
|
141 | exists : bool | |||
|
142 | Whether the target exists. | |||
91 | """ |
|
143 | """ | |
92 | raise NotImplementedError('must be implemented in a subclass') |
|
144 | return self.file_exists(name, path) or self.path_exists("%s/%s" % (path, name)) | |
93 |
|
145 | |||
94 | def get_model(self, name, path='', content=True): |
|
146 | def get_model(self, name, path='', content=True): | |
95 | """Get the model of a file or directory with or without content.""" |
|
147 | """Get the model of a file or directory with or without content.""" | |
@@ -100,7 +152,11 b' class ContentsManager(LoggingConfigurable):' | |||||
100 | raise NotImplementedError('must be implemented in a subclass') |
|
152 | raise NotImplementedError('must be implemented in a subclass') | |
101 |
|
153 | |||
102 | def update(self, model, name, path=''): |
|
154 | def update(self, model, name, path=''): | |
103 |
"""Update the file or directory and return the model with no content. |
|
155 | """Update the file or directory and return the model with no content. | |
|
156 | ||||
|
157 | For use in PATCH requests, to enable renaming a file without | |||
|
158 | re-uploading its contents. Only used for renaming at the moment. | |||
|
159 | """ | |||
104 | raise NotImplementedError('must be implemented in a subclass') |
|
160 | raise NotImplementedError('must be implemented in a subclass') | |
105 |
|
161 | |||
106 | def delete(self, name, path=''): |
|
162 | def delete(self, name, path=''): | |
@@ -126,12 +182,12 b' class ContentsManager(LoggingConfigurable):' | |||||
126 | """delete a checkpoint for a file""" |
|
182 | """delete a checkpoint for a file""" | |
127 | raise NotImplementedError("must be implemented in a subclass") |
|
183 | raise NotImplementedError("must be implemented in a subclass") | |
128 |
|
184 | |||
129 | def info_string(self): |
|
|||
130 | return "Serving notebooks" |
|
|||
131 |
|
||||
132 | # ContentsManager API part 2: methods that have useable default |
|
185 | # ContentsManager API part 2: methods that have useable default | |
133 | # implementations, but can be overridden in subclasses. |
|
186 | # implementations, but can be overridden in subclasses. | |
134 |
|
187 | |||
|
188 | def info_string(self): | |||
|
189 | return "Serving contents" | |||
|
190 | ||||
135 | def get_kernel_path(self, name, path='', model=None): |
|
191 | def get_kernel_path(self, name, path='', model=None): | |
136 | """ Return the path to start kernel in """ |
|
192 | """ Return the path to start kernel in """ | |
137 | return path |
|
193 | return path | |
@@ -144,7 +200,7 b' class ContentsManager(LoggingConfigurable):' | |||||
144 | filename : unicode |
|
200 | filename : unicode | |
145 | The name of a file, including extension |
|
201 | The name of a file, including extension | |
146 | path : unicode |
|
202 | path : unicode | |
147 |
The |
|
203 | The API path of the target's directory | |
148 |
|
204 | |||
149 | Returns |
|
205 | Returns | |
150 | ------- |
|
206 | ------- | |
@@ -176,7 +232,15 b' class ContentsManager(LoggingConfigurable):' | |||||
176 | model['type'] = 'file' |
|
232 | model['type'] = 'file' | |
177 | model['format'] = 'text' |
|
233 | model['format'] = 'text' | |
178 | if 'name' not in model: |
|
234 | if 'name' not in model: | |
179 | model['name'] = self.increment_filename('Untitled' + ext, path) |
|
235 | if model['type'] == 'directory': | |
|
236 | untitled = self.untitled_directory | |||
|
237 | elif model['type'] == 'notebook': | |||
|
238 | untitled = self.untitled_notebook | |||
|
239 | elif model['type'] == 'file': | |||
|
240 | untitled = self.untitled_file | |||
|
241 | else: | |||
|
242 | raise HTTPError(400, "Unexpected model type: %r" % model['type']) | |||
|
243 | model['name'] = self.increment_filename(untitled + ext, path) | |||
180 |
|
244 | |||
181 | model['path'] = path |
|
245 | model['path'] = path | |
182 | model = self.save(model, model['name'], model['path']) |
|
246 | model = self.save(model, model['name'], model['path']) | |
@@ -186,9 +250,16 b' class ContentsManager(LoggingConfigurable):' | |||||
186 | """Copy an existing file and return its new model. |
|
250 | """Copy an existing file and return its new model. | |
187 |
|
251 | |||
188 | If to_name not specified, increment `from_name-Copy#.ext`. |
|
252 | If to_name not specified, increment `from_name-Copy#.ext`. | |
|
253 | ||||
|
254 | copy_from can be a full path to a file, | |||
|
255 | or just a base name. If a base name, `path` is used. | |||
189 | """ |
|
256 | """ | |
190 | path = path.strip('/') |
|
257 | path = path.strip('/') | |
191 | model = self.get_model(from_name, path) |
|
258 | if '/' in from_name: | |
|
259 | from_path, from_name = from_name.rsplit('/', 1) | |||
|
260 | else: | |||
|
261 | from_path = path | |||
|
262 | model = self.get_model(from_name, from_path) | |||
192 | if model['type'] == 'directory': |
|
263 | if model['type'] == 'directory': | |
193 | raise HTTPError(400, "Can't copy directories") |
|
264 | raise HTTPError(400, "Can't copy directories") | |
194 | if not to_name: |
|
265 | if not to_name: | |
@@ -196,6 +267,7 b' class ContentsManager(LoggingConfigurable):' | |||||
196 | copy_name = u'{0}-Copy{1}'.format(base, ext) |
|
267 | copy_name = u'{0}-Copy{1}'.format(base, ext) | |
197 | to_name = self.increment_filename(copy_name, path) |
|
268 | to_name = self.increment_filename(copy_name, path) | |
198 | model['name'] = to_name |
|
269 | model['name'] = to_name | |
|
270 | model['path'] = path | |||
199 | model = self.save(model, to_name, path) |
|
271 | model = self.save(model, to_name, path) | |
200 | return model |
|
272 | return model | |
201 |
|
273 | |||
@@ -218,7 +290,7 b' class ContentsManager(LoggingConfigurable):' | |||||
218 | self.notary.mark_cells(nb, True) |
|
290 | self.notary.mark_cells(nb, True) | |
219 | self.save(model, name, path) |
|
291 | self.save(model, name, path) | |
220 |
|
292 | |||
221 | def check_and_sign(self, nb, name, path=''): |
|
293 | def check_and_sign(self, nb, name='', path=''): | |
222 | """Check for trusted cells, and sign the notebook. |
|
294 | """Check for trusted cells, and sign the notebook. | |
223 |
|
295 | |||
224 | Called as a part of saving notebooks. |
|
296 | Called as a part of saving notebooks. | |
@@ -226,18 +298,18 b' class ContentsManager(LoggingConfigurable):' | |||||
226 | Parameters |
|
298 | Parameters | |
227 | ---------- |
|
299 | ---------- | |
228 | nb : dict |
|
300 | nb : dict | |
229 |
The notebook |
|
301 | The notebook object (in nbformat.current format) | |
230 | name : string |
|
302 | name : string | |
231 | The filename of the notebook |
|
303 | The filename of the notebook (for logging) | |
232 | path : string |
|
304 | path : string | |
233 | The notebook's directory |
|
305 | The notebook's directory (for logging) | |
234 | """ |
|
306 | """ | |
235 | if self.notary.check_cells(nb): |
|
307 | if self.notary.check_cells(nb): | |
236 | self.notary.sign(nb) |
|
308 | self.notary.sign(nb) | |
237 | else: |
|
309 | else: | |
238 | self.log.warn("Saving untrusted notebook %s/%s", path, name) |
|
310 | self.log.warn("Saving untrusted notebook %s/%s", path, name) | |
239 |
|
311 | |||
240 | def mark_trusted_cells(self, nb, name, path=''): |
|
312 | def mark_trusted_cells(self, nb, name='', path=''): | |
241 | """Mark cells as trusted if the notebook signature matches. |
|
313 | """Mark cells as trusted if the notebook signature matches. | |
242 |
|
314 | |||
243 | Called as a part of loading notebooks. |
|
315 | Called as a part of loading notebooks. | |
@@ -245,11 +317,11 b' class ContentsManager(LoggingConfigurable):' | |||||
245 | Parameters |
|
317 | Parameters | |
246 | ---------- |
|
318 | ---------- | |
247 | nb : dict |
|
319 | nb : dict | |
248 |
The notebook |
|
320 | The notebook object (in nbformat.current format) | |
249 | name : string |
|
321 | name : string | |
250 | The filename of the notebook |
|
322 | The filename of the notebook (for logging) | |
251 | path : string |
|
323 | path : string | |
252 | The notebook's directory |
|
324 | The notebook's directory (for logging) | |
253 | """ |
|
325 | """ | |
254 | trusted = self.notary.check_signature(nb) |
|
326 | trusted = self.notary.check_signature(nb) | |
255 | if not trusted: |
|
327 | if not trusted: |
@@ -22,8 +22,6 b' from IPython.utils import py3compat' | |||||
22 | from IPython.utils.data import uniq_stable |
|
22 | from IPython.utils.data import uniq_stable | |
23 |
|
23 | |||
24 |
|
24 | |||
25 | # TODO: Remove this after we create the contents web service and directories are |
|
|||
26 | # no longer listed by the notebook web service. |
|
|||
27 | def notebooks_only(dir_model): |
|
25 | def notebooks_only(dir_model): | |
28 | return [nb for nb in dir_model['content'] if nb['type']=='notebook'] |
|
26 | return [nb for nb in dir_model['content'] if nb['type']=='notebook'] | |
29 |
|
27 | |||
@@ -279,9 +277,9 b' class APITest(NotebookTestBase):' | |||||
279 |
|
277 | |||
280 | def test_create_untitled_txt(self): |
|
278 | def test_create_untitled_txt(self): | |
281 | resp = self.api.create_untitled(path='foo/bar', ext='.txt') |
|
279 | resp = self.api.create_untitled(path='foo/bar', ext='.txt') | |
282 |
self._check_created(resp, ' |
|
280 | self._check_created(resp, 'untitled0.txt', 'foo/bar', type='file') | |
283 |
|
281 | |||
284 |
resp = self.api.read(path='foo/bar', name=' |
|
282 | resp = self.api.read(path='foo/bar', name='untitled0.txt') | |
285 | model = resp.json() |
|
283 | model = resp.json() | |
286 | self.assertEqual(model['type'], 'file') |
|
284 | self.assertEqual(model['type'], 'file') | |
287 | self.assertEqual(model['format'], 'text') |
|
285 | self.assertEqual(model['format'], 'text') | |
@@ -363,6 +361,10 b' class APITest(NotebookTestBase):' | |||||
363 | resp = self.api.copy(u'Γ§ d.ipynb', u'cΓΈpy.ipynb', path=u'Γ₯ b') |
|
361 | resp = self.api.copy(u'Γ§ d.ipynb', u'cΓΈpy.ipynb', path=u'Γ₯ b') | |
364 | self._check_created(resp, u'cΓΈpy.ipynb', u'Γ₯ b') |
|
362 | self._check_created(resp, u'cΓΈpy.ipynb', u'Γ₯ b') | |
365 |
|
363 | |||
|
364 | def test_copy_path(self): | |||
|
365 | resp = self.api.copy(u'foo/a.ipynb', u'cΓΈpyfoo.ipynb', path=u'Γ₯ b') | |||
|
366 | self._check_created(resp, u'cΓΈpyfoo.ipynb', u'Γ₯ b') | |||
|
367 | ||||
366 | def test_copy_dir_400(self): |
|
368 | def test_copy_dir_400(self): | |
367 | # can't copy directories |
|
369 | # can't copy directories | |
368 | with assert_http_error(400): |
|
370 | with assert_http_error(400): |
@@ -19,7 +19,7 b' define([' | |||||
19 | // base_url: string |
|
19 | // base_url: string | |
20 | // notebook_path: string |
|
20 | // notebook_path: string | |
21 | notebooklist.NotebookList.call(this, selector, $.extend({ |
|
21 | notebooklist.NotebookList.call(this, selector, $.extend({ | |
22 |
element_name: 'running'}, |
|
22 | element_name: 'running'}, | |
23 | options)); |
|
23 | options)); | |
24 | }; |
|
24 | }; | |
25 |
|
25 | |||
@@ -28,13 +28,20 b' define([' | |||||
28 | KernelList.prototype.sessions_loaded = function (d) { |
|
28 | KernelList.prototype.sessions_loaded = function (d) { | |
29 | this.sessions = d; |
|
29 | this.sessions = d; | |
30 | this.clear_list(); |
|
30 | this.clear_list(); | |
31 | var item; |
|
31 | var item, path_name; | |
32 |
for ( |
|
32 | for (path_name in d) { | |
33 | item = this.new_notebook_item(-1); |
|
33 | if (!d.hasOwnProperty(path_name)) { | |
34 | this.add_link('', path, item); |
|
34 | // nothing is safe in javascript | |
35 | this.add_shutdown_button(item, this.sessions[path]); |
|
35 | continue; | |
|
36 | } | |||
|
37 | item = this.new_item(-1); | |||
|
38 | this.add_link({ | |||
|
39 | name: path_name, | |||
|
40 | path: '', | |||
|
41 | type: 'notebook', | |||
|
42 | }, item); | |||
|
43 | this.add_shutdown_button(item, this.sessions[path_name]); | |||
36 | } |
|
44 | } | |
37 |
|
||||
38 | $('#running_list_header').toggle($.isEmptyObject(d)); |
|
45 | $('#running_list_header').toggle($.isEmptyObject(d)); | |
39 | }; |
|
46 | }; | |
40 |
|
47 |
@@ -80,7 +80,7 b' define([' | |||||
80 | var name_and_ext = utils.splitext(f.name); |
|
80 | var name_and_ext = utils.splitext(f.name); | |
81 | var file_ext = name_and_ext[1]; |
|
81 | var file_ext = name_and_ext[1]; | |
82 | if (file_ext === '.ipynb') { |
|
82 | if (file_ext === '.ipynb') { | |
83 |
var item = that.new_ |
|
83 | var item = that.new_item(0); | |
84 | item.addClass('new-file'); |
|
84 | item.addClass('new-file'); | |
85 | that.add_name_input(f.name, item); |
|
85 | that.add_name_input(f.name, item); | |
86 | // Store the notebook item in the reader so we can use it later |
|
86 | // Store the notebook item in the reader so we can use it later | |
@@ -236,7 +236,7 b' define([' | |||||
236 | var icon = NotebookList.icons[model.type]; |
|
236 | var icon = NotebookList.icons[model.type]; | |
237 | var uri_prefix = NotebookList.uri_prefixes[model.type]; |
|
237 | var uri_prefix = NotebookList.uri_prefixes[model.type]; | |
238 | item.find(".item_icon").addClass(icon).addClass('icon-fixed-width'); |
|
238 | item.find(".item_icon").addClass(icon).addClass('icon-fixed-width'); | |
239 | item.find("a.item_link") |
|
239 | var link = item.find("a.item_link") | |
240 | .attr('href', |
|
240 | .attr('href', | |
241 | utils.url_join_encode( |
|
241 | utils.url_join_encode( | |
242 | this.base_url, |
|
242 | this.base_url, | |
@@ -245,6 +245,11 b' define([' | |||||
245 | name |
|
245 | name | |
246 | ) |
|
246 | ) | |
247 | ); |
|
247 | ); | |
|
248 | // directory nav doesn't open new tabs | |||
|
249 | // files, notebooks do | |||
|
250 | if (model.type !== "directory") { | |||
|
251 | link.attr('target','_blank'); | |||
|
252 | } | |||
248 | var path_name = utils.url_path_join(path, name); |
|
253 | var path_name = utils.url_path_join(path, name); | |
249 | if (model.type == 'file') { |
|
254 | if (model.type == 'file') { | |
250 | this.add_delete_button(item); |
|
255 | this.add_delete_button(item); | |
@@ -362,7 +367,10 b' define([' | |||||
362 | var nbdata = item.data('nbdata'); |
|
367 | var nbdata = item.data('nbdata'); | |
363 | var content_type = 'application/json'; |
|
368 | var content_type = 'application/json'; | |
364 | var model = { |
|
369 | var model = { | |
|
370 | path: path, | |||
|
371 | name: nbname, | |||
365 | content : JSON.parse(nbdata), |
|
372 | content : JSON.parse(nbdata), | |
|
373 | type : 'notebook' | |||
366 | }; |
|
374 | }; | |
367 | var settings = { |
|
375 | var settings = { | |
368 | processData : false, |
|
376 | processData : false, | |
@@ -372,7 +380,7 b' define([' | |||||
372 | data : JSON.stringify(model), |
|
380 | data : JSON.stringify(model), | |
373 | headers : {'Content-Type': content_type}, |
|
381 | headers : {'Content-Type': content_type}, | |
374 | success : function (data, status, xhr) { |
|
382 | success : function (data, status, xhr) { | |
375 |
that.add_link( |
|
383 | that.add_link(model, item); | |
376 | that.add_delete_button(item); |
|
384 | that.add_delete_button(item); | |
377 | }, |
|
385 | }, | |
378 | error : utils.log_ajax_error, |
|
386 | error : utils.log_ajax_error, |
General Comments 0
You need to be logged in to leave comments.
Login now