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