##// END OF EJS Templates
Moves load_notebook to ContentManager and adds new_notebook to Google Drive version
KesterTong -
Show More
@@ -29,6 +29,40 b' define(['
29 */
29 */
30
30
31 /**
31 /**
32 * Load a notebook.
33 *
34 * Calls success_callback with notebook JSON object (as string), or
35 * error_callback with error.
36 *
37 * @method load_notebook
38 * @param {String} path
39 * @param {String} name
40 * @param {Function} success_callback
41 * @param {Function} error_callback
42 */
43 ContentManager.prototype.load_notebook = function (path, name, success_callback,
44 error_callback) {
45 // We do the call with settings so we can set cache to false.
46 var settings = {
47 processData : false,
48 cache : false,
49 type : "GET",
50 dataType : "json",
51 success : success_callback,
52 error : error_callback,
53 };
54 this.events.trigger('notebook_loading.Notebook');
55 var url = utils.url_join_encode(
56 this.base_url,
57 'api/notebooks',
58 path,
59 name
60 );
61 $.ajax(url, settings);
62 };
63
64
65 /**
32 * Creates a new notebook file at the specified path, and
66 * Creates a new notebook file at the specified path, and
33 * opens that notebook in a new window.
67 * opens that notebook in a new window.
34 *
68 *
@@ -45,11 +45,40 b' define(['
45 };
45 };
46
46
47 /**
47 /**
48 * Name of newly created notebook files.
49 * @type {string}
50 */
51 ContentManager.NEW_NOTEBOOK_TITLE = 'Untitled';
52
53 /**
54 * Extension for notebook files.
55 * @type {string}
56 */
57 ContentManager.NOTEBOOK_EXTENSION = 'ipynb';
58
59
60 ContentManager.MULTIPART_BOUNDARY = '-------314159265358979323846';
61
62 ContentManager.NOTEBOOK_MIMETYPE = 'application/ipynb';
63
64
65 /**
48 * low level Google Drive functions
66 * low level Google Drive functions
67 *
68 * NOTE: these functions should only be called after gapi_ready has been
69 * resolved, with the excpetion of authorize(), and on_gapi_load() which
70 * is private and should not be called at all. Typical usage is:
71 *
72 * var that = this;
73 * this.gapi_ready.done(function () {
74 * that.get_id_for_path(...)
75 * ...
76 * });
49 */
77 */
50
78
51 /*
79 /*
52 * Load Google Drive client library
80 * Load Google Drive client library
81 * @private
53 * @method on_gapi_load
82 * @method on_gapi_load
54 */
83 */
55 ContentManager.prototype.on_gapi_load = function() {
84 ContentManager.prototype.on_gapi_load = function() {
@@ -111,6 +140,10 b' define(['
111 * Gets the Google Drive folder ID corresponding to a path. Since
140 * Gets the Google Drive folder ID corresponding to a path. Since
112 * the Google Drive API doesn't expose a path structure, it is necessary
141 * the Google Drive API doesn't expose a path structure, it is necessary
113 * to manually walk the path from root.
142 * to manually walk the path from root.
143 * @method get_id_for_path
144 * @param {String} path The path
145 * @param {Function} onSuccess called with the folder Id on success
146 * @param {Function} onFailure called with the error on Failure
114 */
147 */
115 ContentManager.prototype.get_id_for_path = function(path, onSuccess, onFailure) {
148 ContentManager.prototype.get_id_for_path = function(path, onSuccess, onFailure) {
116 // Use recursive strategy, with helper function
149 // Use recursive strategy, with helper function
@@ -134,7 +167,8 b' define(['
134 }
167 }
135
168
136 var query = ('mimeType = \'' + FOLDER_MIME_TYPE + '\''
169 var query = ('mimeType = \'' + FOLDER_MIME_TYPE + '\''
137 + ' and title = \'' + this_component + '\'');
170 + ' and title = \'' + this_component + '\''
171 + ' and trashed = false');
138 var request = gapi.client.drive.children.list({
172 var request = gapi.client.drive.children.list({
139 'folderId': base_id,
173 'folderId': base_id,
140 'q': query
174 'q': query
@@ -165,12 +199,219 b' define(['
165 get_id_for_relative_path('root', path.split('/').reverse());
199 get_id_for_relative_path('root', path.split('/').reverse());
166 }
200 }
167
201
202 /**
203 * Gets the Google Drive folder ID corresponding to a path. Since
204 * the Google Drive API doesn't expose a path structure, it is necessary
205 * to manually walk the path from root.
206 * @method get_id_for_path
207 * @param {String} folder_id The google Drive folder id to search
208 * @param {String} filename The filename to find in folder_id
209 * @param {Function} onSuccess called with a files resource on success (see
210 * Google Drive API documentation for more information on the files
211 * resource).
212 * @param {Function} onFailure called with the error on Failure
213 */
214 ContentManager.prototype.get_resource_for_filename = function(
215 folder_id,
216 filename,
217 onSuccess,
218 onFailure) {
219 var query = ('title = \'' + filename + '\''
220 + ' and \'' + folder_id + '\' in parents'
221 + ' and trashed = false');
222 var request = gapi.client.drive.files.list({
223 'q': query
224 });
225 request.execute(function(response) {
226 if (!response || response['error']) {
227 onFailure(response ? response['error'] : null);
228 return;
229 }
230
231 var files = response['items'];
232 if (!files) {
233 // 'directory does not exist' error.
234 onFailure();
235 return;
236 }
237
238 if (files.length > 1) {
239 // 'runtime error' this should not happen
240 onFailure();
241 return;
242 }
243
244 onSuccess(files[0]);
245
246 });
247 };
248
249 /**
250 * Uploads a notebook to Drive, either creating a new one or saving an
251 * existing one.
252 *
253 * @method upload_to_drive
254 * @param {string} data The file contents as a string
255 * @param {Object} metadata File metadata
256 * @param {function(gapi.client.drive.files.Resource)} success_callback callback for
257 * success
258 * @param {function(?):?} error_callback callback for error, takes response object
259 * @param {string=} opt_fileId file Id. If false, a new file is created.
260 * @param {Object?} opt_params a dictionary containing the following keys
261 * pinned: whether this save should be pinned
262 */
263 ContentManager.prototype.upload_to_drive = function(data, metadata,
264 success_callback, error_callback, opt_fileId, opt_params) {
265 var params = opt_params || {};
266 var delimiter = '\r\n--' + ContentManager.MULTIPART_BOUNDARY + '\r\n';
267 var close_delim = '\r\n--' + ContentManager.MULTIPART_BOUNDARY + '--';
268 var body = delimiter +
269 'Content-Type: application/json\r\n\r\n' +
270 JSON.stringify(metadata) +
271 delimiter +
272 'Content-Type: ' + ContentManager.NOTEBOOK_MIMETYPE + '\r\n' +
273 '\r\n' +
274 data +
275 close_delim;
276
277 var path = '/upload/drive/v2/files';
278 var method = 'POST';
279 if (opt_fileId) {
280 path += '/' + opt_fileId;
281 method = 'PUT';
282 }
283
284 var request = gapi.client.request({
285 'path': path,
286 'method': method,
287 'params': {
288 'uploadType': 'multipart',
289 'pinned' : params['pinned']
290 },
291 'headers': {
292 'Content-Type': 'multipart/mixed; boundary="' +
293 ContentManager.MULTIPART_BOUNDARY + '"'
294 },
295 'body': body
296 });
297 request.execute(function(response) {
298 if (!response || response['error']) {
299 error_callback(response ? response['error'] : null);
300 return;
301 }
302
303 success_callback(response);
304 });
305 };
306
307 /**
308 * Obtains the filename that should be used for a new file in a given folder.
309 * This is the next file in the series Untitled0, Untitled1, ... in the given
310 * drive folder. As a fallback, returns Untitled.
311 *
312 * @method get_new_filename
313 * @param {function(string)} callback Called with the name for the new file.
314 * @param {string} opt_folderId optinal Drive folder Id to search for
315 * filenames. Uses root, if none is specified.
316 */
317 ContentManager.prototype.get_new_filename = function(callback, opt_folderId) {
318 /** @type {string} */
319 var folderId = opt_folderId || 'root';
320 var query = 'title contains \'' + ContentManager.NEW_NOTEBOOK_TITLE + '\'' +
321 ' and \'' + folderId + '\' in parents' +
322 ' and trashed = false';
323 var request = gapi.client.drive.files.list({
324 'maxResults': 1000,
325 'folderId' : folderId,
326 'q': query
327 });
328
329 request.execute(function(response) {
330 // Use 'Untitled.ipynb' as a fallback in case of error
331 var fallbackFilename = ContentManager.NEW_NOTEBOOK_TITLE + '.' +
332 ContentManager.NOTEBOOK_EXTENSION;
333 if (!response || response['error']) {
334 callback(fallbackFilename);
335 return;
336 }
337
338 var files = response['items'] || [];
339 var existingFilenames = $.map(files, function(filesResource) {
340 return filesResource['title'];
341 });
342
343 // Loop over file names Untitled0, ... , UntitledN where N is the number of
344 // elements in existingFilenames. Select the first file name that does not
345 // belong to existingFilenames. This is guaranteed to find a file name
346 // that does not belong to existingFilenames, since there are N + 1 file
347 // names tried, and existingFilenames contains N elements.
348 for (var i = 0; i <= existingFilenames.length; i++) {
349 /** @type {string} */
350 var filename = ContentManager.NEW_NOTEBOOK_TITLE + i + '.' +
351 ContentManager.NOTEBOOK_EXTENSION;
352 if (existingFilenames.indexOf(filename) == -1) {
353 callback(filename);
354 return;
355 }
356 }
357
358 // Control should not reach this point, so an error has occured
359 callback(fallbackFilename);
360 });
361 };
168
362
169 /**
363 /**
170 * Notebook Functions
364 * Notebook Functions
171 */
365 */
172
366
173 /**
367 /**
368 * Load a notebook.
369 *
370 * Calls success_callback with notebook JSON object (as string), or
371 * error_callback with error.
372 *
373 * @method load_notebook
374 * @param {String} path
375 * @param {String} name
376 * @param {Function} success_callback
377 * @param {Function} error_callback
378 */
379 ContentManager.prototype.load_notebook = function (path, name, success_callback,
380 error_callback) {
381 var that = this;
382 this.gapi_ready.done(function() {
383 that.get_id_for_path(path, function(folder_id) {
384 that.get_resource_for_filename(folder_id, name, function(file_resource) {
385 // Sends request to load file to drive.
386 var token = gapi.auth.getToken()['access_token'];
387 var xhrRequest = new XMLHttpRequest();
388 xhrRequest.open('GET', file_resource['downloadUrl'], true);
389 xhrRequest.setRequestHeader('Authorization', 'Bearer ' + token);
390 xhrRequest.onreadystatechange = function(e) {
391 if (xhrRequest.readyState == 4) {
392 if (xhrRequest.status == 200) {
393 var notebook_contents = xhrRequest.responseText;
394 //colab.nbformat.convertJsonNotebookToRealtime(
395 // notebook_contents, model);
396 var model = JSON.parse(notebook_contents);
397
398 success_callback({
399 content: model,
400 // A hack to deal with file/memory format conversions
401 name: model.metadata.name
402 });
403 } else {
404 error_callback(xhrRequest);
405 }
406 }
407 };
408 xhrRequest.send();
409 }, error_callback)
410 }, error_callback);
411 });
412 };
413
414 /**
174 * Creates a new notebook file at the specified path, and
415 * Creates a new notebook file at the specified path, and
175 * opens that notebook in a new window.
416 * opens that notebook in a new window.
176 *
417 *
@@ -178,46 +419,46 b' define(['
178 * @param {String} path The path to create the new notebook at
419 * @param {String} path The path to create the new notebook at
179 */
420 */
180 ContentManager.prototype.new_notebook = function(path) {
421 ContentManager.prototype.new_notebook = function(path) {
181 var base_url = this.base_url;
422 var that = this;
182 var settings = {
423 this.gapi_ready.done(function() {
183 processData : false,
424 that.get_id_for_path(path, function(folder_id) {
184 cache : false,
425 that.get_new_filename(function(filename) {
185 type : "POST",
426 var data = {
186 dataType : "json",
427 'worksheets': [{
187 async : false,
428 'cells' : [{
188 success : function (data, status, xhr){
429 'cell_type': 'code',
430 'input': '',
431 'outputs': [],
432 'language': 'python'
433 }],
434 }],
435 'metadata': {
436 'name': filename,
437 },
438 'nbformat': 3,
439 'nbformat_minor': 0
440 };
441 var metadata = {
442 'parents' : [{'id' : folder_id}],
443 'title' : filename,
444 'description': 'IP[y] file',
445 'mimeType': ContentManager.NOTEBOOK_MIMETYPE
446 }
447 that.upload_to_drive(JSON.stringify(data), metadata, function (data, status, xhr) {
189 var notebook_name = data.name;
448 var notebook_name = data.name;
190 window.open(
449 window.open(
191 utils.url_join_encode(
450 utils.url_join_encode(
192 base_url,
451 that.base_url,
193 'notebooks',
452 'notebooks',
194 path,
453 path,
195 notebook_name
454 filename
196 ),
455 ),
197 '_blank'
456 '_blank'
198 );
457 );
199 },
458 }, function(){});
200 error : function(xhr, status, error) {
459 }, folder_id);
201 utils.log_ajax_error(xhr, status, error);
460 })
202 var msg;
203 if (xhr.responseJSON && xhr.responseJSON.message) {
204 msg = xhr.responseJSON.message;
205 } else {
206 msg = xhr.statusText;
207 }
208 dialog.modal({
209 title : 'Creating Notebook Failed',
210 body : "The error was: " + msg,
211 buttons : {'OK' : {'class' : 'btn-primary'}}
212 });
461 });
213 }
214 };
215 var url = utils.url_join_encode(
216 base_url,
217 'api/notebooks',
218 path
219 );
220 $.ajax(url,settings);
221 };
462 };
222
463
223 ContentManager.prototype.delete_notebook = function(name, path) {
464 ContentManager.prototype.delete_notebook = function(name, path) {
@@ -380,7 +621,8 b' define(['
380 that.get_id_for_path(path, function(folder_id) {
621 that.get_id_for_path(path, function(folder_id) {
381 query = ('(fileExtension = \'ipynb\' or'
622 query = ('(fileExtension = \'ipynb\' or'
382 + ' mimeType = \'' + FOLDER_MIME_TYPE + '\')'
623 + ' mimeType = \'' + FOLDER_MIME_TYPE + '\')'
383 + ' and \'' + folder_id + '\' in parents');
624 + ' and \'' + folder_id + '\' in parents'
625 + ' and trashed = false');
384 var request = gapi.client.drive.files.list({
626 var request = gapi.client.drive.files.list({
385 'maxResults' : 1000,
627 'maxResults' : 1000,
386 'q' : query
628 'q' : query
@@ -2140,26 +2140,13 b' define(['
2140 * @param {String} notebook_name and path A notebook to load
2140 * @param {String} notebook_name and path A notebook to load
2141 */
2141 */
2142 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
2142 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
2143 var that = this;
2144 this.notebook_name = notebook_name;
2143 this.notebook_name = notebook_name;
2145 this.notebook_path = notebook_path;
2144 this.notebook_path = notebook_path;
2146 // We do the call with settings so we can set cache to false.
2145 this.content_manager.load_notebook(
2147 var settings = {
2146 notebook_path,
2148 processData : false,
2147 notebook_name,
2149 cache : false,
2148 $.proxy(this.load_notebook_success,this),
2150 type : "GET",
2149 $.proxy(this.load_notebook_error,this));
2151 dataType : "json",
2152 success : $.proxy(this.load_notebook_success,this),
2153 error : $.proxy(this.load_notebook_error,this),
2154 };
2155 this.events.trigger('notebook_loading.Notebook');
2156 var url = utils.url_join_encode(
2157 this.base_url,
2158 'api/contents',
2159 this.notebook_path,
2160 this.notebook_name
2161 );
2162 $.ajax(url, settings);
2163 };
2150 };
2164
2151
2165 /**
2152 /**
General Comments 0
You need to be logged in to leave comments. Login now