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