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 [% Asset.css("css/datatables.css") | $raw %]
12 <body id="tools_quotes" class="tools">
13 [% INCLUDE 'header.inc' %]
14 [% INCLUDE 'cat-search.inc' %]
16 <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>
18 <div class="main container-fluid">
20 <div class="col-sm-10 col-sm-push-2">
23 [% INCLUDE 'quotes-upload-toolbar.inc' %]
24 <h2>Quote uploader</h2>
25 <div id="instructions">
26 <fieldset id="file_uploader_help" class="rows">
27 <legend>Instructions</legend>
28 <div id="file_uploader_inst">
30 <li>The quote uploader accepts standard csv files with two columns: "source","text"</li>
31 <li>Click the "Choose File" button and select the csv file to be uploaded.</li>
32 <li>The file will be imported into an editable table for review prior to saving.</li>
35 <div id="file_editor_inst">
37 <li>Click on any field to edit the contents; Press the <Enter> key to save edit.</li>
38 <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>
39 <li>Click the 'Save Quotes' button in the toolbar to save the entire batch of quotes.</li>
44 <fieldset id="file_uploader" class="rows">
45 <legend>Upload quotes</legend>
46 <div id="file_upload">
47 <input type="file" name="file" />
48 <button id="cancel_upload" style="display:none">Cancel upload</button>
49 <div id="progress_bar"><div class="percent">0%</div></div>
52 <table id="quotes_editor" style="visibility: hidden;">
61 <!-- tbody content is generated by DataTables -->
64 <td>Loading data...</td>
69 <fieldset id="footer" class="action" style="visibility: hidden;">
73 </div> <!-- /.col-sm-10.col-sm-push-2 -->
75 <div class="col-sm-2 col-sm-pull-10">
77 [% INCLUDE 'tools-menu.inc' %]
79 </div> <!-- /.col-sm-2.col-sm-pull-10 -->
82 [% MACRO jsinclude BLOCK %]
83 [% Asset.js("js/tools-menu.js") | $raw %]
84 [% INCLUDE 'datatables.inc' %]
85 [% Asset.js("lib/jquery/plugins/jquery.jeditable.mini.js") | $raw %]
87 var oTable; //DataTable object
88 $(document).ready(function() {
90 $("#cancel_upload").on("click",function(e){
94 $("#cancel_quotes").on("click",function(){
95 return confirm( _("Are you sure you want to cancel this import?") );
99 // FileReader() code copied and hacked from:
100 // http://www.html5rocks.com/en/tutorials/file/dndfiles/
101 // fnCSVToArray() gratefully borrowed from:
102 // http://www.bennadel.com/blog/1504-Ask-Ben-Parsing-CSV-Strings-With-Javascript-Exec-Regular-Expression-Command.htm
105 var progress = document.querySelector('.percent');
106 $("#server_response").hide();
108 function yuiGetData() {
109 fnGetData(document.getElementById('quotes_editor'));
112 function fnAbortRead() {
116 function fnErrorHandler(evt) {
117 switch(evt.target.error.code) {
118 case evt.target.error.NOT_FOUND_ERR:
119 alert(_("File Not Found!"));
121 case evt.target.error.NOT_READABLE_ERR:
122 alert(_("File is not readable"));
124 case evt.target.error.ABORT_ERR:
127 alert(_("An error occurred reading this file."));
131 function fnUpdateProgress(evt) {
132 // evt is an ProgressEvent.
133 if (evt.lengthComputable) {
134 var percentLoaded = Math.round((evt.loaded / evt.total) * 100);
135 // Increase the progress bar length.
136 if (percentLoaded < 100) {
137 progress.style.width = percentLoaded + '%';
138 progress.textContent = percentLoaded + '%';
143 function fnCSVToArray( strData, strDelimiter ){
144 // This will parse a delimited string into an array of
145 // arrays. The default delimiter is the comma, but this
146 // can be overriden in the second argument.
148 // Check to see if the delimiter is defined. If not,
149 // then default to comma.
150 strDelimiter = (strDelimiter || ",");
152 // Create a regular expression to parse the CSV values.
153 var objPattern = new RegExp(
156 "(\\" + strDelimiter + "|\\r?\\n|\\r|^)" +
158 "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" +
160 "([^\"\\" + strDelimiter + "\\r\\n]*))"
165 // Create an array to hold our data. Give the array
166 // a default empty first row.
169 // Create an array to hold our individual pattern
171 var arrMatches = null;
173 // Keep looping over the regular expression matches
174 // until we can no longer find a match.
175 while (arrMatches = objPattern.exec( strData )){
177 // Get the delimiter that was found.
178 var strMatchedDelimiter = arrMatches[ 1 ];
180 // Check to see if the given delimiter has a length
181 // (is not the start of string) and if it matches
182 // field delimiter. If it does not, then we know
183 // that this delimiter is a row delimiter.
184 if ( strMatchedDelimiter.length && (strMatchedDelimiter != strDelimiter) ){
185 // Since we have reached a new row of data,
186 // add an empty row to our data array.
187 // Note: if there is not more data, we will have to remove this row later
191 // Now that we have our delimiter out of the way,
192 // let's check to see which kind of value we
193 // captured (quoted or unquoted).
194 if (arrMatches[ 2 ]){
195 // We found a quoted value. When we capture
196 // this value, unescape any double quotes.
197 var strMatchedValue = arrMatches[ 2 ].replace(
198 new RegExp( "\"\"", "g" ),
201 } else if (arrMatches[3]){
202 // We found a non-quoted value.
203 var strMatchedValue = arrMatches[ 3 ];
205 // There is no more valid data so remove the row we added earlier
206 // Is there a better way? Perhaps a look-ahead regexp?
207 arrData.splice(arrData.length-1, 1);
210 // Now that we have our value string, let's add
211 // it to the data array.
212 arrData[ arrData.length - 1 ].push( strMatchedValue );
215 // Return the parsed data.
219 function fnDataTable(aaData) {
220 for(var i=0; i<aaData.length; i++) {
221 aaData[i].unshift(i+1); // Add a column w/quote number
224 /* Transition from the quote file uploader to the quote file editor interface */
225 $('#toolbar').css("visibility","visible");
226 $('#toolbar').css("position","");
227 $('#file_editor_inst').css("visibility", "visible");
228 $('#file_editor_inst').css("position", "");
229 $('#file_uploader_inst').css("visibility", "hidden");
230 $('#save_quotes').css("visibility","visible");
231 $('#file_uploader').css("visibility","hidden");
232 $('#file_uploader').css("position","absolute");
233 $('#file_uploader').css("top","-150px");
234 $('#quotes_editor').css("visibility","visible");
235 $("#save_quotes").on("click", yuiGetData);
236 $("#delete_quote").on("click", fnClickDeleteRow);
238 oTable = $('#quotes_editor').dataTable( {
239 "bAutoWidth" : false,
242 "sPaginationType" : "full_numbers",
243 "sDom": '<"top pager"iflp>rt<"bottom pager"flp><"clear">',
247 "sTitle" : _("Number"),
251 "sTitle" : _("Source"),
255 "sTitle" : _("Quote"),
259 "fnPreDrawCallback": function(oSettings) {
262 "fnRowCallback": function( nRow, aData, iDisplayIndex ) {
263 /* do foo on various cells in the current row */
264 var quoteNum = $('td', nRow)[0].innerHTML;
265 $(nRow).attr("id", quoteNum); /* set row ids to quote number */
266 $('td:eq(0)', nRow).click(function() {$(this.parentNode).toggleClass('selected',this.clicked);}); /* add row selectors */
267 $('td:eq(0)', nRow).attr("title", _("Click ID to select/deselect quote"));
268 /* apply no_edit id to noEditFields */
269 noEditFields = [0]; /* number */
270 for (i=0; i<noEditFields.length; i++) {
271 $('td', nRow)[noEditFields[i]].setAttribute("id","no_edit");
275 "fnDrawCallback": function(oSettings) {
276 /* Apply the jEditable handlers to the table on all fields w/o the no_edit id */
277 $('#quotes_editor tbody td[id!="no_edit"]').editable( function(value, settings) {
278 var cellPosition = oTable.fnGetPosition( this );
279 oTable.fnUpdate(value, cellPosition[0], cellPosition[1], false, false);
283 "callback" : function( sValue, y ) {
284 oTable.fnDraw(false); /* no filter/sort or we lose our pagination */
290 $('#footer').css("visibility","visible");
293 function fnHandleFileSelect(evt) {
294 // Reset progress indicator on new file selection.
295 progress.style.width = '0%';
296 progress.textContent = '0%';
298 reader = new FileReader();
299 reader.onerror = fnErrorHandler;
300 reader.onprogress = fnUpdateProgress;
301 reader.onabort = function(e) {
302 alert(_("File read cancelled"));
303 parent.location='quotes-upload.pl';
305 reader.onloadstart = function(e) {
306 $('#cancel_upload').show();
307 $('#progress_bar').addClass("loading");
309 reader.onload = function(e) {
310 // Ensure that the progress bar displays 100% at the end.
311 progress.style.width = '100%';
312 progress.textContent = '100%';
313 $('#cancel_upload').hide();
314 quotes = fnCSVToArray(e.target.result, ',');
318 // perform various sanity checks on the target file prior to uploading...
319 var fileType = evt.target.files[0].type || 'unknown';
320 var fileSizeInK = Math.round(evt.target.files[0].size/1024);
322 if (!fileType.match(/comma-separated-values|csv|excel|calc/i)) {
323 alert(_("Uploads limited to csv. Incorrect filetype: %s").format(fileType));
324 parent.location='quotes-upload.pl';
327 if (fileSizeInK > 512) {
328 if (!confirm(_("%s %s KB Do you really want to upload this file?").format(evt.target.files[0].name, fileSizeInK))) {
329 parent.location='quotes-upload.pl';
333 // Read in the image file as a text string.
334 reader.readAsText(evt.target.files[0]);
337 $('#file_upload').one('change', fnHandleFileSelect);
341 function fnGetData(element) {
343 url : "/cgi-bin/koha/tools/quotes/quotes-upload_ajax.pl",
345 contentType : "application/x-www-form-urlencoded", // we must claim this mimetype or CGI will not decode the URL encoding
348 "quote" : encodeURI ( JSON.stringify(oTable.fnGetData()) ),
351 success : function(){
352 var response = JSON.parse(jqXHR.responseText);
353 if (response.success) {
354 alert(_("%s quotes saved.").format(response.records));
355 window.location.reload(true); // is this the best route?
357 alert(_("%s quotes saved, but an error has occurred. Please ask your administrator to check the server log for more details.").format(response.records));
358 window.location.reload(true); // is this the best route?
364 function fnClickDeleteRow() {
365 var idsToDelete = oTable.$('.selected').map(function() {
369 alert(_("Please select a quote(s) by clicking the quote id(s) you desire to delete."));
371 else if (confirm(_("Are you sure you wish to delete quote(s) %s?").format(idsToDelete))) {
372 oTable.$('.selected').each(function(){
373 oTable.fnDeleteRow(this);
380 [% INCLUDE 'intranet-bottom.inc' %]