Replace perl/tt2 based circ history download with Javascript and a user/lew/circ_history_csv_redo
authorLlewellyn Marshall <llewellyn.marshall@ncdcr.gov>
Thu, 11 May 2023 20:07:26 +0000 (16:07 -0400)
committerLlewellyn Marshall <llewellyn.marshall@ncdcr.gov>
Mon, 15 May 2023 18:26:17 +0000 (14:26 -0400)
streaming API call to the Circ application. Added a download progress bar to the
myOpac page (Bootstrap only).

Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
Open-ILS/src/templates-bootstrap/opac/myopac/circ_history.tt2
Open-ILS/src/templates/opac/myopac/circ_history.tt2
Open-ILS/src/templates/opac/parts/myopac/circ_history_download.tt2 [new file with mode: 0644]

index 6436f79..f17af7d 100644 (file)
@@ -222,6 +222,147 @@ sub checkouts_by_user_opac {
     return $data;
 }
 
+__PACKAGE__->register_method(
+    method  => "circ_history_count",
+    api_name    => "open-ils.circ.circ_history.count",
+    notes       => q/
+        Returns the number of archived circulations for a user
+        @param auth the auth token of the requestor
+        @param user_id the user to check
+    /);
+
+sub circ_history_count {
+    my( $self, $client, $auth, $user_id ) = @_; 
+    
+    my $e = new_editor( authtoken => $auth );
+    return $e->event unless $e->checkauth;
+
+    my $user_id = (defined $user_id) ? $user_id : $e->requestor->id;
+    if( $e->requestor->id ne $user_id ) {
+        return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
+    }
+    
+    my $count = $e->json_query({
+        select => {
+            auch => [{
+                column => 'id',
+                transform => 'count',
+                aggregate => 1
+            }]
+        },
+        from => 'auch',
+        where => {'+auch' => {usr => $user_id}}
+    })->[0]->{id};
+
+    return $count;
+}
+
+__PACKAGE__->register_method(
+    method  => "circ_history_by_user",
+    api_name    => "open-ils.circ.actor.user.circ_history",
+    stream => 1,
+    NOTES        => <<"    NOTES");
+    Streams a list of circ history objects with the same  
+    default flesh as MyOPAC's circ history page.
+    NOTES
+    
+sub circ_history_by_user {
+    my ($self, $client, $auth, $user_id, $flesh, $limit, $offset) = @_;
+    
+    my $e = new_editor( authtoken => $auth );
+    return $e->event unless $e->checkauth;
+
+    my $user_id = (defined $user_id) ? $user_id : $e->requestor->id;
+    if( $e->requestor->id ne $user_id ) {
+        return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
+    }
+    
+    my %limits = ();
+    $limits{offset} = $offset if defined $offset;
+    $limits{limit} = $limit if defined $limit;
+
+    my %flesh_ops = (
+        flesh => 3,
+        flesh_fields => {
+            auch => ['target_copy','source_circ'],
+            acp => ['call_number','parts'],
+            acn => ['record','prefix','suffix']
+        },
+    );
+
+    $e->xact_begin;
+    my $circs = $e->search_action_user_circ_history(
+        [
+            {usr => $user_id},
+            {   # order newest to oldest by default
+                order_by => {auch => 'xact_start DESC'},
+                $flesh ? %flesh_ops : (),
+                %limits
+            }
+        ],
+        {substream => 1}
+    );
+    $e->rollback;
+
+    unless($flesh){
+        for my $circ (@$circs) {
+            $client->respond(
+                { 
+                    circ => $circ          
+                }
+            );        
+        }
+        return undef;
+    }
+
+    $e->xact_begin;
+    my %unapi_cache = ();
+    for my $circ (@$circs) {
+        if ($circ->target_copy->call_number->id == -1) {
+            $client->respond(
+                { 
+                    circ => $circ,
+                    marc_xml => undef # pre-cat copy, use the dummy title/author instead                
+                }
+            );            
+            next;
+        }
+        my $bre_id = $circ->target_copy->call_number->record->id;
+        my $unapi;
+        if (exists $unapi_cache{$bre_id}) {
+            $unapi = $unapi_cache{$bre_id};
+        } else {
+            my $result = $e->json_query({
+                from => [
+                    'unapi.bre', $bre_id, 'marcxml','record','{mra}', undef, undef, undef
+                ]
+            });
+            if ($result) {
+                $unapi_cache{$bre_id} = $unapi = $result->[0]->{'unapi.bre'};
+            }
+        }
+        if ($unapi) {
+            $client->respond(
+                { 
+                    circ => $circ,
+                    marc_xml => $unapi
+                }
+            );            
+        } else {
+            $client->respond(
+                { 
+                    circ => $circ,
+                    marc_xml => undef # failed, but try to go on             
+                }
+            );
+        }
+    }
+    $e->rollback;
+
+    return undef;
+}
 
 __PACKAGE__->register_method(
     method  => "title_from_transaction",
index e18bdf5..e3cd9f5 100755 (executable)
@@ -1,5 +1,6 @@
 [%  PROCESS "opac/parts/header.tt2";
     PROCESS "opac/parts/misc_util.tt2";
+    PROCESS "opac/parts/myopac/circ_history_download.tt2";
     PROCESS "opac/parts/myopac/column_sort_support.tt2";
     WRAPPER "opac/parts/myopac/base.tt2";
     myopac_page = "circ_history"
         </table>
     </div>
     </form>
-    <form action="[% mkurl(ctx.opac_root _ '/myopac/circ_history/export') %]" method="post">
-                <div>
-                    [%- INCLUDE "opac/parts/preserve_params.tt2" %]
-                    [% IF ctx.circs.size > 0 %]
-                    <input type="hidden" name="filename" value="[% l('circ_history.csv') %]"/>
-                    <button type="submit" class="btn btn-action"><i class="fas fa-file-download" aria-hidden="true"></i> [% l('Download CSV') %]</button>
-                    [% END %]
-                </div>
-            </form>
+    <div id="csv-download-bar-wrapper" class="hidden progress">
+        <div id="csv-download-bar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
+    </div>
+    <br>
+    <button onClick="startCircHistoryDownload()"  class="btn btn-action"><i class="fas fa-file-download" aria-hidden="true"></i> [% l('Download CSV') %]</button> 
     [% END %]
 </div></div>
 [% END %]
index afef26a..d7cfdd6 100644 (file)
@@ -1,5 +1,6 @@
 [%  PROCESS "opac/parts/header.tt2";
     PROCESS "opac/parts/misc_util.tt2";
+    PROCESS "opac/parts/myopac/circ_history_download.tt2";
     PROCESS "opac/parts/myopac/column_sort_support.tt2";
     WRAPPER "opac/parts/myopac/base.tt2";
     myopac_page = "circs"
             [% IF no_next %] class='invisible' [% END %] >[% l('Next') %]<span class="nav_arrow_fix">&#9658;</span></a>
         </span>
         <div class="float-left">
-            <form action="[% mkurl(ctx.opac_root _ '/myopac/circ_history/export') %]" method="post">
                 <div>
-                    [%- INCLUDE "opac/parts/preserve_params.tt2" %]
                     [% IF ctx.circs.size > 0 %]
-                    <input type="hidden" name="filename" value="[% l('circ_history.csv') %]"/>
-                    <button type="submit">[% l('Download CSV') %]</button>
+                    <button onclick="startCircHistoryDownload()">[% l('Download CSV') %]</button>
                     [% END %]
                 </div>
-            </form>
         </div>
     </div>
     <div class="clear-both"></div>
diff --git a/Open-ILS/src/templates/opac/parts/myopac/circ_history_download.tt2 b/Open-ILS/src/templates/opac/parts/myopac/circ_history_download.tt2
new file mode 100644 (file)
index 0000000..92d393c
--- /dev/null
@@ -0,0 +1,147 @@
+<script language='javascript' src='/opac/common/js/utils.js'> </script>
+<script language='javascript' src='/opac/common/js/config.js'> </script> 
+
+<script language='javascript' src='/opac/common/js/JSON_v1.js'> </script>
+<script language='javascript'>
+var fmclasses = {
+    "auch":  ["id","usr","target_copy","checkin_time","due_date","xact_start","source_circ"],
+    "acp":  ["age_protect","alert_message","barcode","call_number"],
+    "acn":  ["copies","create_date","creator","deleted","edit_date","editor","id","label","owning_lib","record","notes","uri_maps","uris","label_sortkey","label_class","prefix","suffix","dewey"],
+    "acnp": ["id","label","label_sortkey","owning_lib"],
+    "acns": ["id","label","label_sortkey","owning_lib"],
+};
+</script>
+<script language='javascript' src='/opac/common/js/fmgen.js'> </script>
+<script language='javascript' src='/opac/common/js/opac_utils.js'> </script>
+
+<script>
+    const auth = "[% CGI.cookie('ses') %]";
+    const progressBarWrapper = "#csv-download-bar-wrapper";
+    const progressBar = "#csv-download-bar";
+    const header = '"Title","Author","Checkout Date","Due Date","Date Returned","Barcode","Call Number","Format"';
+    //number of records to retrieve from the server at once
+    const maxRecords = 50;
+    //number of records processed locally
+    var processedRecords = 0;
+    //total number of AUCH records
+    var totalRecords = 0;
+       content = [];
+    
+    function startCircHistoryDownload(){
+        
+        new OpenSRF.ClientSession('open-ils.circ').request({
+            method: 'open-ils.circ.circ_history.count',
+            params: [auth],
+            oncomplete: circHistoryDownload,
+            onerror: function(e){console.log(e);}
+        }).send();         
+    }
+    
+    function circHistoryDownload(r){
+        resp = r.recv();
+        if(!resp)return;
+        totalRecords = resp.content();
+        console.log("Total records to process: " + totalRecords);
+        processedRecords = 0;
+        showProgressBar();
+        for(var i = 0; i < totalRecords; i += maxRecords){
+            new OpenSRF.ClientSession('open-ils.circ').request({
+                method: 'open-ils.circ.actor.user.circ_history',
+                params: [auth,null,true,maxRecords,i],
+                async: true,
+                onresponse: addRecord,
+                oncomplete: exportRecords,
+                onerror: function(e){console.log(e);}
+            }).send();
+        }
+    }
+
+    function addRecord(r){
+        var c = r.response_queue[r.response_queue.length-1].content();
+        var circ = c.circ;
+        var xml = c.marc_xml;
+        var title = titleFromXml(xml);
+        var author = authorFromXml(xml); 
+        var format = formatFromXml(xml); 
+        var callNumber = cnFromCirc(circ);
+        content.push(
+            [title,author,circ.xact_start(),circ.due_date(),circ.checkin_time(),circ.target_copy().barcode(),callNumber,format]
+        )
+        processedRecords += 1;
+    }
+    
+    function showProgressBar(){
+        $(progressBarWrapper).removeClass("hidden");
+    } 
+    
+    function hideProgressBar(){
+        $(progressBarWrapper).addClass("hidden");
+    }
+    
+    function updateProgressBar(){
+        var progress = Math.floor((processedRecords/totalRecords)*100.);
+        $(progressBar).attr('aria-valuenow', progress).css('width', progress+'%');
+    }
+    
+    function exportRecords(){
+        updateProgressBar();
+        if(processedRecords < totalRecords){           
+            return;
+        }
+        content.sort(function(a,b){return (new Date(a[2])) > (new Date(b[2]));})
+        var csv_content = "";
+        var csv_data = [];
+        csv_data.push(header);
+        content.forEach(function(c){    
+                csv_data.push(
+                    c.map(function(p){return '"'+p+'"';})
+                    .join(",")
+                );
+            }
+        );
+        csv_content = csv_data.join("\r\n");
+        var blob = new Blob([csv_content], { type: 'text/csv;charset=utf-8;' });
+        if (navigator.msSaveBlob) { // IE 10+
+            navigator.msSaveBlob(blob, 'circ_history.csv');
+        } else {
+            var link = document.createElement("a");
+            if (link.download !== undefined) { // feature detection
+                // Browsers that support HTML5 download attribute
+                var url = URL.createObjectURL(blob);
+                link.setAttribute("href", url);
+                link.setAttribute("download", 'circ_history.csv');
+                link.style.visibility = 'hidden';
+                document.body.appendChild(link);
+                link.click();
+                document.body.removeChild(link);
+            }
+        }
+        hideProgressBar();
+    }
+    
+    function titleFromXml(marc_xml){
+        var titleParts = $.map($(marc_xml).find('[tag="245"]').find('[code="a"], [code="b"], [code="n"], [code="p"]'),function(p){return p.innerText;})
+        var titlePartsContent = titleParts.join(" ");
+        return (titlePartsContent || '').replace(/[:;\/]/ig, '').trim();
+    } 
+    
+    function authorFromXml(marc_xml){
+        var authorParts = $.map($(marc_xml).find('[tag="100"], [tag="110"], [tag="111"] ').find('[code="a"]'),function(p){return p.innerText;})
+        return (authorParts.join(" ") || '').trim();
+    }
+    
+    function formatFromXml(marc_xml){
+        var formatParts = $.map($(marc_xml).find('field[name="search_format"],field[name="icon_format"],field[name="mr_hold_format"]'),function(p){return p.innerText;})
+        //only take first valid format we find
+        for(var i = 0; i < formatParts.length; i++){
+            if(formatParts[i] != "")return formatParts[i];
+        }
+        return "";
+    }
+    
+    function cnFromCirc(circ){
+        return [circ.target_copy().call_number().prefix().label_sortkey(),circ.target_copy().call_number().label_sortkey(),circ.target_copy().call_number().suffix().label_sortkey()].join("");
+    }
+    
+    
+</script>
\ No newline at end of file