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