##// END OF EJS Templates
alternate notebook upload methods...
Matthias BUSSONNIER -
Show More
@@ -0,0 +1,23 b''
1 /* We need an invisible input field on top of the sentense*/
2 /* "Drag file onto the list ..." */
3
4 .alternate_upload
5 {
6 background-color:none;
7 display: inline;
8 }
9
10 .alternate_upload.form
11 {
12 padding: 0;
13 margin:0;
14 }
15
16 .alternate_upload input.fileinput
17 {
18 background-color:red;
19 position:relative;
20 opacity: 0;
21 z-index: 2;
22 width: 356px;
23 }
@@ -1,267 +1,279 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // NotebookList
9 // NotebookList
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13
13
14 var NotebookList = function (selector) {
14 var NotebookList = function (selector) {
15 this.selector = selector;
15 this.selector = selector;
16 if (this.selector !== undefined) {
16 if (this.selector !== undefined) {
17 this.element = $(selector);
17 this.element = $(selector);
18 this.style();
18 this.style();
19 this.bind_events();
19 this.bind_events();
20 }
20 }
21 };
21 };
22
22
23 NotebookList.prototype.style = function () {
23 NotebookList.prototype.style = function () {
24 $('#notebook_toolbar').addClass('list_toolbar');
24 $('#notebook_toolbar').addClass('list_toolbar');
25 $('#drag_info').addClass('toolbar_info');
25 $('#drag_info').addClass('toolbar_info');
26 $('#notebook_buttons').addClass('toolbar_buttons');
26 $('#notebook_buttons').addClass('toolbar_buttons');
27 $('div#project_name').addClass('list_header ui-widget ui-widget-header');
27 $('div#project_name').addClass('list_header ui-widget ui-widget-header');
28 $('#refresh_notebook_list').button({
28 $('#refresh_notebook_list').button({
29 icons : {primary: 'ui-icon-arrowrefresh-1-s'},
29 icons : {primary: 'ui-icon-arrowrefresh-1-s'},
30 text : false
30 text : false
31 });
31 });
32 };
32 };
33
33
34
34
35 NotebookList.prototype.bind_events = function () {
35 NotebookList.prototype.bind_events = function () {
36 if (IPython.read_only){
36 if (IPython.read_only){
37 return;
37 return;
38 }
38 }
39 var that = this;
39 var that = this;
40 $('#refresh_notebook_list').click(function () {
40 $('#refresh_notebook_list').click(function () {
41 that.load_list();
41 that.load_list();
42 });
42 });
43 this.element.bind('dragover', function () {
43 this.element.bind('dragover', function () {
44 return false;
44 return false;
45 });
45 });
46 this.element.bind('drop', function (event) {
46 this.element.bind('drop', function(event){
47 var files = event.originalEvent.dataTransfer.files;
47 console.log('bound to drop');
48 for (var i = 0, f; f = files[i]; i++) {
48 that.handelFilesUpload(event,'drop');
49 var reader = new FileReader();
50 reader.readAsText(f);
51 var fname = f.name.split('.');
52 var nbname = fname.slice(0,-1).join('.');
53 var nbformat = fname.slice(-1)[0];
54 if (nbformat === 'ipynb') {nbformat = 'json';};
55 if (nbformat === 'py' || nbformat === 'json') {
56 var item = that.new_notebook_item(0);
57 that.add_name_input(nbname, item);
58 item.data('nbformat', nbformat);
59 // Store the notebook item in the reader so we can use it later
60 // to know which item it belongs to.
61 $(reader).data('item', item);
62 reader.onload = function (event) {
63 var nbitem = $(event.target).data('item');
64 that.add_notebook_data(event.target.result, nbitem);
65 that.add_upload_button(nbitem);
66 };
67 };
68 }
69 return false;
49 return false;
70 });
50 });
71 };
51 };
72
52
53 NotebookList.prototype.handelFilesUpload = function(event, dropOrForm) {
54 var that = this;
55 var files;
56 if(dropOrForm =='drop'){
57 files = event.originalEvent.dataTransfer.files;
58 } else
59 {
60 files = event.originalEvent.target.files
61 }
62 for (var i = 0, f; f = files[i]; i++) {
63 var reader = new FileReader();
64 reader.readAsText(f);
65 var fname = f.name.split('.');
66 var nbname = fname.slice(0,-1).join('.');
67 var nbformat = fname.slice(-1)[0];
68 if (nbformat === 'ipynb') {nbformat = 'json';};
69 if (nbformat === 'py' || nbformat === 'json') {
70 var item = that.new_notebook_item(0);
71 that.add_name_input(nbname, item);
72 item.data('nbformat', nbformat);
73 // Store the notebook item in the reader so we can use it later
74 // to know which item it belongs to.
75 $(reader).data('item', item);
76 reader.onload = function (event) {
77 var nbitem = $(event.target).data('item');
78 that.add_notebook_data(event.target.result, nbitem);
79 that.add_upload_button(nbitem);
80 };
81 };
82 }
83 return false;
84 };
73
85
74 NotebookList.prototype.clear_list = function () {
86 NotebookList.prototype.clear_list = function () {
75 this.element.children('.list_item').remove();
87 this.element.children('.list_item').remove();
76 }
88 }
77
89
78
90
79 NotebookList.prototype.load_list = function () {
91 NotebookList.prototype.load_list = function () {
80 this.clear_list();
92 this.clear_list();
81 var settings = {
93 var settings = {
82 processData : false,
94 processData : false,
83 cache : false,
95 cache : false,
84 type : "GET",
96 type : "GET",
85 dataType : "json",
97 dataType : "json",
86 success : $.proxy(this.list_loaded, this)
98 success : $.proxy(this.list_loaded, this)
87 };
99 };
88 var url = $('body').data('baseProjectUrl') + 'notebooks';
100 var url = $('body').data('baseProjectUrl') + 'notebooks';
89 $.ajax(url, settings);
101 $.ajax(url, settings);
90 };
102 };
91
103
92
104
93 NotebookList.prototype.list_loaded = function (data, status, xhr) {
105 NotebookList.prototype.list_loaded = function (data, status, xhr) {
94 var len = data.length;
106 var len = data.length;
95 // Todo: remove old children
107 // Todo: remove old children
96 for (var i=0; i<len; i++) {
108 for (var i=0; i<len; i++) {
97 var notebook_id = data[i].notebook_id;
109 var notebook_id = data[i].notebook_id;
98 var nbname = data[i].name;
110 var nbname = data[i].name;
99 var item = this.new_notebook_item(i);
111 var item = this.new_notebook_item(i);
100 this.add_link(notebook_id, nbname, item);
112 this.add_link(notebook_id, nbname, item);
101 if (!IPython.read_only){
113 if (!IPython.read_only){
102 // hide delete buttons when readonly
114 // hide delete buttons when readonly
103 this.add_delete_button(item);
115 this.add_delete_button(item);
104 }
116 }
105 };
117 };
106 };
118 };
107
119
108
120
109 NotebookList.prototype.new_notebook_item = function (index) {
121 NotebookList.prototype.new_notebook_item = function (index) {
110 var item = $('<div/>');
122 var item = $('<div/>');
111 item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
123 item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
112 item.css('border-top-style','none');
124 item.css('border-top-style','none');
113 var item_name = $('<span/>').addClass('item_name');
125 var item_name = $('<span/>').addClass('item_name');
114
126
115 item.append(item_name);
127 item.append(item_name);
116 if (index === -1) {
128 if (index === -1) {
117 this.element.append(item);
129 this.element.append(item);
118 } else {
130 } else {
119 this.element.children().eq(index).after(item);
131 this.element.children().eq(index).after(item);
120 }
132 }
121 return item;
133 return item;
122 };
134 };
123
135
124
136
125 NotebookList.prototype.add_link = function (notebook_id, nbname, item) {
137 NotebookList.prototype.add_link = function (notebook_id, nbname, item) {
126 item.data('nbname', nbname);
138 item.data('nbname', nbname);
127 item.data('notebook_id', notebook_id);
139 item.data('notebook_id', notebook_id);
128 var new_item_name = $('<span/>').addClass('item_name');
140 var new_item_name = $('<span/>').addClass('item_name');
129 new_item_name.append(
141 new_item_name.append(
130 $('<a/>').
142 $('<a/>').
131 attr('href', $('body').data('baseProjectUrl')+notebook_id).
143 attr('href', $('body').data('baseProjectUrl')+notebook_id).
132 attr('target','_blank').
144 attr('target','_blank').
133 text(nbname)
145 text(nbname)
134 );
146 );
135 var e = item.find('.item_name');
147 var e = item.find('.item_name');
136 if (e.length === 0) {
148 if (e.length === 0) {
137 item.append(new_item_name);
149 item.append(new_item_name);
138 } else {
150 } else {
139 e.replaceWith(new_item_name);
151 e.replaceWith(new_item_name);
140 };
152 };
141 };
153 };
142
154
143
155
144 NotebookList.prototype.add_name_input = function (nbname, item) {
156 NotebookList.prototype.add_name_input = function (nbname, item) {
145 item.data('nbname', nbname);
157 item.data('nbname', nbname);
146 var new_item_name = $('<span/>').addClass('item_name');
158 var new_item_name = $('<span/>').addClass('item_name');
147 new_item_name.append(
159 new_item_name.append(
148 $('<input/>').addClass('ui-widget ui-widget-content').
160 $('<input/>').addClass('ui-widget ui-widget-content').
149 attr('value', nbname).
161 attr('value', nbname).
150 attr('size', '30').
162 attr('size', '30').
151 attr('type', 'text')
163 attr('type', 'text')
152 );
164 );
153 var e = item.find('.item_name');
165 var e = item.find('.item_name');
154 if (e.length === 0) {
166 if (e.length === 0) {
155 item.append(new_item_name);
167 item.append(new_item_name);
156 } else {
168 } else {
157 e.replaceWith(new_item_name);
169 e.replaceWith(new_item_name);
158 };
170 };
159 };
171 };
160
172
161
173
162 NotebookList.prototype.add_notebook_data = function (data, item) {
174 NotebookList.prototype.add_notebook_data = function (data, item) {
163 item.data('nbdata',data);
175 item.data('nbdata',data);
164 };
176 };
165
177
166
178
167 NotebookList.prototype.add_delete_button = function (item) {
179 NotebookList.prototype.add_delete_button = function (item) {
168 var new_buttons = $('<span/>').addClass('item_buttons');
180 var new_buttons = $('<span/>').addClass('item_buttons');
169 var delete_button = $('<button>Delete</button>').button().
181 var delete_button = $('<button>Delete</button>').button().
170 click(function (e) {
182 click(function (e) {
171 // $(this) is the button that was clicked.
183 // $(this) is the button that was clicked.
172 var that = $(this);
184 var that = $(this);
173 // We use the nbname and notebook_id from the parent notebook_item element's
185 // We use the nbname and notebook_id from the parent notebook_item element's
174 // data because the outer scopes values change as we iterate through the loop.
186 // data because the outer scopes values change as we iterate through the loop.
175 var parent_item = that.parents('div.list_item');
187 var parent_item = that.parents('div.list_item');
176 var nbname = parent_item.data('nbname');
188 var nbname = parent_item.data('nbname');
177 var notebook_id = parent_item.data('notebook_id');
189 var notebook_id = parent_item.data('notebook_id');
178 var dialog = $('<div/>');
190 var dialog = $('<div/>');
179 dialog.html('Are you sure you want to permanently delete the notebook: ' + nbname + '?');
191 dialog.html('Are you sure you want to permanently delete the notebook: ' + nbname + '?');
180 parent_item.append(dialog);
192 parent_item.append(dialog);
181 dialog.dialog({
193 dialog.dialog({
182 resizable: false,
194 resizable: false,
183 modal: true,
195 modal: true,
184 title: "Delete notebook",
196 title: "Delete notebook",
185 buttons : {
197 buttons : {
186 "Delete": function () {
198 "Delete": function () {
187 var settings = {
199 var settings = {
188 processData : false,
200 processData : false,
189 cache : false,
201 cache : false,
190 type : "DELETE",
202 type : "DELETE",
191 dataType : "json",
203 dataType : "json",
192 success : function (data, status, xhr) {
204 success : function (data, status, xhr) {
193 parent_item.remove();
205 parent_item.remove();
194 }
206 }
195 };
207 };
196 var url = $('body').data('baseProjectUrl') + 'notebooks/' + notebook_id;
208 var url = $('body').data('baseProjectUrl') + 'notebooks/' + notebook_id;
197 $.ajax(url, settings);
209 $.ajax(url, settings);
198 $(this).dialog('close');
210 $(this).dialog('close');
199 },
211 },
200 "Cancel": function () {
212 "Cancel": function () {
201 $(this).dialog('close');
213 $(this).dialog('close');
202 }
214 }
203 }
215 }
204 });
216 });
205 });
217 });
206 new_buttons.append(delete_button);
218 new_buttons.append(delete_button);
207 var e = item.find('.item_buttons');
219 var e = item.find('.item_buttons');
208 if (e.length === 0) {
220 if (e.length === 0) {
209 item.append(new_buttons);
221 item.append(new_buttons);
210 } else {
222 } else {
211 e.replaceWith(new_buttons);
223 e.replaceWith(new_buttons);
212 };
224 };
213 };
225 };
214
226
215
227
216 NotebookList.prototype.add_upload_button = function (item) {
228 NotebookList.prototype.add_upload_button = function (item) {
217 var that = this;
229 var that = this;
218 var new_buttons = $('<span/>').addClass('item_buttons');
230 var new_buttons = $('<span/>').addClass('item_buttons');
219 var upload_button = $('<button>Upload</button>').button().
231 var upload_button = $('<button>Upload</button>').button().
220 click(function (e) {
232 click(function (e) {
221 var nbname = item.find('.item_name > input').attr('value');
233 var nbname = item.find('.item_name > input').attr('value');
222 var nbformat = item.data('nbformat');
234 var nbformat = item.data('nbformat');
223 var nbdata = item.data('nbdata');
235 var nbdata = item.data('nbdata');
224 var content_type = 'text/plain';
236 var content_type = 'text/plain';
225 if (nbformat === 'json') {
237 if (nbformat === 'json') {
226 content_type = 'application/json';
238 content_type = 'application/json';
227 } else if (nbformat === 'py') {
239 } else if (nbformat === 'py') {
228 content_type = 'application/x-python';
240 content_type = 'application/x-python';
229 };
241 };
230 var settings = {
242 var settings = {
231 processData : false,
243 processData : false,
232 cache : false,
244 cache : false,
233 type : 'POST',
245 type : 'POST',
234 dataType : 'json',
246 dataType : 'json',
235 data : nbdata,
247 data : nbdata,
236 headers : {'Content-Type': content_type},
248 headers : {'Content-Type': content_type},
237 success : function (data, status, xhr) {
249 success : function (data, status, xhr) {
238 that.add_link(data, nbname, item);
250 that.add_link(data, nbname, item);
239 that.add_delete_button(item);
251 that.add_delete_button(item);
240 }
252 }
241 };
253 };
242
254
243 var qs = $.param({name:nbname, format:nbformat});
255 var qs = $.param({name:nbname, format:nbformat});
244 var url = $('body').data('baseProjectUrl') + 'notebooks?' + qs;
256 var url = $('body').data('baseProjectUrl') + 'notebooks?' + qs;
245 $.ajax(url, settings);
257 $.ajax(url, settings);
246 });
258 });
247 var cancel_button = $('<button>Cancel</button>').button().
259 var cancel_button = $('<button>Cancel</button>').button().
248 click(function (e) {
260 click(function (e) {
249 item.remove();
261 item.remove();
250 });
262 });
251 upload_button.addClass('upload_button');
263 upload_button.addClass('upload_button');
252 new_buttons.append(upload_button).append(cancel_button);
264 new_buttons.append(upload_button).append(cancel_button);
253 var e = item.find('.item_buttons');
265 var e = item.find('.item_buttons');
254 if (e.length === 0) {
266 if (e.length === 0) {
255 item.append(new_buttons);
267 item.append(new_buttons);
256 } else {
268 } else {
257 e.replaceWith(new_buttons);
269 e.replaceWith(new_buttons);
258 };
270 };
259 };
271 };
260
272
261
273
262 IPython.NotebookList = NotebookList;
274 IPython.NotebookList = NotebookList;
263
275
264 return IPython;
276 return IPython;
265
277
266 }(IPython));
278 }(IPython));
267
279
@@ -1,39 +1,42 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // On document ready
9 // On document ready
10 //============================================================================
10 //============================================================================
11
11
12
12
13 $(document).ready(function () {
13 $(document).ready(function () {
14
14
15 IPython.page = new IPython.Page();
15 IPython.page = new IPython.Page();
16
16
17 $('div#tabs').tabs();
17 $('div#tabs').tabs();
18 $('div#tabs').on('tabsselect', function (event, ui) {
18 $('div#tabs').on('tabsselect', function (event, ui) {
19 var new_url = $('body').data('baseProjectUrl') + '#' + ui.panel.id;
19 var new_url = $('body').data('baseProjectUrl') + '#' + ui.panel.id;
20 window.history.replaceState({}, '', new_url);
20 window.history.replaceState({}, '', new_url);
21 });
21 });
22 $('div#main_app').addClass('border-box-sizing ui-widget');
22 $('div#main_app').addClass('border-box-sizing ui-widget');
23 $('div#notebooks_toolbar').addClass('ui-widget ui-helper-clearfix');
23 $('div#notebooks_toolbar').addClass('ui-widget ui-helper-clearfix');
24 $('#new_notebook').button().click(function (e) {
24 $('#new_notebook').button().click(function (e) {
25 window.open($('body').data('baseProjectUrl')+'new');
25 window.open($('body').data('baseProjectUrl')+'new');
26 });
26 });
27
27
28 IPython.read_only = $('body').data('readOnly') === 'True';
28 IPython.read_only = $('body').data('readOnly') === 'True';
29 IPython.notebook_list = new IPython.NotebookList('div#notebook_list');
29 IPython.notebook_list = new IPython.NotebookList('div#notebook_list');
30 IPython.cluster_list = new IPython.ClusterList('div#cluster_list');
30 IPython.cluster_list = new IPython.ClusterList('div#cluster_list');
31 IPython.login_widget = new IPython.LoginWidget('span#login_widget');
31 IPython.login_widget = new IPython.LoginWidget('span#login_widget');
32
32
33 IPython.notebook_list.load_list();
33 IPython.notebook_list.load_list();
34 IPython.cluster_list.load_list();
34 IPython.cluster_list.load_list();
35
36 IPython.page.show();
35 IPython.page.show();
37
36
37 // bound the upload method to the on change of the file select list
38 $("#alternate_upload").change(function (event){
39 IPython.notebook_list.handelFilesUpload(event,'form');
40 });
38 });
41 });
39
42
@@ -1,77 +1,84 b''
1 {% extends page.html %}
1 {% extends page.html %}
2
2
3 {% block title %}IPython Dashboard{% end %}
3 {% block title %}IPython Dashboard{% end %}
4
4
5 {% block stylesheet %}
5 {% block stylesheet %}
6 <link rel="stylesheet" href="{{static_url("css/projectdashboard.css") }}" type="text/css" />
6 <link rel="stylesheet" href="{{static_url("css/projectdashboard.css") }}" type="text/css" />
7 <link rel="stylesheet" href="{{static_url("css/alternate_uploadform.css") }}" type="text/css" />
7 {% end %}
8 {% end %}
8
9
9
10
10 {% block params %}
11 {% block params %}
11
12
12 data-project={{project}}
13 data-project={{project}}
13 data-base-project-url={{base_project_url}}
14 data-base-project-url={{base_project_url}}
14 data-base-kernel-url={{base_kernel_url}}
15 data-base-kernel-url={{base_kernel_url}}
15 data-read-only={{read_only}}
16 data-read-only={{read_only}}
16
17
17 {% end %}
18 {% end %}
18
19
19
20
20 {% block site %}
21 {% block site %}
21
22
22 <div id="main_app">
23 <div id="main_app">
23
24
24 <div id="tabs">
25 <div id="tabs">
25 <ul>
26 <ul>
26 <li><a href="#tab1">Notebooks</a></li>
27 <li><a href="#tab1">Notebooks</a></li>
27 <li><a href="#tab2">Clusters</a></li>
28 <li><a href="#tab2">Clusters</a></li>
28 </ul>
29 </ul>
29
30
30 <div id="tab1">
31 <div id="tab1">
31 {% if logged_in or not read_only %}
32 {% if logged_in or not read_only %}
32 <div id="notebook_toolbar">
33 <div id="notebook_toolbar">
33 <span id="drag_info">Drag files onto the list to import
34
34 notebooks.</span>
35 <form id='alternate_upload' action="notebooks" enctype="multipart/form-data" method="post" class='alternate_upload'>
36 <span id="drag_info" style="position:absolute" >Drag files
37 onto the list, or click here, to import
38 notebooks.</span>
39 <input type="file" name="datafile" class="fileinput">
40 </form>
41
35
42
36 <span id="notebook_buttons">
43 <span id="notebook_buttons">
37 <button id="refresh_notebook_list" title="Refresh notebook list">Refresh</button>
44 <button id="refresh_notebook_list" title="Refresh notebook list">Refresh</button>
38 <button id="new_notebook" title="Create new notebook">New Notebook</button>
45 <button id="new_notebook" title="Create new notebook">New Notebook</button>
39 </span>
46 </span>
40 </div>
47 </div>
41 {% end %}
48 {% end %}
42
49
43 <div id="notebook_list">
50 <div id="notebook_list">
44 <div id="project_name"><h2>{{project}}</h2></div>
51 <div id="project_name"><h2>{{project}}</h2></div>
45 </div>
52 </div>
46 </div>
53 </div>
47 <div id="tab2">
54 <div id="tab2">
48
55
49 <div id="cluster_toolbar">
56 <div id="cluster_toolbar">
50 <span id="cluster_list_info">IPython parallel computing clusters</span>
57 <span id="cluster_list_info">IPython parallel computing clusters</span>
51
58
52 <span id="cluster_buttons">
59 <span id="cluster_buttons">
53 <button id="refresh_cluster_list" title="Refresh cluster list">Refresh</button>
60 <button id="refresh_cluster_list" title="Refresh cluster list">Refresh</button>
54 </span>
61 </span>
55 </div>
62 </div>
56
63
57 <div id="cluster_list">
64 <div id="cluster_list">
58 <div id="cluster_header">
65 <div id="cluster_header">
59 <span>profile</span>
66 <span>profile</span>
60 <span>action</span>
67 <span>action</span>
61 <span title="Enter the number of engines to start or empty for default"># of engines</span>
68 <span title="Enter the number of engines to start or empty for default"># of engines</span>
62 <span>status</span>
69 <span>status</span>
63 </div>
70 </div>
64 </div>
71 </div>
65
72
66 </div>
73 </div>
67 </div>
74 </div>
68
75
69 </div>
76 </div>
70
77
71 {% end %}
78 {% end %}
72
79
73 {% block script %}
80 {% block script %}
74 <script src="{{static_url("js/notebooklist.js") }}" type="text/javascript" charset="utf-8"></script>
81 <script src="{{static_url("js/notebooklist.js") }}" type="text/javascript" charset="utf-8"></script>
75 <script src="{{static_url("js/clusterlist.js") }}" type="text/javascript" charset="utf-8"></script>
82 <script src="{{static_url("js/clusterlist.js") }}" type="text/javascript" charset="utf-8"></script>
76 <script src="{{static_url("js/projectdashboardmain.js") }}" type="text/javascript" charset="utf-8"></script>
83 <script src="{{static_url("js/projectdashboardmain.js") }}" type="text/javascript" charset="utf-8"></script>
77 {% end %}
84 {% end %}
General Comments 0
You need to be logged in to leave comments. Login now