[Koha-patches] [PATCH 6/7] Beginning work on QOTD uploader

Chris Nighswonger cnighswonger at foundations.edu
Fri Apr 27 21:26:09 CEST 2012


This series will add a DataTable's based upload/editor with which
to upload csv files containing quotes to be used by the QOTD
feature.

The file should be formatted thusly:

"source","text-of-quote"
"source","text-of-quote"
...

Note: This work serves as a good example of potential improvements
in all other "editor" and file upload areas of Koha.

This patch is a squash of the following work:

Adding code to parse CSV file contents and push it into a DataTable

Adding in jEditable to enable table editing

Adding ajax to post data back to the server to be saved

Fixing edit and adding delete functionality

Adding some missing css as well as server feedback on save

Fixing a bug which limited the number of quotes which could be uploaded

Also fixing a minor bug with fnCSVToArray and doing some style cleanup.
---
 koha-tmpl/intranet-tmpl/prog/en/css/uploader.css   |   38 +++
 .../prog/en/modules/tools/quotes-upload.tt         |  300 ++++++++++++++++++++
 .../intranet-tmpl/prog/en/modules/tools/quotes.tt  |   10 +-
 koha-tmpl/intranet-tmpl/prog/img/x_alt_16x16.png   |  Bin 0 -> 215 bytes
 tools/quotes-upload.pl                             |   44 +++
 tools/quotes/quotes-upload_ajax.pl                 |   68 +++++
 6 files changed, 455 insertions(+), 5 deletions(-)
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/css/uploader.css
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/tools/quotes-upload.tt
 create mode 100644 koha-tmpl/intranet-tmpl/prog/img/x_alt_16x16.png
 create mode 100755 tools/quotes-upload.pl
 create mode 100755 tools/quotes/quotes-upload_ajax.pl

diff --git a/koha-tmpl/intranet-tmpl/prog/en/css/uploader.css b/koha-tmpl/intranet-tmpl/prog/en/css/uploader.css
new file mode 100644
index 0000000..eca87b9
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/css/uploader.css
@@ -0,0 +1,38 @@
+#progress_bar {
+  margin: 10px 0;
+  padding: 3px;
+  border: 1px solid #000;
+  font-size: 14px;
+  clear: both;
+  opacity: 0;
+  -moz-transition: opacity 1s linear;
+  -o-transition: opacity 1s linear;
+  -webkit-transition: opacity 1s linear;
+}
+#progress_bar.loading {
+  opacity: 1.0;
+}
+#progress_bar .percent {
+  background-color: #99ccff;
+  height: auto;
+  width: 0;
+}
+#server_response {
+    background-color: white;
+    background-image: url("../../img/x_alt_16x16.png");
+    background-repeat: no-repeat;
+    background-origin: padding-box;
+    background-position: right top;
+    border: 1px solid #DDDDDD;
+    color: #999999;
+    font-size: 14px;
+    height: 30px;
+    left: 50%;
+    margin-left: -125px;
+    margin-top: -15px;
+    padding: 14px 0 2px;
+    position: fixed;
+    text-align: center;
+    top: 50%;
+    width: 250px;
+}
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/quotes-upload.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/quotes-upload.tt
new file mode 100644
index 0000000..05fac8b
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/quotes-upload.tt
@@ -0,0 +1,300 @@
+    [% INCLUDE 'doc-head-open.inc' %]
+    <title>Koha &rsaquo; Tools &rsaquo; Quote Uploader</title>
+    [% INCLUDE 'doc-head-close.inc' %]
+    <link rel="stylesheet" type="text/css" href="/intranet-tmpl/prog/en/css/uploader.css" />
+    <link rel="stylesheet" type="text/css" href="/intranet-tmpl/prog/en/css/datatables.css" />
+    <script type="text/javascript" src="/intranet-tmpl/prog/en/lib/jquery/plugins/jquery.dataTables.min.js"></script>
+    [% INCLUDE 'datatables-strings.inc' %]
+    </script>
+    <script type="text/javascript" src="/intranet-tmpl/prog/en/js/datatables.js"></script>
+    <script type="text/javascript" src="/intranet-tmpl/prog/en/js/jquery.jeditable.mini.js"></script>
+    <script type="text/javascript">
+    //<![CDATA[
+    var oTable; //DataTable object
+    $(document).ready(function() {
+
+    // Credits:
+    // FileReader() code copied and hacked from:
+    // http://www.html5rocks.com/en/tutorials/file/dndfiles/
+    // fnCSVToArray() gratefully borrowed from:
+    // http://www.bennadel.com/blog/1504-Ask-Ben-Parsing-CSV-Strings-With-Javascript-Exec-Regular-Expression-Command.htm
+
+    var reader;
+    var progress = document.querySelector('.percent');
+    $("#server_response").hide();
+
+    function fnAbortRead() {
+        reader.abort();
+    }
+
+    function fnErrorHandler(evt) {
+        switch(evt.target.error.code) {
+            case evt.target.error.NOT_FOUND_ERR:
+                alert('File Not Found!');
+                break;
+            case evt.target.error.NOT_READABLE_ERR:
+                alert('File is not readable');
+                break;
+            case evt.target.error.ABORT_ERR:
+                break; // noop
+            default:
+                alert('An error occurred reading this file.');
+        };
+    }
+
+    function fnUpdateProgress(evt) {
+        // evt is an ProgressEvent.
+        if (evt.lengthComputable) {
+            var percentLoaded = Math.round((evt.loaded / evt.total) * 100);
+            // Increase the progress bar length.
+            if (percentLoaded < 100) {
+                progress.style.width = percentLoaded + '%';
+                progress.textContent = percentLoaded + '%';
+            }
+        }
+    }
+
+    function fnCSVToArray( strData, strDelimiter ){
+        // This will parse a delimited string into an array of
+        // arrays. The default delimiter is the comma, but this
+        // can be overriden in the second argument.
+
+        // Check to see if the delimiter is defined. If not,
+        // then default to comma.
+        strDelimiter = (strDelimiter || ",");
+
+        // Create a regular expression to parse the CSV values.
+        var objPattern = new RegExp(
+        (
+            // Delimiters.
+            "(\\" + strDelimiter + "|\\r?\\n|\\r|^)" +
+            // Quoted fields.
+            "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" +
+            // Standard fields.
+            "([^\"\\" + strDelimiter + "\\r\\n]*))"
+        ),
+            "gi"
+        );
+
+        // Create an array to hold our data. Give the array
+        // a default empty first row.
+        var arrData = [[]];
+
+        // Create an array to hold our individual pattern
+        // matching groups.
+        var arrMatches = null;
+
+        // Keep looping over the regular expression matches
+        // until we can no longer find a match.
+        while (arrMatches = objPattern.exec( strData )){
+
+            // Get the delimiter that was found.
+            var strMatchedDelimiter = arrMatches[ 1 ];
+
+            // Check to see if the given delimiter has a length
+            // (is not the start of string) and if it matches
+            // field delimiter. If it does not, then we know
+            // that this delimiter is a row delimiter.
+            if ( strMatchedDelimiter.length && (strMatchedDelimiter != strDelimiter) ){
+                // Since we have reached a new row of data,
+                // add an empty row to our data array.
+                // Note: if there is not more data, we will have to remove this row later
+                arrData.push( [] );
+            }
+
+            // Now that we have our delimiter out of the way,
+            // let's check to see which kind of value we
+            // captured (quoted or unquoted).
+            if (arrMatches[ 2 ]){
+                // We found a quoted value. When we capture
+                // this value, unescape any double quotes.
+                var strMatchedValue = arrMatches[ 2 ].replace(
+                new RegExp( "\"\"", "g" ),
+                    "\""
+                );
+            } else if (arrMatches[3]){
+                // We found a non-quoted value.
+                var strMatchedValue = arrMatches[ 3 ];
+            } else {
+                // There is no more valid data so remove the row we added earlier
+                // Is there a better way? Perhaps a look-ahead regexp?
+                arrData.splice(arrData.length-1, 1);
+            }
+
+            // Now that we have our value string, let's add
+            // it to the data array.
+            arrData[ arrData.length - 1 ].push( strMatchedValue );
+        }
+
+        // Return the parsed data.
+        return( arrData );
+    }
+
+    function fnDataTable(aaData) {
+        for(var i=0; i<aaData.length; i++) {
+            aaData[i].push('Delete'); //this is hackish FIXME
+        }
+        document.getElementById('quotes_editor').style.visibility="visible";
+        document.getElementById('file_uploader').style.visibility="hidden";
+        oTable = $('#quotes_editor').dataTable( {
+            "bAutoWidth"        : false,
+            "bPaginate"         : true,
+            "bSort"             : false,
+            "sPaginationType"   : "full_numbers",
+            "sDom"              : '<"save_quotes">frtip',
+            "aaData"            : aaData,
+            "aoColumns"         : [
+                {
+                    "sTitle"  : "Source",
+                    "sWidth"  : "15%",
+                },
+                {
+                    "sTitle"  : "Quote",
+                    "sWidth"  : "75%",
+                },
+                {
+                    "sTitle"  : "Actions",
+                    "sWidth"  : "10%",
+                },
+            ],
+           "fnPreDrawCallback": function(oSettings) {
+                return true;
+            },
+            "fnRowCallback": function( nRow, aData, iDisplayIndex ) {
+                noEditFields = [2]; /* action */
+                /* console.log('Quote ID: '+quoteID); */
+                /* do foo on various cells in the current row */
+                $('td:eq(2)', nRow).html('<input type="button" class="delete" value="Delete" onclick="fnClickDeleteRow(this.parentNode);" />');
+                /* apply no_edit id to noEditFields */
+                for (i=0; i<noEditFields.length; i++) {
+                    $('td', nRow)[noEditFields[i]].setAttribute("id","no_edit");
+                }
+                return nRow;
+            },
+           "fnDrawCallback": function(oSettings) {
+                /* Apply the jEditable handlers to the table on all fields w/o the no_edit id */
+                $('#quotes_editor tbody td[id!="no_edit"]').editable( function(value, settings) {
+                        var cellPosition = oTable.fnGetPosition( this );
+                        oTable.fnUpdate(value, cellPosition[0], cellPosition[1], false, false);
+                        return(value);
+                    },
+                    {
+                    "callback"      : function( sValue, y ) {
+                                          oTable.fnDraw(false); /* no filter/sort or we lose our pagination */
+                                      },
+                    "height"        : "14px",
+                });
+                $("div.save_quotes").html('<input type="button" class="add_quote_button" value="Save Quotes" style="float: right;" onclick="fnGetData(document.getElementById(\'quotes_editor\'));"/>');
+           },
+        });
+    }
+
+    function fnHandleFileSelect(evt) {
+        // Reset progress indicator on new file selection.
+        progress.style.width = '0%';
+        progress.textContent = '0%';
+
+        reader = new FileReader();
+        reader.onerror = fnErrorHandler;
+        reader.onprogress = fnUpdateProgress;
+        reader.onabort = function(e) {
+            alert('File read cancelled');
+        };
+        reader.onloadstart = function(e) {
+            document.getElementById('progress_bar').className = 'loading';
+        };
+        reader.onload = function(e) {
+            // Ensure that the progress bar displays 100% at the end.
+            progress.style.width = '100%';
+            progress.textContent = '100%';
+            setTimeout("document.getElementById('progress_bar').className='';", 2000);
+            quotes = fnCSVToArray(e.target.result, ',');
+            fnDataTable(quotes);
+        }
+        // Read in the image file as a text string.
+        reader.readAsText(evt.target.files[0]);
+    }
+
+    document.getElementById('files').addEventListener('change', fnHandleFileSelect, false);
+
+    });
+
+    function fnGetData(element) {
+        var jqXHR = $.ajax({
+            url         : "/cgi-bin/koha/tools/quotes/quotes-upload_ajax.pl",
+            type        : "POST",
+            contentType : "application/x-www-form-urlencoded", // we must claim this mimetype or CGI will not decode the URL encoding
+            dataType    : "json",
+            data        : {
+                            "quote"     : JSON.stringify(oTable.fnGetData()),
+                            "action"    : "add",
+                          },
+            success     : function(){
+                            var response = JSON.parse(jqXHR.responseText);
+                            if (response.success) {
+                                $("#server_response").text(response.records+' quotes saved.');
+                            }
+                            else {
+                                $("#server_response").text('An error has occurred. '+response.records+' quotes saved. Please ask your administrator to check the server log for more details.');
+                            }
+                            $("#server_response").fadeIn(200);
+                          },
+        });
+    }
+
+    function fnClickDeleteRow(td) {
+        oTable.fnDeleteRow(oTable.fnGetPosition(td)[0]);
+    }
+
+    function fnResetUpload() {
+        $('#server_response').fadeOut(200);
+        window.location.reload(true);   // is this the best route?
+    }
+
+    //]]>
+    </script>
+</head>
+<body id="tools_quotes" class="tools">
+[% INCLUDE 'header.inc' %]
+[% INCLUDE 'cat-search.inc' %]
+
+<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; Quote Uploader</div>
+
+<div id="doc3" class="yui-t2">
+    <div id="bd">
+        <div id="yui-main">
+            <div class="yui-b">
+
+                <div id="file_uploader" style="float: left; width: 100%; visibility:visible;">
+                    <input type="file" id="files" name="file" />
+                    <button onclick="fnAbortRead();">Cancel Upload</button>
+                    <div id="progress_bar"><div class="percent">0%</div></div>
+                </div>
+                <div id="server_response" onclick='fnResetUpload()'>Server Response</div>
+                <table id="quotes_editor" style="float: left; width: 100%; visibility:hidden;">
+                <thead>
+                    <tr>
+                        <th>Source</th>
+                        <th>Text</th>
+                        <th>Actions</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <!-- tbody content is generated by DataTables -->
+                    <tr>
+                        <td></td>
+                        <td>Loading data...</td>
+                        <td></td>
+                    </tr>
+                </tbody>
+                </table>
+
+
+
+            </div>
+        </div>
+    <div class="yui-b noprint">
+        [% INCLUDE 'tools-menu.inc' %]
+    </div>
+</div>
+[% INCLUDE 'intranet-bottom.inc' %]
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/quotes.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/quotes.tt
index 49df31d..a5c87e4 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/quotes.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/quotes.tt
@@ -1,12 +1,12 @@
     [% INCLUDE 'doc-head-open.inc' %]
     <title>Koha &rsaquo; Tools &rsaquo; Quote Editor</title>
     [% INCLUDE 'doc-head-close.inc' %]
-    <link rel="stylesheet" type="text/css" href="/intranet-tmpl/prog/en/css/datatables.css" />
-    <script type="text/javascript" src="/intranet-tmpl/prog/en/lib/jquery/plugins/jquery.dataTables.min.js"></script>
+    <link rel="stylesheet" type="text/css" href="[% themelang %]/css/datatables.css" />
+    <script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.dataTables.min.js"></script>
     [% INCLUDE 'datatables-strings.inc' %]
     </script>
-    <script type="text/javascript" src="/intranet-tmpl/prog/en/js/datatables.js"></script>
-    <script type="text/javascript" src="/intranet-tmpl/prog/en/js/jquery.jeditable.mini.js"></script>
+    <script type="text/javascript" src="[% themelang %]/js/datatables.js"></script>
+    <script type="text/javascript" src="[% themelang %]/js/jquery.jeditable.mini.js"></script>
     <script type="text/javascript">
     //<![CDATA[
     var oTable; /* oTable needs to be global */
@@ -64,7 +64,7 @@
                         });
                    },
         });
-        $("div.add_quote").html('<input type="button" class="add_quote_button" value="Add Quote" style="float: right;" onclick="fnClickAddRow();"/>');
+        $("div.add_quote").html('<input type="button" class="add_quote_button" value="Add Quote" style="float: right;" onclick="fnClickAddRow();"/><input type="button" class="import_quote_button" value="Import Quotes" style="float: right;" onclick="parent.location=\'quotes-upload.pl\'"/>');
     });
 
         function fnClickAddQuote() {
diff --git a/koha-tmpl/intranet-tmpl/prog/img/x_alt_16x16.png b/koha-tmpl/intranet-tmpl/prog/img/x_alt_16x16.png
new file mode 100644
index 0000000000000000000000000000000000000000..a99310e42eb61377703e8a2564fb7c88191ccb3e
GIT binary patch
literal 215
zcmeAS at N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk?
zp1FzXsX?iUDV2pMQ*D5XDm`5sLn>}1B^+Skbl4-`@qh8dzX6tW92h&!CtvL7JMVlo
zDWlH6B~7BPKkr1PkcskcmUrnTm!&skWE^<pRWRet at kIi66Cxg%CU8cqIp^(Qq{7(l
zohY$fUiiWyvDH>;Y+4LmdmRcDf at FF{)Kylgq;?q#- at GZQaDc&4dBx&a%xew<9l_w~
L>gTe~DWM4fy}3+!

literal 0
HcmV?d00001

diff --git a/tools/quotes-upload.pl b/tools/quotes-upload.pl
new file mode 100755
index 0000000..7db61ce
--- /dev/null
+++ b/tools/quotes-upload.pl
@@ -0,0 +1,44 @@
+#!/usr/bin/perl
+
+# Copyright 2012 Foundations Bible College Inc.
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 2 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use strict;
+use warnings;
+
+use CGI;
+use autouse 'Data::Dumper' => qw(Dumper);
+
+use C4::Auth;
+use C4::Koha;
+use C4::Context;
+use C4::Output;
+
+my $cgi = new CGI;
+
+my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
+    {
+        template_name   => "tools/quotes-upload.tt",
+        query           => $cgi,
+        type            => "intranet",
+        authnotrequired => 0,
+        flagsrequired   => { tools => 'edit_quotes' },
+        debug           => 1,
+    }
+);
+
+output_html_with_http_headers $cgi, $cookie, $template->output;
diff --git a/tools/quotes/quotes-upload_ajax.pl b/tools/quotes/quotes-upload_ajax.pl
new file mode 100755
index 0000000..f1c3746
--- /dev/null
+++ b/tools/quotes/quotes-upload_ajax.pl
@@ -0,0 +1,68 @@
+#!/usr/bin/perl
+
+# Copyright 2012 Foundations Bible College Inc.
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 2 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use strict;
+use warnings;
+
+use CGI;
+use JSON;
+use autouse 'Data::Dumper' => qw(Dumper);
+
+use C4::Auth;
+use C4::Koha;
+use C4::Context;
+use C4::Output;
+
+my $cgi = new CGI;
+my $dbh = C4::Context->dbh;
+
+my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
+    {
+        template_name   => "",
+        query           => $cgi,
+        type            => "intranet",
+        authnotrequired => 0,
+        flagsrequired   => { tools => 'edit_quotes' },
+        debug           => 1,
+    }
+);
+
+my $success = 'true';
+
+my $quotes = decode_json($cgi->param('quote'));
+my $action = $cgi->param('action');
+
+my $sth = $dbh->prepare('INSERT INTO quotes (source, text) VALUES (?, ?);');
+
+my $insert_count = 0;
+
+foreach my $quote (@$quotes) {
+    $insert_count++ if $sth->execute($quote->[0], $quote->[1]);
+    if ($sth->err) {
+        warn sprintf('Database returned the following error: %s', $sth->errstr);
+        $success = 'false';
+    }
+}
+
+print $cgi->header('application/json');
+
+print to_json({
+                success => $success,
+                records => $insert_count,
+});
-- 
1.7.0.4



More information about the Koha-patches mailing list