Expired holds shelf printer needs to be a holds shelf *clearer* and printer
authorsenator <senator@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Thu, 7 Oct 2010 22:37:45 +0000 (22:37 +0000)
committersenator <senator@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Thu, 7 Oct 2010 22:37:45 +0000 (22:37 +0000)
This needs cleaned up and stuff, and made into something cooler.
Basically just does what XUL interfaces under the Circ menu can already do,
but streamlined to tolerate really big datasets.

Much of this code originates from berick and miker.

git-svn-id: svn://svn.open-ils.org/ILS/trunk@18230 dcc99617-32d9-48b4-a31d-7c20da2025e4

Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm
Open-ILS/web/opac/extras/circ/alt_holds_print.html
Open-ILS/web/opac/extras/circ/alt_holds_print.js [new file with mode: 0644]

index ebdaf53..97cb623 100644 (file)
@@ -34,6 +34,8 @@ use OpenILS::Application::Actor::Friends;
 use DateTime;
 use DateTime::Format::ISO8601;
 use OpenSRF::Utils qw/:datetime/;
+use Digest::MD5 qw(md5_hex);
+use OpenSRF::Utils::Cache;
 my $apputils = "OpenILS::Application::AppUtils";
 my $U = $apputils;
 
@@ -1407,7 +1409,7 @@ sub print_hold_pull_list_stream {
             (@$sort ? (order_by => $sort) : ()),
             ($$params{limit} ? (limit => $$params{limit}) : ()),
             ($$params{offset} ? (offset => $$params{offset}) : ())
-        }, {"subquery" => 1}
+        }, {"substream" => 1}
     ) or return $e->die_event;
 
     $logger->info("about to stream back " . scalar(@$holds_ids) . " holds");
@@ -2754,6 +2756,86 @@ sub find_hold_mvr {
        return ( $U->record_to_mvr($title), $volume, $copy, $issuance );
 }
 
+__PACKAGE__->register_method(
+    method    => 'clear_shelf_cache',
+    api_name  => 'open-ils.circ.hold.clear_shelf.get_cache',
+    stream    => 1,
+    signature => {
+        desc => q/
+            Returns the holds processed with the given cache key
+        /
+    }
+);
+
+sub clear_shelf_cache {
+    my($self, $client, $auth, $cache_key, $chunk_size) = @_;
+    my $e = new_editor(authtoken => $auth, xact => 1);
+    return $e->die_event unless $e->checkauth and $e->allowed('VIEW_HOLD');
+
+    $chunk_size ||= 25;
+    my $hold_data = OpenSRF::Utils::Cache->new('global')->get_cache($cache_key);
+
+    if (!$hold_data) {
+        $logger->info("no hold data found in cache"); # XXX TODO return event
+        $e->rollback;
+        return undef;
+    }
+
+    my $maximum = 0;
+    foreach (keys %$hold_data) {
+        $maximum += scalar(@{ $hold_data->{$_} });
+    }
+    $client->respond({"maximum" => $maximum, "progress" => 0});
+
+    for my $action (sort keys %$hold_data) {
+        while (@{$hold_data->{$action}}) {
+            my @hid_chunk = splice @{$hold_data->{$action}}, 0, $chunk_size;
+
+            my $result_chunk = $e->json_query({
+                "select" => {
+                    "acp" => ["barcode"],
+                    "au" => [qw/
+                        first_given_name second_given_name family_name alias
+                    /],
+                    "acn" => ["label"],
+                    "bre" => ["marc"],
+                    "acpl" => ["name"],
+                    "ahr" => ["id"]
+                },
+                "from" => {
+                    "ahr" => {
+                        "acp" => {
+                            "field" => "id", "fkey" => "current_copy",
+                            "join" => {
+                                "acn" => {
+                                    "field" => "id", "fkey" => "call_number",
+                                    "join" => {
+                                        "bre" => {
+                                            "field" => "id", "fkey" => "record"
+                                        }
+                                    }
+                                },
+                                "acpl" => {"field" => "id", "fkey" => "location"}
+                            }
+                        },
+                        "au" => {"field" => "id", "fkey" => "usr"}
+                    }
+                },
+                "where" => {"+ahr" => {"id" => \@hid_chunk}}
+            }, {"substream" => 1}) or return $e->die_event;
+
+            $client->respond([
+                map {
+                    +{"action" => $action, "hold_details" => $_}
+                } @$result_chunk
+            ]);
+        }
+    }
+
+    $e->rollback;
+    return undef;
+}
+
 
 __PACKAGE__->register_method(
     method    => 'clear_shelf_process',
@@ -2776,6 +2858,7 @@ sub clear_shelf_process {
 
        my $e = new_editor(authtoken=>$auth, xact => 1);
        $e->checkauth or return $e->die_event;
+       my $cache = OpenSRF::Utils::Cache->new('global');
 
     $org_id ||= $e->requestor->ws_ou;
        $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
@@ -2793,8 +2876,9 @@ sub clear_shelf_process {
         { idlist => 1 }
     );
 
-
     my @holds;
+    my $chunk_size = 25; # chunked status updates
+    my $counter = 0;
     for my $hold_id (@$hold_ids) {
 
         $logger->info("Clear shelf processing hold $hold_id");
@@ -2821,51 +2905,47 @@ sub clear_shelf_process {
         }
 
         push(@holds, $hold);
+        $client->respond({maximum => scalar(@holds), progress => $counter}) if ( (++$counter % $chunk_size) == 0);
     }
 
     if ($e->commit) {
 
+        my %cache_data = (
+            hold => [],
+            transit => [],
+            shelf => []
+        );
+
         for my $hold (@holds) {
 
             my $copy = $hold->current_copy;
-
             my ($alt_hold) = __PACKAGE__->find_nearest_permitted_hold($e, $copy, $e->requestor, 1);
 
             if($alt_hold) {
 
-                # copy is needed for a hold
-                $client->respond({action => 'hold', copy => $copy, hold_id => $hold->id});
+                push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
 
             } elsif($copy->circ_lib != $e->requestor->ws_ou) {
 
-                # copy needs to transit
-                $client->respond({action => 'transit', copy => $copy, hold_id => $hold->id});
+                push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
 
             } else {
 
-                # copy needs to go back to the shelf
-                $client->respond({action => 'shelf', copy => $copy, hold_id => $hold->id});
+                push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
             }
         }
 
-        # tell the client we're done
-        $client->respond_complete;
-
-        # fire off the hold cancelation trigger
-        my $trigger = OpenSRF::AppSession->connect('open-ils.trigger');
+        my $cache_key = md5_hex(time . $$ . rand());
+        $logger->info("clear_shelf_cache: storing under $cache_key");
+        $cache->put_cache($cache_key, \%cache_data, 7200); # TODO: 2 hours.  configurable?
 
-        for my $hold (@holds) {
-
-            my $req = $trigger->request(
-                'open-ils.trigger.event.autocreate', 
-                'hold_request.cancel.expire_holds_shelf', 
-                $hold, $org_id);
-
-            # wait for response so don't flood the service
-            $req->recv;
-        }
+        # tell the client we're done
+        $client->respond_complete({cache_key => $cache_key});
 
-        $trigger->disconnect;
+        # fire off the hold cancelation trigger and wait for response so don't flood the service
+        $U->create_events_for_hook(
+            'hold_request.cancel.expire_holds_shelf', 
+            $_, $org_id, undef, undef, 1) for @holds;
 
     } else {
         # tell the client we're done
index 6cde9e2..849b369 100644 (file)
@@ -8,13 +8,9 @@
             @import url('/opac/skin/default/css/layout.css');
         </style>
         <style type="text/css">
-           /* html, body {
-                height: 100%;
-                width: 100%;
-                margin: 0px 0px 0px 0px;
-                padding: 0px 0px 0px 0px;
-                overflow: hidden;
-            } */
+            #clear_holds_deck { margin-bottom: 1em; }
+            a { color: blue; text-decoration: underline; }
+            small { font-size: 9pt; }
             body { font-size: 14pt; }
             td {
                 padding-right: 1em;
         <script type="text/javascript" src="/js/dojo/openils/AutoIDL.js"></script>
         <script type="text/javascript" src="/js/dojo/openils/User.js"></script>
         <script type="text/javascript" src="/js/dojo/openils/Util.js"></script>
+        <script type="text/javascript" src="/opac/extras/circ/alt_holds_print.js"></script>
         <script type="text/javascript">
-            dojo.require("dojo.cookie");
-            dojo.require("dojox.xml.parser");
-            dojo.require("openils.BibTemplate");
-            dojo.require("openils.widget.ProgressDialog");
-
-            function do_pull_list(user, cgi) {
-                progress_dialog.show(true);
-
-                var any = false;
-
-                fieldmapper.standardRequest(
-                    ['open-ils.circ','open-ils.circ.hold_pull_list.print.stream'],
-                    { async : true,
-                      params: [
-                        user.authtoken,
-                        { org_id     : cgi.param('o'),
-                          limit      : cgi.param('limit'),
-                          offset     : cgi.param('offset'),
-                          chunk_size : cgi.param('chunk_size'),
-                          sort       : sort_order
-                        }
-                      ],
-                      onresponse : function (r) {
-                        any = true;
-                        dojo.forEach( openils.Util.readResponse(r), function (hold_fm) {
-    
-                            // hashify the hold
-                            var hold = hold_fm.toHash(true);
-                            hold.usr = hold_fm.usr().toHash(true);
-                            hold.usr.card = hold_fm.usr().card().toHash(true);
-                            hold.current_copy = hold_fm.current_copy().toHash(true);
-                            hold.current_copy.location = hold_fm.current_copy().location().toHash(true);
-                            hold.current_copy.call_number = hold_fm.current_copy().call_number().toHash(true);
-                            hold.current_copy.call_number.record = hold_fm.current_copy().call_number().record().toHash(true);
-    
-                            // clone the template's html
-                            var tr = dojo.clone(
-                                dojo.query("tr", dojo.byId('template'))[0]
-                            );
-                            dojo.query("td:not([type])", tr).forEach(
-                                function(td) {
-                                    td.innerHTML =
-                                        dojo.string.substitute(td.innerHTML, hold);
-                                }
-                            );
-    
-                            new openils.BibTemplate({
-                                root : tr,
-                                xml  : dojox.xml.parser.parse(hold.current_copy.call_number.record.marc),
-                                delay: false
-                            });
-    
-                            dojo.place(tr, "target");
-                        });
-                      },
-                      oncomplete : function () {
-                        progress_dialog.hide();
-                        if (any)
-                            window.print();
-                        else
-                            alert(dojo.byId("no_results").innerHTML);
-                      }
-                    }
-                );
-            }
-
-            function place_by_sortkey(node, container) {
-                /*Don't use a forEach() or anything like that here. too slow.*/
-                var sortkey = dojo.attr(node, "sortkey");
-                for (var i = 0; i < container.childNodes.length; i++) {
-                    var rover = container.childNodes[i];
-                    if (rover.nodeType != 1) continue;
-                    if (dojo.attr(rover, "sortkey") > sortkey) {
-                        dojo.place(node, rover, "before");
-                        return;
-                    }
-                }
-                dojo.place(node, container, "last");
-            }
-
-            function do_shelf_expired_holds(user, cgi) {
-                progress_dialog.show(true);
-
-                var any = false;
-                var target = dojo.byId("target");
-                fieldmapper.standardRequest(
-                    ["open-ils.circ",
-                        "open-ils.circ.captured_holds.expired.print.stream"], {
-                        "async": true,
-                        "params": [
-                            user.authtoken, {
-                                "org_id": cgi.param("o"),
-                                "limit": cgi.param("limit"),
-                                "offset": cgi.param("offset"),
-                                "chunk_size": cgi.param("chunk_size"),
-                                "sort": sort_order
-                            }
-                        ],
-                        "onresponse": function(r) {
-                            dojo.forEach(
-                                openils.Util.readResponse(r),
-                                function(hold_fields) {
-                                    any = true;
-                                    /* munge this object to make it look like
-                                       the template expects */
-                                    var hold  = {
-                                        "usr": {},
-                                        "current_copy": {
-                                            "barcode": hold_fields.barcode,
-                                            "call_number": {
-                                                "label": hold_fields.label,
-                                                "record": {"marc": hold_fields.marc}
-                                            },
-                                            "location": {"name": hold_fields.name}
-                                        }
-                                    };
-                                    if (hold_fields.alias) {
-                                        hold.usr.display_name = hold_fields.alias;
-                                    } else {
-                                        hold.usr.display_name = [
-                                            (hold_fields.family_name ? hold_fields.family_name : ""),
-                                            (hold_fields.first_given_name ? hold_fields.first_given_name : ""),
-                                            (hold_fields.second_given_name ? hold_fields.second_given_name : "")
-                                        ].join(" ");
-                                    }
-                                    ["first_given_name","second_given_name","family_name","alias"].forEach(function(k) {hold.usr[k] = hold_fields[k]; });
-    
-                                    // clone the template's html
-                                    var tr = dojo.clone(
-                                        dojo.query("tr", dojo.byId('template'))[0]
-                                    );
-                                    dojo.query("td:not([type])", tr).forEach(
-                                        function(td) {
-                                            td.innerHTML =
-                                                dojo.string.substitute(td.innerHTML, hold);
-                                        }
-                                    );
-            
-                                    new openils.BibTemplate({
-                                        "root": tr,
-                                        "xml": dojox.xml.parser.parse(hold.current_copy.call_number.record.marc),
-                                        "delay": false
-                                    });
-            
-                                    dojo.attr(
-                                        tr, "sortkey", hold.usr.display_name
-                                    );
-                                    place_by_sortkey(tr, target);
-                                }
-                            );
-                        },
-                        "oncomplete": function() {
-                            progress_dialog.hide();
-                            if (any)
-                                window.print();
-                            else
-                                alert(dojo.byId("no_results").innerHTML);
-                        }
-                    }
-                );
-            }
-
             function my_init() {
-                var cgi = new CGI();
-                var ses = (typeof ses == "function" ? ses() : 0) ||
+                cgi = new CGI();
+                authtoken = (typeof ses == "function" ? ses() : 0) ||
                     cgi.param("ses") || dojo.cookie("ses");
-                var user = new openils.User({"authtoken": ses});
 
                 if (cgi.param("do") == "shelf_expired_holds") {
-                    do_shelf_expired_holds(user, cgi);
+                    dojo.byId("clear_holds_launcher").onclick = function() {
+                        if (confirm("Are you sure you're ready to clear the expired holds from the shelf?")) { /* XXX i18n */
+                            do_clear_holds(cgi);
+                        }
+                    };
+                    openils.Util.show("clear_holds_deck");
                 } else {
                     dojo.query("[only='shelf_expired_holds']").forEach(dojo.destroy);
-                    do_pull_list(user, cgi);
+                    do_pull_list(cgi);
                 }
             }
             dojo.addOnLoad(my_init);
     </head>
     <body class='tundra'>
 
-        <div dojoType="openils.widget.ProgressDialog" jsId="progress_dialog"></div>
+        <div style="width: 320px;"
+            dojoType="openils.widget.ProgressDialog"
+            jsId="progress_dialog"></div>
         <div class="hide_me" id="no_results">No results</div>
+        <div class="hide_me" id="clear_holds_deck">
+            [ <a id="clear_holds_launcher"
+                href="javascript:void(0);">Clear expired holds</a> ]
+            <small><em id="clear_holds_set_label"></em></small>
+        </div>
 <!-- START OF TEMPLATE SECTION -->
-
         <table>
-            <tbody id='target'>
+            <thead>
                 <tr>
                     <th only="shelf_expired_holds">Patron</th>
+                    <th only="shelf_expired_holds">Action</th>
                     <th>Title</th>
                     <th>Author</th>
                     <th>Shelving Location</th>
                     <th>Call Number</th>
                     <th>Barcode</th>
                 </tr>
+            </thead>
+            <tbody id='target'>
             </tbody>
             <tbody id='template' class='hide_me'>
                 <tr>
                     <td only="shelf_expired_holds">${usr.display_name}</td>
+                    <td only="shelf_expired_holds">${action}</td>
                     <td type='opac/slot-data' query='datafield[tag=245]'></td>
                     <td type='opac/slot-data' query='datafield[tag^=1]' limit='1'> </td>
                     <td>${current_copy.location.name}</td>
                 </tr>
             </tbody>
         </table>
-
 <!-- END OF TEMPLATE SECTION -->
-
-
     </body>
 </html>
diff --git a/Open-ILS/web/opac/extras/circ/alt_holds_print.js b/Open-ILS/web/opac/extras/circ/alt_holds_print.js
new file mode 100644 (file)
index 0000000..f4ea1c3
--- /dev/null
@@ -0,0 +1,202 @@
+dojo.require("dojo.cookie");
+dojo.require("dojox.xml.parser");
+dojo.require("openils.BibTemplate");
+dojo.require("openils.widget.ProgressDialog");
+
+var authtoken;
+var cgi;
+
+function do_pull_list() {
+    progress_dialog.show(true);
+
+    var any = false;
+
+    fieldmapper.standardRequest(
+        ['open-ils.circ','open-ils.circ.hold_pull_list.print.stream'],
+        { async : true,
+          params: [
+            authtoken, {
+              org_id     : cgi.param('o'),
+              limit      : cgi.param('limit'),
+              offset     : cgi.param('offset'),
+              chunk_size : cgi.param('chunk_size'),
+              sort       : sort_order
+            }
+          ],
+          onresponse : function (r) {
+            any = true;
+            dojo.forEach( openils.Util.readResponse(r), function (hold_fm) {
+
+                // hashify the hold
+                var hold = hold_fm.toHash(true);
+                hold.usr = hold_fm.usr().toHash(true);
+                hold.usr.card = hold_fm.usr().card().toHash(true);
+                hold.current_copy = hold_fm.current_copy().toHash(true);
+                hold.current_copy.location = hold_fm.current_copy().location().toHash(true);
+                hold.current_copy.call_number = hold_fm.current_copy().call_number().toHash(true);
+                hold.current_copy.call_number.record = hold_fm.current_copy().call_number().record().toHash(true);
+
+                // clone the template's html
+                var tr = dojo.clone(
+                    dojo.query("tr", dojo.byId('template'))[0]
+                );
+                dojo.query("td:not([type])", tr).forEach(
+                    function(td) {
+                        td.innerHTML =
+                            dojo.string.substitute(td.innerHTML, hold);
+                    }
+                );
+
+                new openils.BibTemplate({
+                    root : tr,
+                    xml  : dojox.xml.parser.parse(hold.current_copy.call_number.record.marc),
+                    delay: false
+                });
+
+                dojo.place(tr, "target");
+            });
+          },
+          oncomplete : function () {
+            progress_dialog.hide();
+            if (any)
+                window.print();
+            else
+                alert(dojo.byId("no_results").innerHTML);
+          }
+        }
+    );
+}
+
+function place_by_sortkey(node, container) {
+    /*Don't use a forEach() or anything like that here. too slow.*/
+    var sortkey = dojo.attr(node, "sortkey");
+    for (var i = 0; i < container.childNodes.length; i++) {
+        var rover = container.childNodes[i];
+        if (rover.nodeType != 1) continue;
+        if (dojo.attr(rover, "sortkey") > sortkey) {
+            dojo.place(node, rover, "before");
+            return;
+        }
+    }
+    dojo.place(node, container, "last");
+}
+
+function hashify_fields(fields) {
+    var hold  = {
+        "usr": {},
+        "current_copy": {
+            "barcode": fields.barcode,
+            "call_number": {
+                "label": fields.label,
+                "record": {"marc": fields.marc}
+            },
+            "location": {"name": fields.name}
+        }
+    };
+
+    if (fields.alias) {
+        hold.usr.display_name = fields.alias;
+    } else {
+        hold.usr.display_name = [
+            (fields.family_name ? fields.family_name : ""),
+            (fields.first_given_name ? fields.first_given_name : ""),
+            (fields.second_given_name ? fields.second_given_name : "")
+        ].join(" ");
+    }
+
+    ["first_given_name","second_given_name","family_name","alias"].forEach(
+        function(k) { hold.usr[k] = fields[k]; }
+    );
+
+    return hold;
+}
+
+function do_clear_holds() {
+    progress_dialog.show(true);
+
+    var launcher;
+    fieldmapper.standardRequest(
+        ["open-ils.circ", "open-ils.circ.hold.clear_shelf.process"], {
+            "async": true,
+            "params": [authtoken, cgi.param("o")],
+            "onresponse": function(r) {
+                if (r = openils.Util.readResponse(r)) {
+                    if (r.cache_key) { /* complete */
+                        launcher = dojo.byId("clear_holds_launcher");
+                        launcher.innerHTML = "Re-fetch for Printing"; /* XXX i18n */
+                        launcher.onclick =
+                            function() { do_clear_holds_from_cache(r.cache_key); };
+                        dojo.byId("clear_holds_set_label").innerHTML = r.cache_key;
+                    } else if (r.maximum) {
+                        progress_dialog.update(r);
+                    }
+                }
+            },
+            "oncomplete": function() {
+                progress_dialog.hide();
+                if (launcher) launcher.onclick();
+                else alert(dojo.byId("no_results").innerHTML);
+            }
+        }
+    );
+}
+
+function do_clear_holds_from_cache(cache_key) {
+    progress_dialog.show(true);
+
+    var any = false;
+    var target = dojo.byId("target");
+    dojo.empty(target);
+    var template = dojo.query("tr", dojo.byId("template"))[0];
+    fieldmapper.standardRequest(
+        ["open-ils.circ",
+            "open-ils.circ.hold.clear_shelf.get_cache"], {
+            "async": true,
+            "params": [authtoken, cache_key, cgi.param("chunk_size")],
+            "onresponse": function(r) {
+                dojo.forEach(
+                    openils.Util.readResponse(r),
+                    function(resp) {
+                        if (resp.maximum) {
+                            progress_dialog.update(resp);
+                            return;
+                        }
+
+                        var hold = hashify_fields(resp.hold_details);
+                        hold.action = resp.action;
+
+                        var tr = dojo.clone(template);
+                        any = true;
+
+                        dojo.query("td:not([type])", tr).forEach(
+                            function(td) {
+                                td.innerHTML =
+                                    dojo.string.substitute(td.innerHTML, hold);
+                            }
+                        );
+
+                        new openils.BibTemplate({
+                            "root": tr,
+                            "xml": dojox.xml.parser.parse(
+                                hold.current_copy.call_number.record.marc
+                            ),
+                            "delay": false
+                        });
+
+                        dojo.attr(tr, "sortkey", hold.usr.display_name);
+                        place_by_sortkey(tr, target);
+                        progress_dialog.update({"progress": 1});
+                    }
+                );
+            },
+            "oncomplete": function() {
+                progress_dialog.hide();
+                if (any)
+                    window.print();
+                else
+                    alert(dojo.byId("no_results").innerHTML);
+            }
+        }
+    );
+}
+