From: senator <senator@dcc99617-32d9-48b4-a31d-7c20da2025e4> Date: Thu, 7 Oct 2010 22:37:45 +0000 (+0000) Subject: Expired holds shelf printer needs to be a holds shelf *clearer* and printer X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=6b95ceb01414552816045b820def4db40fb70158;p=evergreen%2Fmasslnc.git Expired holds shelf printer needs to be a holds shelf *clearer* and printer 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 --- diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm b/Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm index ebdaf5332a..97cb623fda 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/Circ/Holds.pm @@ -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 diff --git a/Open-ILS/web/opac/extras/circ/alt_holds_print.html b/Open-ILS/web/opac/extras/circ/alt_holds_print.html index 6cde9e2595..849b3692b0 100644 --- a/Open-ILS/web/opac/extras/circ/alt_holds_print.html +++ b/Open-ILS/web/opac/extras/circ/alt_holds_print.html @@ -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; @@ -44,180 +40,23 @@ <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); @@ -225,24 +64,34 @@ </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> @@ -251,9 +100,6 @@ </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 index 0000000000..f4ea1c3387 --- /dev/null +++ b/Open-ILS/web/opac/extras/circ/alt_holds_print.js @@ -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); + } + } + ); +} +