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