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 %]
11 <body id="tools_quotes" class="tools">
12 [% INCLUDE 'header.inc' %]
13 [% INCLUDE 'cat-search.inc' %]
15 <nav aria-label="Breadcrumb" class="breadcrumb">
18 <a href="/cgi-bin/koha/mainpage.pl">Home</a>
21 <a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a>
24 <a href="/cgi-bin/koha/tools/quotes.pl">Quote editor</a>
27 <a href="#" aria-current="page">
34 <div class="main container-fluid">
36 <div class="col-sm-10 col-sm-push-2">
38 <div id="toolbar" class="btn-toolbar" style="visibility: hidden; position: absolute">
39 <div class="btn-group"><a class="btn btn-default" id="save_quotes" href="#"><i class="fa fa-save"></i> Save quotes</a></div>
40 <div class="btn-group"><a href="/cgi-bin/koha/tools/quotes-upload.pl" id="cancel_quotes" class="btn btn-default"><i class="fa fa-remove"></i> Cancel import</a></div>
43 <h2>Quote uploader</h2>
45 <div id="messages" style="display: none;">
46 <div class="import_success dialog message" style="display: none;"></div>
47 <div class="import_errors dialog alert" style="display: none;"></div>
50 <div id="csv_error" class="alert" style="visibility: hidden;">Something went wrong, check your CSV file.</div>
51 <div id="instructions">
52 <fieldset id="file_uploader_help" class="rows">
53 <legend>Instructions</legend>
54 <div id="file_uploader_inst">
56 <li>The quote uploader accepts standard csv files with two columns: "source","text"</li>
57 <li>Click the "Choose File" button and select the csv file to be uploaded.</li>
58 <li>The file will be imported into an editable table for review prior to saving.</li>
61 <div id="file_editor_inst">
63 <li>Click on any field to edit the contents; Press the <Enter> key to save edit.</li>
64 <li>Click the 'Save Quotes' button in the toolbar to save the entire batch of quotes.</li>
70 <fieldset id="file_uploader" class="rows">
71 <legend>Upload quotes</legend>
72 <div id="file_upload">
73 <input type="file" name="file" />
74 <button id="cancel_upload" style="display:none">Cancel upload</button>
75 <div id="progress_bar"><div class="percent">0%</div></div>
78 <table id="quotes_editor" style="visibility: hidden;">
83 <th class="noExport">Actions</th>
87 <!-- tbody content is generated by DataTables -->
90 <td>Loading data...</td>
95 <fieldset id="footer" class="action" style="visibility: hidden;">
99 </div> <!-- /.col-sm-10.col-sm-push-2 -->
101 <div class="col-sm-2 col-sm-pull-10">
103 [% INCLUDE 'tools-menu.inc' %]
105 </div> <!-- /.col-sm-2.col-sm-pull-10 -->
106 </div> <!-- /.row -->
108 [% MACRO jsinclude BLOCK %]
109 [% Asset.js("js/tools-menu.js") | $raw %]
110 [% INCLUDE 'datatables.inc' %]
111 [% Asset.js("lib/jquery/plugins/jquery.jeditable.mini.js") | $raw %]
113 var oTable; //DataTable object
114 $(document).ready(function() {
116 $("#cancel_upload").on("click",function(e){
120 $("#cancel_quotes").on("click",function(){
121 return confirm( _("Are you sure you want to cancel this import?") );
125 // FileReader() code copied and hacked from:
126 // http://www.html5rocks.com/en/tutorials/file/dndfiles/
127 // fnCSVToArray() gratefully borrowed from:
128 // http://www.bennadel.com/blog/1504-Ask-Ben-Parsing-CSV-Strings-With-Javascript-Exec-Regular-Expression-Command.htm
131 var progress = document.querySelector('.percent');
132 $("#server_response").hide();
134 function yuiGetData() {
135 fnGetData(document.getElementById('quotes_editor'));
138 function fnAbortRead() {
142 function fnErrorHandler(evt) {
143 switch(evt.target.error.code) {
144 case evt.target.error.NOT_FOUND_ERR:
145 alert(_("File Not Found!"));
147 case evt.target.error.NOT_READABLE_ERR:
148 alert(_("File is not readable"));
150 case evt.target.error.ABORT_ERR:
153 alert(_("An error occurred reading this file."));
157 function fnUpdateProgress(evt) {
158 // evt is an ProgressEvent.
159 if (evt.lengthComputable) {
160 var percentLoaded = Math.round((evt.loaded / evt.total) * 100);
161 // Increase the progress bar length.
162 if (percentLoaded < 100) {
163 progress.style.width = percentLoaded + '%';
164 progress.textContent = percentLoaded + '%';
169 function fnCSVToArray( strData, strDelimiter ){
170 // This will parse a delimited string into an array of
171 // arrays. The default delimiter is the comma, but this
172 // can be overriden in the second argument.
174 // Check to see if the delimiter is defined. If not,
175 // then default to comma.
176 strDelimiter = (strDelimiter || ",");
178 // Create a regular expression to parse the CSV values.
179 var objPattern = new RegExp(
182 "(\\" + strDelimiter + "|\\r?\\n|\\r|^)" +
184 "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" +
186 "([^\"\\" + strDelimiter + "\\r\\n]*))"
191 // Create an array to hold our data. Give the array
192 // a default empty first row.
195 // Create an array to hold our individual pattern
197 var arrMatches = null;
199 // Keep looping over the regular expression matches
200 // until we can no longer find a match.
201 while (arrMatches = objPattern.exec( strData )){
203 // Get the delimiter that was found.
204 var strMatchedDelimiter = arrMatches[ 1 ];
206 // Check to see if the given delimiter has a length
207 // (is not the start of string) and if it matches
208 // field delimiter. If it does not, then we know
209 // that this delimiter is a row delimiter.
210 if ( strMatchedDelimiter.length && (strMatchedDelimiter != strDelimiter) ){
211 // Since we have reached a new row of data,
212 // add an empty row to our data array.
213 // Note: if there is not more data, we will have to remove this row later
217 // Now that we have our delimiter out of the way,
218 // let's check to see which kind of value we
219 // captured (quoted or unquoted).
220 if (arrMatches[ 2 ]){
221 // We found a quoted value. When we capture
222 // this value, unescape any double quotes.
223 var strMatchedValue = arrMatches[ 2 ].replace(
224 new RegExp( "\"\"", "g" ),
227 } else if (arrMatches[3]){
228 // We found a non-quoted value.
229 var strMatchedValue = arrMatches[ 3 ];
231 // There is no more valid data so remove the row we added earlier
232 // Is there a better way? Perhaps a look-ahead regexp?
233 arrData.splice(arrData.length-1, 1);
236 // Now that we have our value string, let's add
237 // it to the data array.
238 if ( arrData[ arrData.length - 1 ] ) {
239 arrData[ arrData.length - 1 ].push( strMatchedValue );
241 $('#csv_error').css( 'visibility' , 'visible' );
245 // Return the parsed data.
249 function fnDataTable(aaData) {
250 for(var i=0; i<aaData.length; i++) {
251 aaData[i].unshift(i+1); // Add a column w/quote number
254 /* Transition from the quote file uploader to the quote file editor interface */
255 $('#toolbar').css("visibility","visible");
256 $('#toolbar').css("position","");
257 $('#file_editor_inst').css("visibility", "visible");
258 $('#file_editor_inst').css("position", "");
259 $('#file_uploader_inst').css("visibility", "hidden");
260 $('#save_quotes').css("visibility","visible");
261 $('#file_uploader').css("visibility","hidden");
262 $('#file_uploader').css("position","absolute");
263 $('#file_uploader').css("top","-150px");
264 $('#quotes_editor').css("visibility","visible");
265 $("#save_quotes").on("click", yuiGetData);
267 oTable = $('#quotes_editor').dataTable( {
268 "bAutoWidth" : false,
271 "sPaginationType" : "full_numbers",
272 "sDom": '<"top pager"iflp>rt<"bottom pager"flp><"clear">',
276 "sTitle" : _("Number"),
280 "sTitle" : _("Source"),
284 "sTitle" : _("Quote"),
288 "fnPreDrawCallback": function(oSettings) {
291 "fnRowCallback": function( nRow, aData, iDisplayIndex ) {
292 /* do foo on various cells in the current row */
293 var quoteNum = $('td', nRow)[0].innerHTML;
294 $(nRow).attr("id", quoteNum); /* set row ids to quote number */
295 /* apply no_edit id to noEditFields */
296 noEditFields = [0]; /* number */
297 for (i=0; i<noEditFields.length; i++) {
298 $('td', nRow)[noEditFields[i]].setAttribute("id","no_edit");
302 "fnDrawCallback": function(oSettings) {
303 /* Apply the jEditable handlers to the table on all fields w/o the no_edit id */
304 $('#quotes_editor tbody td[id!="no_edit"]').editable( function(value, settings) {
305 var cellPosition = oTable.fnGetPosition( this );
306 oTable.fnUpdate(value, cellPosition[0], cellPosition[1], false, false);
310 "callback" : function( sValue, y ) {
311 oTable.fnDraw(false); /* no filter/sort or we lose our pagination */
317 $('#footer').css("visibility","visible");
320 function fnHandleFileSelect(evt) {
321 // Reset progress indicator on new file selection.
322 progress.style.width = '0%';
323 progress.textContent = '0%';
325 reader = new FileReader();
326 reader.onerror = fnErrorHandler;
327 reader.onprogress = fnUpdateProgress;
328 reader.onabort = function(e) {
329 alert(_("File read cancelled"));
330 parent.location='quotes-upload.pl';
332 reader.onloadstart = function(e) {
333 $('#cancel_upload').show();
334 $('#progress_bar').addClass("loading");
336 reader.onload = function(e) {
337 // Ensure that the progress bar displays 100% at the end.
338 progress.style.width = '100%';
339 progress.textContent = '100%';
340 $('#cancel_upload').hide();
341 quotes = fnCSVToArray(e.target.result, ',');
345 // perform various sanity checks on the target file prior to uploading...
346 var fileType = evt.target.files[0].type || 'unknown';
347 var fileSizeInK = Math.round(evt.target.files[0].size/1024);
349 if (!fileType.match(/comma-separated-values|csv|excel|calc/i)) {
350 alert(_("Uploads limited to csv. Incorrect filetype: %s").format(fileType));
351 parent.location='quotes-upload.pl';
354 if (fileSizeInK > 512) {
355 if (!confirm(_("%s %s KB Do you really want to upload this file?").format(evt.target.files[0].name, fileSizeInK))) {
356 parent.location='quotes-upload.pl';
360 // Read in the image file as a text string.
361 reader.readAsText(evt.target.files[0]);
364 $('#file_upload').one('change', fnHandleFileSelect);
366 var MSG_IMPORT_SUCCESS = _("%s quotes imported successfully");
367 var MSG_IMPORT_ERROR = _("%s quotes have not been imported. An error occurred");
368 function fnGetData(element) {
369 var lines = oTable.fnGetData();
370 $(lines).each(function(line){
371 var data = {source: this[1], text: this[2]};
372 var success = 0; var error = 0;
374 url : "/api/v1/quotes",
376 data : JSON.stringify(data),
377 dataType : "application/json",
378 success : function(data) {
379 $("#messages").show();
380 var import_success = $("#messages .import_success");
381 import_success.show();
382 import_success.data("nb")
383 nb_success = import_success.data("nb") || 0;
385 $("#messages .import_success").text(MSG_IMPORT_SUCCESS.format(nb_success));
386 import_success.data("nb", nb_success);
388 error : function(xhr) {
389 if (xhr.status==201) { this.success(null, "Created", xhr); return; }
390 $("#messages").show();
391 var import_error = $("#messages .import_error");
393 import_error.data("nb")
394 nb_error = import_error.data("nb") || 0;
396 $("#messages .import_error").text(MSG_IMPORT_ERROR.format(nb_error));
397 import_error.data("nb", nb_error);
403 }); // $(document).ready
407 [% INCLUDE 'intranet-bottom.inc' %]