##// END OF EJS Templates
add missing error dialogs to tree actions...
Min RK -
Show More
@@ -1,101 +1,105 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
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 'jquery',
5 'jquery',
6 'base/js/namespace',
6 'base/js/namespace',
7 'base/js/utils',
7 'base/js/utils',
8 'base/js/dialog',
8 'base/js/dialog',
9 ], function ($, IPython, utils, dialog) {
9 ], function ($, IPython, utils, dialog) {
10 "use strict";
10 "use strict";
11
11
12 var NewNotebookWidget = function (selector, options) {
12 var NewNotebookWidget = function (selector, options) {
13 this.selector = selector;
13 this.selector = selector;
14 this.base_url = options.base_url;
14 this.base_url = options.base_url;
15 this.notebook_path = options.notebook_path;
15 this.notebook_path = options.notebook_path;
16 this.contents = options.contents;
16 this.contents = options.contents;
17 this.default_kernel = null;
17 this.default_kernel = null;
18 this.kernelspecs = {};
18 this.kernelspecs = {};
19 if (this.selector !== undefined) {
19 if (this.selector !== undefined) {
20 this.element = $(selector);
20 this.element = $(selector);
21 this.request_kernelspecs();
21 this.request_kernelspecs();
22 }
22 }
23 this.bind_events();
23 this.bind_events();
24 };
24 };
25
25
26 NewNotebookWidget.prototype.bind_events = function () {
26 NewNotebookWidget.prototype.bind_events = function () {
27 var that = this;
27 var that = this;
28 this.element.find('#new_notebook').click(function () {
28 this.element.find('#new_notebook').click(function () {
29 that.new_notebook();
29 that.new_notebook();
30 });
30 });
31 };
31 };
32
32
33 NewNotebookWidget.prototype.request_kernelspecs = function () {
33 NewNotebookWidget.prototype.request_kernelspecs = function () {
34 /** request and then load kernel specs */
34 /** request and then load kernel specs */
35 var url = utils.url_join_encode(this.base_url, 'api/kernelspecs');
35 var url = utils.url_join_encode(this.base_url, 'api/kernelspecs');
36 utils.promising_ajax(url).then($.proxy(this._load_kernelspecs, this));
36 utils.promising_ajax(url).then($.proxy(this._load_kernelspecs, this));
37 };
37 };
38
38
39 NewNotebookWidget.prototype._load_kernelspecs = function (data) {
39 NewNotebookWidget.prototype._load_kernelspecs = function (data) {
40 /** load kernelspec list */
40 /** load kernelspec list */
41 var that = this;
41 var that = this;
42 this.kernelspecs = data.kernelspecs;
42 this.kernelspecs = data.kernelspecs;
43 var menu = this.element.find("#notebook-kernels");
43 var menu = this.element.find("#notebook-kernels");
44 var keys = Object.keys(data.kernelspecs).sort(function (a, b) {
44 var keys = Object.keys(data.kernelspecs).sort(function (a, b) {
45 var da = data.kernelspecs[a].spec.display_name;
45 var da = data.kernelspecs[a].spec.display_name;
46 var db = data.kernelspecs[b].spec.display_name;
46 var db = data.kernelspecs[b].spec.display_name;
47 if (da === db) {
47 if (da === db) {
48 return 0;
48 return 0;
49 } else if (da > db) {
49 } else if (da > db) {
50 return 1;
50 return 1;
51 } else {
51 } else {
52 return -1;
52 return -1;
53 }
53 }
54 });
54 });
55
55
56 // Create the kernel list in reverse order because
56 // Create the kernel list in reverse order because
57 // the .after insertion causes each item to be added
57 // the .after insertion causes each item to be added
58 // to the top of the list.
58 // to the top of the list.
59 for (var i = keys.length - 1; i >= 0; i--) {
59 for (var i = keys.length - 1; i >= 0; i--) {
60 var ks = this.kernelspecs[keys[i]];
60 var ks = this.kernelspecs[keys[i]];
61 var li = $("<li>")
61 var li = $("<li>")
62 .attr("id", "kernel-" +ks.name)
62 .attr("id", "kernel-" +ks.name)
63 .data('kernelspec', ks).append(
63 .data('kernelspec', ks).append(
64 $('<a>')
64 $('<a>')
65 .attr('href', '#')
65 .attr('href', '#')
66 .click($.proxy(this.new_notebook, this, ks.name))
66 .click($.proxy(this.new_notebook, this, ks.name))
67 .text(ks.spec.display_name)
67 .text(ks.spec.display_name)
68 .attr('title', 'Create a new notebook with ' + ks.spec.display_name)
68 .attr('title', 'Create a new notebook with ' + ks.spec.display_name)
69 );
69 );
70 menu.after(li);
70 menu.after(li);
71 }
71 }
72 };
72 };
73
73
74 NewNotebookWidget.prototype.new_notebook = function (kernel_name) {
74 NewNotebookWidget.prototype.new_notebook = function (kernel_name) {
75 /** create and open a new notebook */
75 /** create and open a new notebook */
76 var that = this;
76 var that = this;
77 kernel_name = kernel_name || this.default_kernel;
77 kernel_name = kernel_name || this.default_kernel;
78 var w = window.open();
78 var w = window.open();
79 this.contents.new_untitled(that.notebook_path, {type: "notebook"}).then(
79 this.contents.new_untitled(that.notebook_path, {type: "notebook"}).then(
80 function (data) {
80 function (data) {
81 var url = utils.url_join_encode(
81 var url = utils.url_join_encode(
82 that.base_url, 'notebooks', data.path
82 that.base_url, 'notebooks', data.path
83 );
83 );
84 if (kernel_name) {
84 if (kernel_name) {
85 url += "?kernel_name=" + kernel_name;
85 url += "?kernel_name=" + kernel_name;
86 }
86 }
87 w.location = url;
87 w.location = url;
88 },
88 }).catch(function (e) {
89 function (error) {
89 w.close();
90 w.close();
90 dialog.modal({
91 dialog.modal({
91 title : 'Creating Notebook Failed',
92 title : 'Creating Notebook Failed',
92 body : $('<div/>')
93 body : "The error was: " + error.message,
93 .text("An error occurred while creating a new notebook.")
94 buttons : {'OK' : {'class' : 'btn-primary'}}
94 .append($('<div/>')
95 });
95 .addClass('alert alert-danger')
96 }
96 .text(e.message || e)),
97 );
97 buttons: {
98 OK: {'class' : 'btn-primary'}
99 }
100 });
101 });
98 };
102 };
99
103
100 return {'NewNotebookWidget': NewNotebookWidget};
104 return {'NewNotebookWidget': NewNotebookWidget};
101 });
105 });
@@ -1,722 +1,754 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
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',
5 'base/js/namespace',
6 'jquery',
6 'jquery',
7 'base/js/utils',
7 'base/js/utils',
8 'base/js/dialog',
8 'base/js/dialog',
9 'base/js/events',
9 'base/js/events',
10 'base/js/keyboard',
10 'base/js/keyboard',
11 ], function(IPython, $, utils, dialog, events, keyboard) {
11 ], function(IPython, $, utils, dialog, events, keyboard) {
12 "use strict";
12 "use strict";
13
13
14 var NotebookList = function (selector, options) {
14 var NotebookList = function (selector, options) {
15 /**
15 /**
16 * Constructor
16 * Constructor
17 *
17 *
18 * Parameters:
18 * Parameters:
19 * selector: string
19 * selector: string
20 * options: dictionary
20 * options: dictionary
21 * Dictionary of keyword arguments.
21 * Dictionary of keyword arguments.
22 * session_list: SessionList instance
22 * session_list: SessionList instance
23 * element_name: string
23 * element_name: string
24 * base_url: string
24 * base_url: string
25 * notebook_path: string
25 * notebook_path: string
26 * contents: Contents instance
26 * contents: Contents instance
27 */
27 */
28 var that = this;
28 var that = this;
29 this.session_list = options.session_list;
29 this.session_list = options.session_list;
30 // allow code re-use by just changing element_name in kernellist.js
30 // allow code re-use by just changing element_name in kernellist.js
31 this.element_name = options.element_name || 'notebook';
31 this.element_name = options.element_name || 'notebook';
32 this.selector = selector;
32 this.selector = selector;
33 if (this.selector !== undefined) {
33 if (this.selector !== undefined) {
34 this.element = $(selector);
34 this.element = $(selector);
35 this.style();
35 this.style();
36 this.bind_events();
36 this.bind_events();
37 }
37 }
38 this.notebooks_list = [];
38 this.notebooks_list = [];
39 this.sessions = {};
39 this.sessions = {};
40 this.base_url = options.base_url || utils.get_body_data("baseUrl");
40 this.base_url = options.base_url || utils.get_body_data("baseUrl");
41 this.notebook_path = options.notebook_path || utils.get_body_data("notebookPath");
41 this.notebook_path = options.notebook_path || utils.get_body_data("notebookPath");
42 this.contents = options.contents;
42 this.contents = options.contents;
43 if (this.session_list && this.session_list.events) {
43 if (this.session_list && this.session_list.events) {
44 this.session_list.events.on('sessions_loaded.Dashboard',
44 this.session_list.events.on('sessions_loaded.Dashboard',
45 function(e, d) { that.sessions_loaded(d); });
45 function(e, d) { that.sessions_loaded(d); });
46 }
46 }
47 };
47 };
48
48
49 NotebookList.prototype.style = function () {
49 NotebookList.prototype.style = function () {
50 var prefix = '#' + this.element_name;
50 var prefix = '#' + this.element_name;
51 $(prefix + '_toolbar').addClass('list_toolbar');
51 $(prefix + '_toolbar').addClass('list_toolbar');
52 $(prefix + '_list_info').addClass('toolbar_info');
52 $(prefix + '_list_info').addClass('toolbar_info');
53 $(prefix + '_buttons').addClass('toolbar_buttons');
53 $(prefix + '_buttons').addClass('toolbar_buttons');
54 $(prefix + '_list_header').addClass('list_header');
54 $(prefix + '_list_header').addClass('list_header');
55 this.element.addClass("list_container");
55 this.element.addClass("list_container");
56 };
56 };
57
57
58 NotebookList.prototype.bind_events = function () {
58 NotebookList.prototype.bind_events = function () {
59 var that = this;
59 var that = this;
60 $('#refresh_' + this.element_name + '_list').click(function () {
60 $('#refresh_' + this.element_name + '_list').click(function () {
61 that.load_sessions();
61 that.load_sessions();
62 });
62 });
63 this.element.bind('dragover', function () {
63 this.element.bind('dragover', function () {
64 return false;
64 return false;
65 });
65 });
66 this.element.bind('drop', function(event){
66 this.element.bind('drop', function(event){
67 that.handleFilesUpload(event,'drop');
67 that.handleFilesUpload(event,'drop');
68 return false;
68 return false;
69 });
69 });
70
70
71 // Bind events for singleton controls.
71 // Bind events for singleton controls.
72 if (!NotebookList._bound_singletons) {
72 if (!NotebookList._bound_singletons) {
73 NotebookList._bound_singletons = true;
73 NotebookList._bound_singletons = true;
74 $('#new-file').click(function(e) {
74 $('#new-file').click(function(e) {
75 var w = window.open();
75 var w = window.open();
76 that.contents.new_untitled(that.notebook_path || '', {type: 'file', ext: '.txt'}).then(function(data) {
76 that.contents.new_untitled(that.notebook_path || '', {type: 'file', ext: '.txt'}).then(function(data) {
77 var url = utils.url_join_encode(
77 var url = utils.url_join_encode(
78 that.base_url, 'edit', data.path
78 that.base_url, 'edit', data.path
79 );
79 );
80 w.location = url;
80 w.location = url;
81 }).catch(function (e) {
82 w.close();
83 dialog.modal({
84 title: 'Creating File Failed',
85 body: $('<div/>')
86 .text("An error occurred while creating a new file.")
87 .append($('<div/>')
88 .addClass('alert alert-danger')
89 .text(e.message || e)),
90 buttons: {
91 OK: {'class': 'btn-primary'}
92 }
93 });
81 });
94 });
82 that.load_sessions();
95 that.load_sessions();
83 });
96 });
84 $('#new-folder').click(function(e) {
97 $('#new-folder').click(function(e) {
85 that.contents.new_untitled(that.notebook_path || '', {type: 'directory'})
98 that.contents.new_untitled(that.notebook_path || '', {type: 'directory'})
86 .then(function(){
99 .then(function(){
87 that.load_list();
100 that.load_list();
101 }).catch(function (e) {
102 dialog.modal({
103 title: 'Creating Folder Failed',
104 body: $('<div/>')
105 .text("An error occurred while creating a new folder.")
106 .append($('<div/>')
107 .addClass('alert alert-danger')
108 .text(e.message || e)),
109 buttons: {
110 OK: {'class': 'btn-primary'}
111 }
112 });
88 });
113 });
114 that.load_sessions();
89 });
115 });
90
116
91 $('.rename-button').click($.proxy(this.rename_selected, this));
117 $('.rename-button').click($.proxy(this.rename_selected, this));
92 $('.shutdown-button').click($.proxy(this.shutdown_selected, this));
118 $('.shutdown-button').click($.proxy(this.shutdown_selected, this));
93 $('.duplicate-button').click($.proxy(this.duplicate_selected, this));
119 $('.duplicate-button').click($.proxy(this.duplicate_selected, this));
94 $('.delete-button').click($.proxy(this.delete_selected, this));
120 $('.delete-button').click($.proxy(this.delete_selected, this));
95 }
121 }
96 };
122 };
97
123
98 NotebookList.prototype.handleFilesUpload = function(event, dropOrForm) {
124 NotebookList.prototype.handleFilesUpload = function(event, dropOrForm) {
99 var that = this;
125 var that = this;
100 var files;
126 var files;
101 if(dropOrForm =='drop'){
127 if(dropOrForm =='drop'){
102 files = event.originalEvent.dataTransfer.files;
128 files = event.originalEvent.dataTransfer.files;
103 } else
129 } else
104 {
130 {
105 files = event.originalEvent.target.files;
131 files = event.originalEvent.target.files;
106 }
132 }
107 for (var i = 0; i < files.length; i++) {
133 for (var i = 0; i < files.length; i++) {
108 var f = files[i];
134 var f = files[i];
109 var name_and_ext = utils.splitext(f.name);
135 var name_and_ext = utils.splitext(f.name);
110 var file_ext = name_and_ext[1];
136 var file_ext = name_and_ext[1];
111
137
112 var reader = new FileReader();
138 var reader = new FileReader();
113 if (file_ext === '.ipynb') {
139 if (file_ext === '.ipynb') {
114 reader.readAsText(f);
140 reader.readAsText(f);
115 } else {
141 } else {
116 // read non-notebook files as binary
142 // read non-notebook files as binary
117 reader.readAsArrayBuffer(f);
143 reader.readAsArrayBuffer(f);
118 }
144 }
119 var item = that.new_item(0, true);
145 var item = that.new_item(0, true);
120 item.addClass('new-file');
146 item.addClass('new-file');
121 that.add_name_input(f.name, item, file_ext == '.ipynb' ? 'notebook' : 'file');
147 that.add_name_input(f.name, item, file_ext == '.ipynb' ? 'notebook' : 'file');
122 // Store the list item in the reader so we can use it later
148 // Store the list item in the reader so we can use it later
123 // to know which item it belongs to.
149 // to know which item it belongs to.
124 $(reader).data('item', item);
150 $(reader).data('item', item);
125 reader.onload = function (event) {
151 reader.onload = function (event) {
126 var item = $(event.target).data('item');
152 var item = $(event.target).data('item');
127 that.add_file_data(event.target.result, item);
153 that.add_file_data(event.target.result, item);
128 that.add_upload_button(item);
154 that.add_upload_button(item);
129 };
155 };
130 reader.onerror = function (event) {
156 reader.onerror = function (event) {
131 var item = $(event.target).data('item');
157 var item = $(event.target).data('item');
132 var name = item.data('name');
158 var name = item.data('name');
133 item.remove();
159 item.remove();
134 dialog.modal({
160 dialog.modal({
135 title : 'Failed to read file',
161 title : 'Failed to read file',
136 body : "Failed to read file '" + name + "'",
162 body : "Failed to read file '" + name + "'",
137 buttons : {'OK' : { 'class' : 'btn-primary' }}
163 buttons : {'OK' : { 'class' : 'btn-primary' }}
138 });
164 });
139 };
165 };
140 }
166 }
141 // Replace the file input form wth a clone of itself. This is required to
167 // Replace the file input form wth a clone of itself. This is required to
142 // reset the form. Otherwise, if you upload a file, delete it and try to
168 // reset the form. Otherwise, if you upload a file, delete it and try to
143 // upload it again, the changed event won't fire.
169 // upload it again, the changed event won't fire.
144 var form = $('input.fileinput');
170 var form = $('input.fileinput');
145 form.replaceWith(form.clone(true));
171 form.replaceWith(form.clone(true));
146 return false;
172 return false;
147 };
173 };
148
174
149 NotebookList.prototype.clear_list = function (remove_uploads) {
175 NotebookList.prototype.clear_list = function (remove_uploads) {
150 /**
176 /**
151 * Clears the navigation tree.
177 * Clears the navigation tree.
152 *
178 *
153 * Parameters
179 * Parameters
154 * remove_uploads: bool=False
180 * remove_uploads: bool=False
155 * Should upload prompts also be removed from the tree.
181 * Should upload prompts also be removed from the tree.
156 */
182 */
157 if (remove_uploads) {
183 if (remove_uploads) {
158 this.element.children('.list_item').remove();
184 this.element.children('.list_item').remove();
159 } else {
185 } else {
160 this.element.children('.list_item:not(.new-file)').remove();
186 this.element.children('.list_item:not(.new-file)').remove();
161 }
187 }
162 };
188 };
163
189
164 NotebookList.prototype.load_sessions = function(){
190 NotebookList.prototype.load_sessions = function(){
165 this.session_list.load_sessions();
191 this.session_list.load_sessions();
166 };
192 };
167
193
168
194
169 NotebookList.prototype.sessions_loaded = function(data){
195 NotebookList.prototype.sessions_loaded = function(data){
170 this.sessions = data;
196 this.sessions = data;
171 this.load_list();
197 this.load_list();
172 };
198 };
173
199
174 NotebookList.prototype.load_list = function () {
200 NotebookList.prototype.load_list = function () {
175 var that = this;
201 var that = this;
176 this.contents.list_contents(that.notebook_path).then(
202 this.contents.list_contents(that.notebook_path).then(
177 $.proxy(this.draw_notebook_list, this),
203 $.proxy(this.draw_notebook_list, this),
178 function(error) {
204 function(error) {
179 that.draw_notebook_list({content: []}, "Server error: " + error.message);
205 that.draw_notebook_list({content: []}, "Server error: " + error.message);
180 }
206 }
181 );
207 );
182 };
208 };
183
209
184 /**
210 /**
185 * Draw the list of notebooks
211 * Draw the list of notebooks
186 * @method draw_notebook_list
212 * @method draw_notebook_list
187 * @param {Array} list An array of dictionaries representing files or
213 * @param {Array} list An array of dictionaries representing files or
188 * directories.
214 * directories.
189 * @param {String} error_msg An error message
215 * @param {String} error_msg An error message
190 */
216 */
191
217
192
218
193 var type_order = {'directory':0,'notebook':1,'file':2};
219 var type_order = {'directory':0,'notebook':1,'file':2};
194
220
195 NotebookList.prototype.draw_notebook_list = function (list, error_msg) {
221 NotebookList.prototype.draw_notebook_list = function (list, error_msg) {
196 list.content.sort(function(a, b) {
222 list.content.sort(function(a, b) {
197 if (type_order[a['type']] < type_order[b['type']]) {
223 if (type_order[a['type']] < type_order[b['type']]) {
198 return -1;
224 return -1;
199 }
225 }
200 if (type_order[a['type']] > type_order[b['type']]) {
226 if (type_order[a['type']] > type_order[b['type']]) {
201 return 1;
227 return 1;
202 }
228 }
203 if (a['name'] < b['name']) {
229 if (a['name'] < b['name']) {
204 return -1;
230 return -1;
205 }
231 }
206 if (a['name'] > b['name']) {
232 if (a['name'] > b['name']) {
207 return 1;
233 return 1;
208 }
234 }
209 return 0;
235 return 0;
210 });
236 });
211 var message = error_msg || 'Notebook list empty.';
237 var message = error_msg || 'Notebook list empty.';
212 var item = null;
238 var item = null;
213 var model = null;
239 var model = null;
214 var len = list.content.length;
240 var len = list.content.length;
215 this.clear_list();
241 this.clear_list();
216 var n_uploads = this.element.children('.list_item').length;
242 var n_uploads = this.element.children('.list_item').length;
217 if (len === 0) {
243 if (len === 0) {
218 item = this.new_item(0);
244 item = this.new_item(0);
219 var span12 = item.children().first();
245 var span12 = item.children().first();
220 span12.empty();
246 span12.empty();
221 span12.append($('<div style="margin:auto;text-align:center;color:grey"/>').text(message));
247 span12.append($('<div style="margin:auto;text-align:center;color:grey"/>').text(message));
222 }
248 }
223 var path = this.notebook_path;
249 var path = this.notebook_path;
224 var offset = n_uploads;
250 var offset = n_uploads;
225 if (path !== '') {
251 if (path !== '') {
226 item = this.new_item(offset, false);
252 item = this.new_item(offset, false);
227 model = {
253 model = {
228 type: 'directory',
254 type: 'directory',
229 name: '..',
255 name: '..',
230 path: utils.url_path_split(path)[0],
256 path: utils.url_path_split(path)[0],
231 };
257 };
232 this.add_link(model, item);
258 this.add_link(model, item);
233 offset += 1;
259 offset += 1;
234 }
260 }
235 for (var i=0; i<len; i++) {
261 for (var i=0; i<len; i++) {
236 model = list.content[i];
262 model = list.content[i];
237 item = this.new_item(i+offset, true);
263 item = this.new_item(i+offset, true);
238 this.add_link(model, item);
264 this.add_link(model, item);
239 }
265 }
240 // Trigger an event when we've finished drawing the notebook list.
266 // Trigger an event when we've finished drawing the notebook list.
241 events.trigger('draw_notebook_list.NotebookList');
267 events.trigger('draw_notebook_list.NotebookList');
242 this._selection_changed();
268 this._selection_changed();
243 };
269 };
244
270
245
271
246 /**
272 /**
247 * Creates a new item.
273 * Creates a new item.
248 * @param {integer} index
274 * @param {integer} index
249 * @param {boolean} [selectable] - tristate, undefined: don't draw checkbox,
275 * @param {boolean} [selectable] - tristate, undefined: don't draw checkbox,
250 * false: don't draw checkbox but pad
276 * false: don't draw checkbox but pad
251 * where it should be, true: draw checkbox.
277 * where it should be, true: draw checkbox.
252 * @return {JQuery} row
278 * @return {JQuery} row
253 */
279 */
254 NotebookList.prototype.new_item = function (index, selectable) {
280 NotebookList.prototype.new_item = function (index, selectable) {
255 var row = $('<div/>')
281 var row = $('<div/>')
256 .addClass("list_item")
282 .addClass("list_item")
257 .addClass("row");
283 .addClass("row");
258
284
259 var item = $("<div/>")
285 var item = $("<div/>")
260 .addClass("col-md-12")
286 .addClass("col-md-12")
261 .appendTo(row);
287 .appendTo(row);
262
288
263 var checkbox;
289 var checkbox;
264 if (selectable !== undefined) {
290 if (selectable !== undefined) {
265 checkbox = $('<input/>')
291 checkbox = $('<input/>')
266 .attr('type', 'checkbox')
292 .attr('type', 'checkbox')
267 .attr('title', 'Click here to rename, delete, etc.')
293 .attr('title', 'Click here to rename, delete, etc.')
268 .appendTo(item);
294 .appendTo(item);
269 }
295 }
270
296
271 $('<i/>')
297 $('<i/>')
272 .addClass('item_icon')
298 .addClass('item_icon')
273 .appendTo(item);
299 .appendTo(item);
274
300
275 var link = $("<a/>")
301 var link = $("<a/>")
276 .addClass("item_link")
302 .addClass("item_link")
277 .appendTo(item);
303 .appendTo(item);
278
304
279 $("<span/>")
305 $("<span/>")
280 .addClass("item_name")
306 .addClass("item_name")
281 .appendTo(link);
307 .appendTo(link);
282
308
283 if (selectable === false) {
309 if (selectable === false) {
284 checkbox.css('visibility', 'hidden');
310 checkbox.css('visibility', 'hidden');
285 } else if (selectable === true) {
311 } else if (selectable === true) {
286 var that = this;
312 var that = this;
287 link.click(function(e) {
313 link.click(function(e) {
288 e.stopPropagation();
314 e.stopPropagation();
289 });
315 });
290 checkbox.click(function(e) {
316 checkbox.click(function(e) {
291 e.stopPropagation();
317 e.stopPropagation();
292 that._selection_changed();
318 that._selection_changed();
293 });
319 });
294 row.click(function(e) {
320 row.click(function(e) {
295 e.stopPropagation();
321 e.stopPropagation();
296 checkbox.prop('checked', !checkbox.prop('checked'));
322 checkbox.prop('checked', !checkbox.prop('checked'));
297 that._selection_changed();
323 that._selection_changed();
298 });
324 });
299 }
325 }
300
326
301 var buttons = $('<div/>')
327 var buttons = $('<div/>')
302 .addClass("item_buttons pull-right")
328 .addClass("item_buttons pull-right")
303 .appendTo(item);
329 .appendTo(item);
304
330
305 $('<div/>')
331 $('<div/>')
306 .addClass('running-indicator')
332 .addClass('running-indicator')
307 .text('Running')
333 .text('Running')
308 .css('visibility', 'hidden')
334 .css('visibility', 'hidden')
309 .appendTo(buttons);
335 .appendTo(buttons);
310
336
311 if (index === -1) {
337 if (index === -1) {
312 this.element.append(row);
338 this.element.append(row);
313 } else {
339 } else {
314 this.element.children().eq(index).after(row);
340 this.element.children().eq(index).after(row);
315 }
341 }
316 return row;
342 return row;
317 };
343 };
318
344
319
345
320 NotebookList.icons = {
346 NotebookList.icons = {
321 directory: 'folder_icon',
347 directory: 'folder_icon',
322 notebook: 'notebook_icon',
348 notebook: 'notebook_icon',
323 file: 'file_icon',
349 file: 'file_icon',
324 };
350 };
325
351
326 NotebookList.uri_prefixes = {
352 NotebookList.uri_prefixes = {
327 directory: 'tree',
353 directory: 'tree',
328 notebook: 'notebooks',
354 notebook: 'notebooks',
329 file: 'edit',
355 file: 'edit',
330 };
356 };
331
357
332 NotebookList.prototype._selection_changed = function() {
358 NotebookList.prototype._selection_changed = function() {
333 var selected = [];
359 var selected = [];
334 var has_running_notebook = false;
360 var has_running_notebook = false;
335 var has_directory = false;
361 var has_directory = false;
336 var has_file = false;
362 var has_file = false;
337 var that = this;
363 var that = this;
338 $('.list_item :checked').each(function(index, item) {
364 $('.list_item :checked').each(function(index, item) {
339 var parent = $(item).parent().parent();
365 var parent = $(item).parent().parent();
340 selected.push({
366 selected.push({
341 name: parent.data('name'),
367 name: parent.data('name'),
342 path: parent.data('path'),
368 path: parent.data('path'),
343 type: parent.data('type')
369 type: parent.data('type')
344 });
370 });
345
371
346 has_running_notebook = has_running_notebook ||
372 has_running_notebook = has_running_notebook ||
347 (parent.data('type') == 'notebook' && that.sessions[parent.data('path')] !== undefined);
373 (parent.data('type') == 'notebook' && that.sessions[parent.data('path')] !== undefined);
348 has_file = has_file || parent.data('type') == 'file';
374 has_file = has_file || parent.data('type') == 'file';
349 has_directory = has_directory || parent.data('type') == 'directory';
375 has_directory = has_directory || parent.data('type') == 'directory';
350 });
376 });
351 this.selected = selected;
377 this.selected = selected;
352
378
353 // Rename is only visible when one item is selected.
379 // Rename is only visible when one item is selected.
354 if (selected.length==1) {
380 if (selected.length==1) {
355 $('.rename-button').css('display', 'inline-block');
381 $('.rename-button').css('display', 'inline-block');
356 } else {
382 } else {
357 $('.rename-button').css('display', 'none');
383 $('.rename-button').css('display', 'none');
358 }
384 }
359
385
360 // Shutdown is only visible when one or more notebooks are visible.
386 // Shutdown is only visible when one or more notebooks are visible.
361 if (has_running_notebook && !(has_file || has_directory)) {
387 if (has_running_notebook && !(has_file || has_directory)) {
362 $('.shutdown-button').css('display', 'inline-block');
388 $('.shutdown-button').css('display', 'inline-block');
363 } else {
389 } else {
364 $('.shutdown-button').css('display', 'none');
390 $('.shutdown-button').css('display', 'none');
365 }
391 }
366
392
367 // Duplicate isn't visible if a directory is selected.
393 // Duplicate isn't visible if a directory is selected.
368 if (selected.length > 0 && !has_directory) {
394 if (selected.length > 0 && !has_directory) {
369 $('.duplicate-button').css('display', 'inline-block');
395 $('.duplicate-button').css('display', 'inline-block');
370 } else {
396 } else {
371 $('.duplicate-button').css('display', 'none');
397 $('.duplicate-button').css('display', 'none');
372 }
398 }
373
399
374 // Delete is visible if one or more items are selected.
400 // Delete is visible if one or more items are selected.
375 if (selected.length > 0) {
401 if (selected.length > 0) {
376 $('.delete-button').css('display', 'inline-block');
402 $('.delete-button').css('display', 'inline-block');
377 } else {
403 } else {
378 $('.delete-button').css('display', 'none');
404 $('.delete-button').css('display', 'none');
379 }
405 }
380 };
406 };
381
407
382 NotebookList.prototype.add_link = function (model, item) {
408 NotebookList.prototype.add_link = function (model, item) {
383 var path = model.path,
409 var path = model.path,
384 name = model.name;
410 name = model.name;
385 item.data('name', name);
411 item.data('name', name);
386 item.data('path', path);
412 item.data('path', path);
387 item.data('type', model.type);
413 item.data('type', model.type);
388 item.find(".item_name").text(name);
414 item.find(".item_name").text(name);
389 var icon = NotebookList.icons[model.type];
415 var icon = NotebookList.icons[model.type];
390 var uri_prefix = NotebookList.uri_prefixes[model.type];
416 var uri_prefix = NotebookList.uri_prefixes[model.type];
391 item.find(".item_icon").addClass(icon).addClass('icon-fixed-width');
417 item.find(".item_icon").addClass(icon).addClass('icon-fixed-width');
392 var link = item.find("a.item_link")
418 var link = item.find("a.item_link")
393 .attr('href',
419 .attr('href',
394 utils.url_join_encode(
420 utils.url_join_encode(
395 this.base_url,
421 this.base_url,
396 uri_prefix,
422 uri_prefix,
397 path
423 path
398 )
424 )
399 );
425 );
400
426
401 var running = (model.type == 'notebook' && this.sessions[path] !== undefined);
427 var running = (model.type == 'notebook' && this.sessions[path] !== undefined);
402 item.find(".item_buttons .running-indicator").css('visibility', running ? '' : 'hidden');
428 item.find(".item_buttons .running-indicator").css('visibility', running ? '' : 'hidden');
403
429
404 // directory nav doesn't open new tabs
430 // directory nav doesn't open new tabs
405 // files, notebooks do
431 // files, notebooks do
406 if (model.type !== "directory") {
432 if (model.type !== "directory") {
407 link.attr('target','_blank');
433 link.attr('target','_blank');
408 }
434 }
409 };
435 };
410
436
411
437
412 NotebookList.prototype.add_name_input = function (name, item, icon_type) {
438 NotebookList.prototype.add_name_input = function (name, item, icon_type) {
413 item.data('name', name);
439 item.data('name', name);
414 item.find(".item_icon").addClass(NotebookList.icons[icon_type]).addClass('icon-fixed-width');
440 item.find(".item_icon").addClass(NotebookList.icons[icon_type]).addClass('icon-fixed-width');
415 item.find(".item_name").empty().append(
441 item.find(".item_name").empty().append(
416 $('<input/>')
442 $('<input/>')
417 .addClass("filename_input")
443 .addClass("filename_input")
418 .attr('value', name)
444 .attr('value', name)
419 .attr('size', '30')
445 .attr('size', '30')
420 .attr('type', 'text')
446 .attr('type', 'text')
421 .keyup(function(event){
447 .keyup(function(event){
422 if(event.keyCode == 13){item.find('.upload_button').click();}
448 if(event.keyCode == 13){item.find('.upload_button').click();}
423 else if(event.keyCode == 27){item.remove();}
449 else if(event.keyCode == 27){item.remove();}
424 })
450 })
425 );
451 );
426 };
452 };
427
453
428
454
429 NotebookList.prototype.add_file_data = function (data, item) {
455 NotebookList.prototype.add_file_data = function (data, item) {
430 item.data('filedata', data);
456 item.data('filedata', data);
431 };
457 };
432
458
433
459
434 NotebookList.prototype.shutdown_selected = function() {
460 NotebookList.prototype.shutdown_selected = function() {
435 var that = this;
461 var that = this;
436 this.selected.forEach(function(item) {
462 this.selected.forEach(function(item) {
437 if (item.type == 'notebook') {
463 if (item.type == 'notebook') {
438 that.shutdown_notebook(item.path);
464 that.shutdown_notebook(item.path);
439 }
465 }
440 });
466 });
441 };
467 };
442
468
443 NotebookList.prototype.shutdown_notebook = function(path) {
469 NotebookList.prototype.shutdown_notebook = function(path) {
444 var that = this;
470 var that = this;
445 var settings = {
471 var settings = {
446 processData : false,
472 processData : false,
447 cache : false,
473 cache : false,
448 type : "DELETE",
474 type : "DELETE",
449 dataType : "json",
475 dataType : "json",
450 success : function () {
476 success : function () {
451 that.load_sessions();
477 that.load_sessions();
452 },
478 },
453 error : utils.log_ajax_error,
479 error : utils.log_ajax_error,
454 };
480 };
455
481
456 var session = this.sessions[path];
482 var session = this.sessions[path];
457 if (session) {
483 if (session) {
458 var url = utils.url_join_encode(
484 var url = utils.url_join_encode(
459 this.base_url,
485 this.base_url,
460 'api/sessions',
486 'api/sessions',
461 session
487 session
462 );
488 );
463 $.ajax(url, settings);
489 $.ajax(url, settings);
464 }
490 }
465 }
491 };
466
492
467 NotebookList.prototype.rename_selected = function() {
493 NotebookList.prototype.rename_selected = function() {
468 if (this.selected.length != 1) return;
494 if (this.selected.length != 1) return;
469
495
470 var that = this;
496 var that = this;
471 var path = this.selected[0].path;
497 var path = this.selected[0].path;
472 var input = $('<input/>').attr('type','text').attr('size','25').addClass('form-control')
498 var input = $('<input/>').attr('type','text').attr('size','25').addClass('form-control')
473 .val(path);
499 .val(path);
474 var dialog_body = $('<div/>').append(
500 var dialog_body = $('<div/>').append(
475 $("<p/>").addClass("rename-message")
501 $("<p/>").addClass("rename-message")
476 .text('Enter a new directory name:')
502 .text('Enter a new directory name:')
477 ).append(
503 ).append(
478 $("<br/>")
504 $("<br/>")
479 ).append(input);
505 ).append(input);
480 var d = dialog.modal({
506 var d = dialog.modal({
481 title : "Rename directory",
507 title : "Rename directory",
482 body : dialog_body,
508 body : dialog_body,
483 buttons : {
509 buttons : {
484 OK : {
510 OK : {
485 class: "btn-primary",
511 class: "btn-primary",
486 click: function() {
512 click: function() {
487 that.contents.rename(path, input.val()).then(function() {
513 that.contents.rename(path, input.val()).then(function() {
488 that.load_list();
514 that.load_list();
489 }).catch(function(e) {
515 }).catch(function(e) {
490 dialog.modal({
516 dialog.modal({
491 title : "Error",
517 title: "Rename Failed",
492 body : $('<div/>')
518 body: $('<div/>')
493 .text("An error occurred while renaming \"" + path + "\" to \"" + input.val() + "\".")
519 .text("An error occurred while renaming \"" + path + "\" to \"" + input.val() + "\".")
494 .append($('<div/>').addClass('alert alert-danger').text(String(e))),
520 .append($('<div/>')
495 buttons : {
521 .addClass('alert alert-danger')
496 OK : {}
522 .text(e.message || e)),
523 buttons: {
524 OK: {'class': 'btn-primary'}
497 }
525 }
498 });
526 });
499 });
527 });
500 }
528 }
501 },
529 },
502 Cancel : {}
530 Cancel : {}
503 },
531 },
504 open : function () {
532 open : function () {
505 // Upon ENTER, click the OK button.
533 // Upon ENTER, click the OK button.
506 input.keydown(function (event) {
534 input.keydown(function (event) {
507 if (event.which === keyboard.keycodes.enter) {
535 if (event.which === keyboard.keycodes.enter) {
508 d.find('.btn-primary').first().click();
536 d.find('.btn-primary').first().click();
509 return false;
537 return false;
510 }
538 }
511 });
539 });
512 input.focus().select();
540 input.focus().select();
513 }
541 }
514 });
542 });
515 };
543 };
516
544
517 NotebookList.prototype.delete_selected = function() {
545 NotebookList.prototype.delete_selected = function() {
518 var message;
546 var message;
519 if (this.selected.length == 1) {
547 if (this.selected.length == 1) {
520 message = 'Are you sure you want to permanently delete: ' + this.selected[0].name + '?';
548 message = 'Are you sure you want to permanently delete: ' + this.selected[0].name + '?';
521 } else {
549 } else {
522 message = 'Are you sure you want to permanently delete the ' + this.selected.length + ' files/folders selected?';
550 message = 'Are you sure you want to permanently delete the ' + this.selected.length + ' files/folders selected?';
523 }
551 }
524 var that = this;
552 var that = this;
525 dialog.modal({
553 dialog.modal({
526 title : "Delete",
554 title : "Delete",
527 body : message,
555 body : message,
528 buttons : {
556 buttons : {
529 Delete : {
557 Delete : {
530 class: "btn-danger",
558 class: "btn-danger",
531 click: function() {
559 click: function() {
532 // Shutdown any/all selected notebooks before deleting
560 // Shutdown any/all selected notebooks before deleting
533 // the files.
561 // the files.
534 that.shutdown_selected();
562 that.shutdown_selected();
535
563
536 // Delete selected.
564 // Delete selected.
537 that.selected.forEach(function(item) {
565 that.selected.forEach(function(item) {
538 that.contents.delete(item.path).then(function() {
566 that.contents.delete(item.path).then(function() {
539 that.notebook_deleted(item.path);
567 that.notebook_deleted(item.path);
540 }).catch(function(e) {
568 }).catch(function(e) {
541 dialog.modal({
569 dialog.modal({
542 title : "Error",
570 title: "Delete Failed",
543 body : $('<div/>')
571 body: $('<div/>')
544 .text("An error occurred while deleting \"" + item.path + "\".")
572 .text("An error occurred while deleting \"" + item.path + "\".")
545 .append($('<div/>').addClass('alert alert-danger').text(String(e))),
573 .append($('<div/>')
546 buttons : {
574 .addClass('alert alert-danger')
547 OK : {}
575 .text(e.message || e)),
576 buttons: {
577 OK: {'class': 'btn-primary'}
548 }
578 }
549 });
579 });
550 });
580 });
551 });
581 });
552 }
582 }
553 },
583 },
554 Cancel : {}
584 Cancel : {}
555 }
585 }
556 });
586 });
557 };
587 };
558
588
559 NotebookList.prototype.duplicate_selected = function() {
589 NotebookList.prototype.duplicate_selected = function() {
560 var message;
590 var message;
561 if (this.selected.length == 1) {
591 if (this.selected.length == 1) {
562 message = 'Are you sure you want to duplicate: ' + this.selected[0].name + '?';
592 message = 'Are you sure you want to duplicate: ' + this.selected[0].name + '?';
563 } else {
593 } else {
564 message = 'Are you sure you want to duplicate the ' + this.selected.length + ' files selected?';
594 message = 'Are you sure you want to duplicate the ' + this.selected.length + ' files selected?';
565 }
595 }
566 var that = this;
596 var that = this;
567 dialog.modal({
597 dialog.modal({
568 title : "Delete",
598 title : "Delete",
569 body : message,
599 body : message,
570 buttons : {
600 buttons : {
571 Duplicate : {
601 Duplicate : {
572 class: "btn-primary",
602 class: "btn-primary",
573 click: function() {
603 click: function() {
574 that.selected.forEach(function(item) {
604 that.selected.forEach(function(item) {
575 that.contents.copy(item.path, that.notebook_path).then(function () {
605 that.contents.copy(item.path, that.notebook_path).then(function () {
576 that.load_list();
606 that.load_list();
577 }).catch(function(e) {
607 }).catch(function(e) {
578 dialog.modal({
608 dialog.modal({
579 title : "Error",
609 title: "Delete Failed",
580 body : $('<div/>')
610 body: $('<div/>')
581 .text("An error occurred while copying \"" + item.path + "\".")
611 .text("An error occurred while deleting \"" + item.path + "\".")
582 .append($('<div/>').addClass('alert alert-danger').text(String(e))),
612 .append($('<div/>')
583 buttons : {
613 .addClass('alert alert-danger')
584 OK : {}
614 .text(e.message || e)),
615 buttons: {
616 OK: {'class': 'btn-primary'}
585 }
617 }
586 });
618 });
587 });
619 });
588 });
620 });
589 }
621 }
590 },
622 },
591 Cancel : {}
623 Cancel : {}
592 }
624 }
593 });
625 });
594 };
626 };
595
627
596 NotebookList.prototype.notebook_deleted = function(path) {
628 NotebookList.prototype.notebook_deleted = function(path) {
597 /**
629 /**
598 * Remove the deleted notebook.
630 * Remove the deleted notebook.
599 */
631 */
600 var that = this;
632 var that = this;
601 $( ":data(path)" ).each(function() {
633 $( ":data(path)" ).each(function() {
602 var element = $(this);
634 var element = $(this);
603 if (element.data("path") === path) {
635 if (element.data("path") === path) {
604 element.remove();
636 element.remove();
605 events.trigger('notebook_deleted.NotebookList');
637 events.trigger('notebook_deleted.NotebookList');
606 that._selection_changed();
638 that._selection_changed();
607 }
639 }
608 });
640 });
609 };
641 };
610
642
611
643
612 NotebookList.prototype.add_upload_button = function (item) {
644 NotebookList.prototype.add_upload_button = function (item) {
613 var that = this;
645 var that = this;
614 var upload_button = $('<button/>').text("Upload")
646 var upload_button = $('<button/>').text("Upload")
615 .addClass('btn btn-primary btn-xs upload_button')
647 .addClass('btn btn-primary btn-xs upload_button')
616 .click(function (e) {
648 .click(function (e) {
617 var filename = item.find('.item_name > input').val();
649 var filename = item.find('.item_name > input').val();
618 var path = utils.url_path_join(that.notebook_path, filename);
650 var path = utils.url_path_join(that.notebook_path, filename);
619 var filedata = item.data('filedata');
651 var filedata = item.data('filedata');
620 var format = 'text';
652 var format = 'text';
621 if (filename.length === 0 || filename[0] === '.') {
653 if (filename.length === 0 || filename[0] === '.') {
622 dialog.modal({
654 dialog.modal({
623 title : 'Invalid file name',
655 title : 'Invalid file name',
624 body : "File names must be at least one character and not start with a dot",
656 body : "File names must be at least one character and not start with a dot",
625 buttons : {'OK' : { 'class' : 'btn-primary' }}
657 buttons : {'OK' : { 'class' : 'btn-primary' }}
626 });
658 });
627 return false;
659 return false;
628 }
660 }
629 if (filedata instanceof ArrayBuffer) {
661 if (filedata instanceof ArrayBuffer) {
630 // base64-encode binary file data
662 // base64-encode binary file data
631 var bytes = '';
663 var bytes = '';
632 var buf = new Uint8Array(filedata);
664 var buf = new Uint8Array(filedata);
633 var nbytes = buf.byteLength;
665 var nbytes = buf.byteLength;
634 for (var i=0; i<nbytes; i++) {
666 for (var i=0; i<nbytes; i++) {
635 bytes += String.fromCharCode(buf[i]);
667 bytes += String.fromCharCode(buf[i]);
636 }
668 }
637 filedata = btoa(bytes);
669 filedata = btoa(bytes);
638 format = 'base64';
670 format = 'base64';
639 }
671 }
640 var model = {};
672 var model = {};
641
673
642 var name_and_ext = utils.splitext(filename);
674 var name_and_ext = utils.splitext(filename);
643 var file_ext = name_and_ext[1];
675 var file_ext = name_and_ext[1];
644 var content_type;
676 var content_type;
645 if (file_ext === '.ipynb') {
677 if (file_ext === '.ipynb') {
646 model.type = 'notebook';
678 model.type = 'notebook';
647 model.format = 'json';
679 model.format = 'json';
648 try {
680 try {
649 model.content = JSON.parse(filedata);
681 model.content = JSON.parse(filedata);
650 } catch (e) {
682 } catch (e) {
651 dialog.modal({
683 dialog.modal({
652 title : 'Cannot upload invalid Notebook',
684 title : 'Cannot upload invalid Notebook',
653 body : "The error was: " + e,
685 body : "The error was: " + e,
654 buttons : {'OK' : {
686 buttons : {'OK' : {
655 'class' : 'btn-primary',
687 'class' : 'btn-primary',
656 click: function () {
688 click: function () {
657 item.remove();
689 item.remove();
658 }
690 }
659 }}
691 }}
660 });
692 });
661 return false;
693 return false;
662 }
694 }
663 content_type = 'application/json';
695 content_type = 'application/json';
664 } else {
696 } else {
665 model.type = 'file';
697 model.type = 'file';
666 model.format = format;
698 model.format = format;
667 model.content = filedata;
699 model.content = filedata;
668 content_type = 'application/octet-stream';
700 content_type = 'application/octet-stream';
669 }
701 }
670 filedata = item.data('filedata');
702 filedata = item.data('filedata');
671
703
672 var on_success = function () {
704 var on_success = function () {
673 item.removeClass('new-file');
705 item.removeClass('new-file');
674 that.add_link(model, item);
706 that.add_link(model, item);
675 that.add_delete_button(item);
707 that.add_delete_button(item);
676 that.session_list.load_sessions();
708 that.session_list.load_sessions();
677 };
709 };
678
710
679 var exists = false;
711 var exists = false;
680 $.each(that.element.find('.list_item:not(.new-file)'), function(k,v){
712 $.each(that.element.find('.list_item:not(.new-file)'), function(k,v){
681 if ($(v).data('name') === filename) { exists = true; return false; }
713 if ($(v).data('name') === filename) { exists = true; return false; }
682 });
714 });
683
715
684 if (exists) {
716 if (exists) {
685 dialog.modal({
717 dialog.modal({
686 title : "Replace file",
718 title : "Replace file",
687 body : 'There is already a file named ' + filename + ', do you want to replace it?',
719 body : 'There is already a file named ' + filename + ', do you want to replace it?',
688 buttons : {
720 buttons : {
689 Overwrite : {
721 Overwrite : {
690 class: "btn-danger",
722 class: "btn-danger",
691 click: function () {
723 click: function () {
692 that.contents.save(path, model).then(on_success);
724 that.contents.save(path, model).then(on_success);
693 }
725 }
694 },
726 },
695 Cancel : {
727 Cancel : {
696 click: function() { item.remove(); }
728 click: function() { item.remove(); }
697 }
729 }
698 }
730 }
699 });
731 });
700 } else {
732 } else {
701 that.contents.save(path, model).then(on_success);
733 that.contents.save(path, model).then(on_success);
702 }
734 }
703
735
704 return false;
736 return false;
705 });
737 });
706 var cancel_button = $('<button/>').text("Cancel")
738 var cancel_button = $('<button/>').text("Cancel")
707 .addClass("btn btn-default btn-xs")
739 .addClass("btn btn-default btn-xs")
708 .click(function (e) {
740 .click(function (e) {
709 item.remove();
741 item.remove();
710 return false;
742 return false;
711 });
743 });
712 item.find(".item_buttons").empty()
744 item.find(".item_buttons").empty()
713 .append(upload_button)
745 .append(upload_button)
714 .append(cancel_button);
746 .append(cancel_button);
715 };
747 };
716
748
717
749
718 // Backwards compatability.
750 // Backwards compatability.
719 IPython.NotebookList = NotebookList;
751 IPython.NotebookList = NotebookList;
720
752
721 return {'NotebookList': NotebookList};
753 return {'NotebookList': NotebookList};
722 });
754 });
General Comments 0
You need to be logged in to leave comments. Login now