Lines 1-379
Link Here
|
1 |
[% USE raw %] |
|
|
2 |
[% USE Asset %] |
3 |
[% SET footerjs = 1 %] |
4 |
[% INCLUDE 'doc-head-open.inc' %] |
5 |
<title>Koha › Tools › Quote uploader</title> |
6 |
[% INCLUDE 'doc-head-close.inc' %] |
7 |
[% Asset.css("css/uploader.css") | $raw %] |
8 |
[% Asset.css("css/quotes.css") | $raw %] |
9 |
</head> |
10 |
|
11 |
<body id="tools_quotes" class="tools"> |
12 |
[% INCLUDE 'header.inc' %] |
13 |
[% INCLUDE 'cat-search.inc' %] |
14 |
|
15 |
<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> › <a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a> › <a href="/cgi-bin/koha/tools/quotes.pl">Quote editor</a> › Quote uploader</div> |
16 |
|
17 |
<div class="main container-fluid"> |
18 |
<div class="row"> |
19 |
<div class="col-sm-10 col-sm-push-2"> |
20 |
<main> |
21 |
|
22 |
[% INCLUDE 'quotes-upload-toolbar.inc' %] |
23 |
<h2>Quote uploader</h2> |
24 |
<div id="instructions"> |
25 |
<fieldset id="file_uploader_help" class="rows"> |
26 |
<legend>Instructions</legend> |
27 |
<div id="file_uploader_inst"> |
28 |
<ul> |
29 |
<li>The quote uploader accepts standard csv files with two columns: "source","text"</li> |
30 |
<li>Click the "Choose File" button and select the csv file to be uploaded.</li> |
31 |
<li>The file will be imported into an editable table for review prior to saving.</li> |
32 |
</ul> |
33 |
</div> |
34 |
<div id="file_editor_inst"> |
35 |
<ul> |
36 |
<li>Click on any field to edit the contents; Press the <Enter> key to save edit.</li> |
37 |
<li>Click on one or more quote numbers to select entire quotes for deletion; Click the 'Delete Quote(s)' button to delete selected quotes.</li> |
38 |
<li>Click the 'Save Quotes' button in the toolbar to save the entire batch of quotes.</li> |
39 |
</ul> |
40 |
</div> |
41 |
</fieldset> |
42 |
</div> |
43 |
<fieldset id="file_uploader" class="rows"> |
44 |
<legend>Upload quotes</legend> |
45 |
<div id="file_upload"> |
46 |
<input type="file" name="file" /> |
47 |
<button id="cancel_upload" style="display:none">Cancel upload</button> |
48 |
<div id="progress_bar"><div class="percent">0%</div></div> |
49 |
</div> |
50 |
</fieldset> |
51 |
<table id="quotes_editor" style="visibility: hidden;"> |
52 |
<thead> |
53 |
<tr> |
54 |
<th>Source</th> |
55 |
<th>Text</th> |
56 |
<th>Actions</th> |
57 |
</tr> |
58 |
</thead> |
59 |
<tbody> |
60 |
<!-- tbody content is generated by DataTables --> |
61 |
<tr> |
62 |
<td></td> |
63 |
<td>Loading data...</td> |
64 |
<td></td> |
65 |
</tr> |
66 |
</tbody> |
67 |
</table> |
68 |
<fieldset id="footer" class="action" style="visibility: hidden;"> |
69 |
</fieldset> |
70 |
|
71 |
</main> |
72 |
</div> <!-- /.col-sm-10.col-sm-push-2 --> |
73 |
|
74 |
<div class="col-sm-2 col-sm-pull-10"> |
75 |
<aside> |
76 |
[% INCLUDE 'tools-menu.inc' %] |
77 |
</aside> |
78 |
</div> <!-- /.col-sm-2.col-sm-pull-10 --> |
79 |
</div> <!-- /.row --> |
80 |
|
81 |
[% MACRO jsinclude BLOCK %] |
82 |
[% Asset.js("js/tools-menu.js") | $raw %] |
83 |
[% INCLUDE 'datatables.inc' %] |
84 |
[% Asset.js("lib/jquery/plugins/jquery.jeditable.mini.js") | $raw %] |
85 |
<script> |
86 |
var oTable; //DataTable object |
87 |
$(document).ready(function() { |
88 |
|
89 |
$("#cancel_upload").on("click",function(e){ |
90 |
e.preventDefault(); |
91 |
fnAbortRead(); |
92 |
}); |
93 |
$("#cancel_quotes").on("click",function(){ |
94 |
return confirm( _("Are you sure you want to cancel this import?") ); |
95 |
}); |
96 |
|
97 |
// Credits: |
98 |
// FileReader() code copied and hacked from: |
99 |
// http://www.html5rocks.com/en/tutorials/file/dndfiles/ |
100 |
// fnCSVToArray() gratefully borrowed from: |
101 |
// http://www.bennadel.com/blog/1504-Ask-Ben-Parsing-CSV-Strings-With-Javascript-Exec-Regular-Expression-Command.htm |
102 |
|
103 |
var reader; |
104 |
var progress = document.querySelector('.percent'); |
105 |
$("#server_response").hide(); |
106 |
|
107 |
function yuiGetData() { |
108 |
fnGetData(document.getElementById('quotes_editor')); |
109 |
} |
110 |
|
111 |
function fnAbortRead() { |
112 |
reader.abort(); |
113 |
} |
114 |
|
115 |
function fnErrorHandler(evt) { |
116 |
switch(evt.target.error.code) { |
117 |
case evt.target.error.NOT_FOUND_ERR: |
118 |
alert(_("File Not Found!")); |
119 |
break; |
120 |
case evt.target.error.NOT_READABLE_ERR: |
121 |
alert(_("File is not readable")); |
122 |
break; |
123 |
case evt.target.error.ABORT_ERR: |
124 |
break; // noop |
125 |
default: |
126 |
alert(_("An error occurred reading this file.")); |
127 |
}; |
128 |
} |
129 |
|
130 |
function fnUpdateProgress(evt) { |
131 |
// evt is an ProgressEvent. |
132 |
if (evt.lengthComputable) { |
133 |
var percentLoaded = Math.round((evt.loaded / evt.total) * 100); |
134 |
// Increase the progress bar length. |
135 |
if (percentLoaded < 100) { |
136 |
progress.style.width = percentLoaded + '%'; |
137 |
progress.textContent = percentLoaded + '%'; |
138 |
} |
139 |
} |
140 |
} |
141 |
|
142 |
function fnCSVToArray( strData, strDelimiter ){ |
143 |
// This will parse a delimited string into an array of |
144 |
// arrays. The default delimiter is the comma, but this |
145 |
// can be overriden in the second argument. |
146 |
|
147 |
// Check to see if the delimiter is defined. If not, |
148 |
// then default to comma. |
149 |
strDelimiter = (strDelimiter || ","); |
150 |
|
151 |
// Create a regular expression to parse the CSV values. |
152 |
var objPattern = new RegExp( |
153 |
( |
154 |
// Delimiters. |
155 |
"(\\" + strDelimiter + "|\\r?\\n|\\r|^)" + |
156 |
// Quoted fields. |
157 |
"(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" + |
158 |
// Standard fields. |
159 |
"([^\"\\" + strDelimiter + "\\r\\n]*))" |
160 |
), |
161 |
"gi" |
162 |
); |
163 |
|
164 |
// Create an array to hold our data. Give the array |
165 |
// a default empty first row. |
166 |
var arrData = [[]]; |
167 |
|
168 |
// Create an array to hold our individual pattern |
169 |
// matching groups. |
170 |
var arrMatches = null; |
171 |
|
172 |
// Keep looping over the regular expression matches |
173 |
// until we can no longer find a match. |
174 |
while (arrMatches = objPattern.exec( strData )){ |
175 |
|
176 |
// Get the delimiter that was found. |
177 |
var strMatchedDelimiter = arrMatches[ 1 ]; |
178 |
|
179 |
// Check to see if the given delimiter has a length |
180 |
// (is not the start of string) and if it matches |
181 |
// field delimiter. If it does not, then we know |
182 |
// that this delimiter is a row delimiter. |
183 |
if ( strMatchedDelimiter.length && (strMatchedDelimiter != strDelimiter) ){ |
184 |
// Since we have reached a new row of data, |
185 |
// add an empty row to our data array. |
186 |
// Note: if there is not more data, we will have to remove this row later |
187 |
arrData.push( [] ); |
188 |
} |
189 |
|
190 |
// Now that we have our delimiter out of the way, |
191 |
// let's check to see which kind of value we |
192 |
// captured (quoted or unquoted). |
193 |
if (arrMatches[ 2 ]){ |
194 |
// We found a quoted value. When we capture |
195 |
// this value, unescape any double quotes. |
196 |
var strMatchedValue = arrMatches[ 2 ].replace( |
197 |
new RegExp( "\"\"", "g" ), |
198 |
"\"" |
199 |
); |
200 |
} else if (arrMatches[3]){ |
201 |
// We found a non-quoted value. |
202 |
var strMatchedValue = arrMatches[ 3 ]; |
203 |
} else { |
204 |
// There is no more valid data so remove the row we added earlier |
205 |
// Is there a better way? Perhaps a look-ahead regexp? |
206 |
arrData.splice(arrData.length-1, 1); |
207 |
} |
208 |
|
209 |
// Now that we have our value string, let's add |
210 |
// it to the data array. |
211 |
arrData[ arrData.length - 1 ].push( strMatchedValue ); |
212 |
} |
213 |
|
214 |
// Return the parsed data. |
215 |
return( arrData ); |
216 |
} |
217 |
|
218 |
function fnDataTable(aaData) { |
219 |
for(var i=0; i<aaData.length; i++) { |
220 |
aaData[i].unshift(i+1); // Add a column w/quote number |
221 |
} |
222 |
|
223 |
/* Transition from the quote file uploader to the quote file editor interface */ |
224 |
$('#toolbar').css("visibility","visible"); |
225 |
$('#toolbar').css("position",""); |
226 |
$('#file_editor_inst').css("visibility", "visible"); |
227 |
$('#file_editor_inst').css("position", ""); |
228 |
$('#file_uploader_inst').css("visibility", "hidden"); |
229 |
$('#save_quotes').css("visibility","visible"); |
230 |
$('#file_uploader').css("visibility","hidden"); |
231 |
$('#file_uploader').css("position","absolute"); |
232 |
$('#file_uploader').css("top","-150px"); |
233 |
$('#quotes_editor').css("visibility","visible"); |
234 |
$("#save_quotes").on("click", yuiGetData); |
235 |
$("#delete_quote").on("click", fnClickDeleteRow); |
236 |
|
237 |
oTable = $('#quotes_editor').dataTable( { |
238 |
"bAutoWidth" : false, |
239 |
"bPaginate" : true, |
240 |
"bSort" : false, |
241 |
"sPaginationType" : "full_numbers", |
242 |
"sDom": '<"top pager"iflp>rt<"bottom pager"flp><"clear">', |
243 |
"aaData" : aaData, |
244 |
"aoColumns" : [ |
245 |
{ |
246 |
"sTitle" : _("Number"), |
247 |
"sWidth" : "2%", |
248 |
}, |
249 |
{ |
250 |
"sTitle" : _("Source"), |
251 |
"sWidth" : "15%", |
252 |
}, |
253 |
{ |
254 |
"sTitle" : _("Quote"), |
255 |
"sWidth" : "83%", |
256 |
}, |
257 |
], |
258 |
"fnPreDrawCallback": function(oSettings) { |
259 |
return true; |
260 |
}, |
261 |
"fnRowCallback": function( nRow, aData, iDisplayIndex ) { |
262 |
/* do foo on various cells in the current row */ |
263 |
var quoteNum = $('td', nRow)[0].innerHTML; |
264 |
$(nRow).attr("id", quoteNum); /* set row ids to quote number */ |
265 |
$('td:eq(0)', nRow).click(function() {$(this.parentNode).toggleClass('selected',this.clicked);}); /* add row selectors */ |
266 |
$('td:eq(0)', nRow).attr("title", _("Click ID to select/deselect quote")); |
267 |
/* apply no_edit id to noEditFields */ |
268 |
noEditFields = [0]; /* number */ |
269 |
for (i=0; i<noEditFields.length; i++) { |
270 |
$('td', nRow)[noEditFields[i]].setAttribute("id","no_edit"); |
271 |
} |
272 |
return nRow; |
273 |
}, |
274 |
"fnDrawCallback": function(oSettings) { |
275 |
/* Apply the jEditable handlers to the table on all fields w/o the no_edit id */ |
276 |
$('#quotes_editor tbody td[id!="no_edit"]').editable( function(value, settings) { |
277 |
var cellPosition = oTable.fnGetPosition( this ); |
278 |
oTable.fnUpdate(value, cellPosition[0], cellPosition[1], false, false); |
279 |
return(value); |
280 |
}, |
281 |
{ |
282 |
"callback" : function( sValue, y ) { |
283 |
oTable.fnDraw(false); /* no filter/sort or we lose our pagination */ |
284 |
}, |
285 |
"height" : "14px", |
286 |
}); |
287 |
}, |
288 |
}); |
289 |
$('#footer').css("visibility","visible"); |
290 |
} |
291 |
|
292 |
function fnHandleFileSelect(evt) { |
293 |
// Reset progress indicator on new file selection. |
294 |
progress.style.width = '0%'; |
295 |
progress.textContent = '0%'; |
296 |
|
297 |
reader = new FileReader(); |
298 |
reader.onerror = fnErrorHandler; |
299 |
reader.onprogress = fnUpdateProgress; |
300 |
reader.onabort = function(e) { |
301 |
alert(_("File read cancelled")); |
302 |
parent.location='quotes-upload.pl'; |
303 |
}; |
304 |
reader.onloadstart = function(e) { |
305 |
$('#cancel_upload').show(); |
306 |
$('#progress_bar').addClass("loading"); |
307 |
}; |
308 |
reader.onload = function(e) { |
309 |
// Ensure that the progress bar displays 100% at the end. |
310 |
progress.style.width = '100%'; |
311 |
progress.textContent = '100%'; |
312 |
$('#cancel_upload').hide(); |
313 |
quotes = fnCSVToArray(e.target.result, ','); |
314 |
fnDataTable(quotes); |
315 |
} |
316 |
|
317 |
// perform various sanity checks on the target file prior to uploading... |
318 |
var fileType = evt.target.files[0].type || 'unknown'; |
319 |
var fileSizeInK = Math.round(evt.target.files[0].size/1024); |
320 |
|
321 |
if (!fileType.match(/comma-separated-values|csv|excel|calc/i)) { |
322 |
alert(_("Uploads limited to csv. Incorrect filetype: %s").format(fileType)); |
323 |
parent.location='quotes-upload.pl'; |
324 |
return; |
325 |
} |
326 |
if (fileSizeInK > 512) { |
327 |
if (!confirm(_("%s %s KB Do you really want to upload this file?").format(evt.target.files[0].name, fileSizeInK))) { |
328 |
parent.location='quotes-upload.pl'; |
329 |
return; |
330 |
} |
331 |
} |
332 |
// Read in the image file as a text string. |
333 |
reader.readAsText(evt.target.files[0]); |
334 |
} |
335 |
|
336 |
$('#file_upload').one('change', fnHandleFileSelect); |
337 |
|
338 |
}); |
339 |
|
340 |
function fnGetData(element) { |
341 |
var jqXHR = $.ajax({ |
342 |
url : "/cgi-bin/koha/tools/quotes/quotes-upload_ajax.pl", |
343 |
type : "POST", |
344 |
contentType : "application/x-www-form-urlencoded", // we must claim this mimetype or CGI will not decode the URL encoding |
345 |
dataType : "json", |
346 |
data : { |
347 |
"quote" : encodeURI ( JSON.stringify(oTable.fnGetData()) ), |
348 |
"action" : "add", |
349 |
}, |
350 |
success : function(){ |
351 |
var response = JSON.parse(jqXHR.responseText); |
352 |
if (response.success) { |
353 |
alert(_("%s quotes saved.").format(response.records)); |
354 |
window.location.reload(true); // is this the best route? |
355 |
} else { |
356 |
alert(_("%s quotes saved, but an error has occurred. Please ask your administrator to check the server log for more details.").format(response.records)); |
357 |
window.location.reload(true); // is this the best route? |
358 |
} |
359 |
}, |
360 |
}); |
361 |
} |
362 |
|
363 |
function fnClickDeleteRow() { |
364 |
var idsToDelete = oTable.$('.selected').map(function() { |
365 |
return this.id; |
366 |
}).get().join(', '); |
367 |
if (!idsToDelete) { |
368 |
alert(_("Please select a quote(s) by clicking the quote id(s) you desire to delete.")); |
369 |
} |
370 |
else if (confirm(_("Are you sure you wish to delete quote(s) %s?").format(idsToDelete))) { |
371 |
oTable.$('.selected').each(function(){ |
372 |
oTable.fnDeleteRow(this); |
373 |
}); |
374 |
} |
375 |
} |
376 |
</script> |
377 |
[% END %] |
378 |
|
379 |
[% INCLUDE 'intranet-bottom.inc' %] |