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