From: Bill Erickson Date: Tue, 7 Jun 2016 21:32:14 +0000 (-0400) Subject: hold targeter reify experiment X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=78a0599629fa7c6394de37d5fbe135e60649dfdf;p=working%2FEvergreen.git hold targeter reify experiment Signed-off-by: Bill Erickson --- diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm b/Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm new file mode 100644 index 0000000000..792732d741 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm @@ -0,0 +1,299 @@ +package OpenILS::Utils::HoldTargeter; +use strict; +use warnings; +use DateTime; +use OpenSRF::AppSession; +use OpenSRF::Utils::Logger qw(:logger); +use OpenILS::Application::AppUtils; +use OpenILS::Utils::CStoreEditor qw/:funcs/; + +my $U = "OpenILS::Application::AppUtils"; +my $dt_parser = DateTime::Format::ISO8601->new; + +# avoid 'duplicate key value violates unique constraint "copy_once_per_hold"' +# cache org unit settings per run +# reduce memory requirements? +# speed +# -- better up-front copy filtering when finding potential copies + +sub new { + my $class = shift; + my $self = { + editor => new_editor(), + ou_setting_cache => {} + }; + return bless($self, $class); +} + +# Pre-fetch necessary data. +sub init { + my $self = shift; + + $self->{org_closed_dates} = + $self->{editor}->search_actor_org_unit_closed_date({ + close_start => {'<=', 'now'}, + close_end => {'>=', 'now'} + }); +} + +# Org unit setting fetch+cache +sub get_ou_setting { + my ($self, $org_id, $setting) = @_; + my $c = $self->{ou_setting_cache}; + + $c->{$org_id} = {} unless $c->{$org_id}; + + if (not exists $c->{$org_id}->{$setting}) { + my $r = $U->ou_ancestor_setting($org_id, $setting, $self->{editor}); + $c->{$org_id}->{$setting} = $r ? $r->{value} : undef; + } + + return $c->{$org_id}->{$setting}; +} + +# TODO: stream messages back to caller +sub exit_targeter { + my ($self, $msg) = @_; + $self->{editor}->rollback; # no-op if commit already occurred. + my $hold_id = $self->{hold_id}; + $logger->info("targeter: exiting hold targeter for $hold_id : $msg"); + return 0; +} + +# Cancel expired holds and kick off the A/T no-target event. +# Returns true if the hold is expired or an error occured. +# False otherwise. +sub handle_expired_hold { + my $self = shift; + my $hold = $self->{hold}; + + return 0 unless $hold->expire_time; + + my $ex_time = + $dt_parser->parse_datetime(cleanse_ISO8601($hold->expire_time)); + return 0 unless DateTime->compare($ex_time, DateTime->now) < 0; + + # Hold is expired + + $hold->cancel_time('now'); + $hold->cancel_cause(1); # == un-targeted expiration + + if (!$self->{editor}->update_action_hold_request($hold)) { + $self->exit_targeter("Error canceling hold"); + return 1; # Tell the caller to exit + } + + $self->{editor}->commit; + + # Fire the A/T handler, but don't wait for a response. + OpenSRF::AppSession->create('open-ils.trigger')->request( + 'open-ils.trigger.event.autocreate', + 'hold_request.cancel.expire_no_target', + $hold, $hold->pickup_lib + ); + + return 1; +} + +# Hold copy maps are only automatically deleted when the hold +# is fulfilled or canceled. Here, they have to be manually removed. +# Returns event on error, undef on success. +sub remove_copy_maps { + my $self = shift; + my $e = $self->{editor}; + my $prev_maps = + $e->search_action_hold_copy_map({hold => $self->{hold_id}}); + for my $map (@$prev_maps) { + $e->delete_action_hold_copy_map($map) or return $e->die_event; + } + return undef; +} + +sub get_hold_copies { + my $self = shift; + my $e = $self->{editor}; + my $hold = $self->{hold}; + + my $hold_target = $hold->target; + my $hold_type = $hold->hold_type; + my $org_unit = $hold->selection_ou; + my $org_depth = $hold->selection_depth || 0; + + my $query = { + select => {acp => ['id']}, + from => {acp => {}}, + where => { + '+acp' => { + deleted => 'f', + circ_lib => { + in => { + select => { + aou => [{ + transform => 'actor.org_unit_descendants', + column => 'id', + result_field => 'id', + params => [$org_depth] + }], + }, + from => 'aou', + where => {id => $org_unit} + } + }, + } + } + }; + + unless ($hold_type eq 'R' || $hold_type eq 'F') { + # Add the holdability filters to the copy query, unless + # we're processing a Recall or Force hold, which bypass most + # holdability checks. + + $query->{from}->{acp} = { + acpl => { + field => 'id', + filter => {holdable => 't', deleted => 'f'}, + fkey => 'location' + }, + ccs => { + field => 'id', + filter => {holdable => 't'}, + fkey => 'status' + } + }; + + $query->{where}->{'+acp'}->{circulate} = 't'; + $query->{where}->{'+acp'}->{holdable} = 't'; + $query->{where}->{'+acp'}->{mint_condition} = 't' + if $U->is_true($hold->mint_condition); + } + + unless ($hold_type eq 'C' || $hold_type eq 'I' || $hold_type eq 'P') { + # TODO: Should this include 'R' holds? The original hold targeter + # does not include them either. + # For volume and higher level holds, avoid targeting copies that + # act as instances of monograph parts. + + $query->{from}->{acp}->{acpm} = { + type => 'left', + field => 'target_copy', + fkey => 'id' + }; + + $query->{where}->{'+acpm'}->{id} = undef; + } + + if ($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') { + + $query->{where}->{'+acp'}->{id} = $hold_target; + + } elsif ($hold_type eq 'V') { + + $query->{where}->{'+acp'}->{call_number} = $hold_target; + + } elsif ($hold_type eq 'P') { + + $query->{from}->{acp}->{acpm} = { + field => 'target_copy', + fkey => 'id', + filter => {part => $hold_target}, + }; + + } elsif ($hold_type eq 'I') { + + $query->{from}->{acp}->{sitem} = { + field => 'unit', + fkey => 'id', + filter => {issuance => $hold_target}, + }; + + } elsif ($hold_type eq 'T') { + + $query->{from}->{acp}->{acn} = { + field => 'id', + fkey => 'call_number', + 'join' => { + bre => { + field => 'id', + filter => {id => $hold_target}, + fkey => 'record' + } + } + }; + + } else { + + $query->{from}->{acp}->{acn} = { + field => 'id', + fkey => 'call_number', + join => { + bre => { + field => 'id', + fkey => 'record', + join => { + mmrsm => { + field => 'source', + fkey => 'id', + filter => {metarecord => $hold_target}, + } + } + } + } + }; + } + + my $res = $e->json_query($query); + return map {$_->{id}} @$res; +} + +sub handle_no_copies { + my $self = shift; + my $e = $self->{editor}; + my $hold = $self->{hold}; + + $hold->prev_check_time('now'); + $hold->clear_current_copy; + + if (!$e->update_action_hold_request($hold)) { + my $evt = $e->die_event; + return $self->exit_targeter( + "Error updating hold request: ".$evt->{textcode}); + } + + $e->commit; + return $self->exit_targeter("No copies available for targeting"); +} + + +# Targets a single hold request +sub target_hold { + my ($self, $hold_id) = @_; + my $e = $self->{editor}; + $self->{hold_id} = $hold_id; + + $e->xact_begin; + + my $hold = $self->{hold} = + $e->retrieve_action_hold_request($hold_id) + or return $self->exit_targeter("No hold found"); + + return $self->exit_targeter("Hold is not eligible for targeting") + if $hold->capture_time || $hold->cancel_time; + + # Remove any existing hold copy maps so they can be replaced. + my $evt = $self->remove_copy_maps; + return $self->exit_targeter( + "Error deleting copy maps: ".$evt->{textcode}) if $evt; + + return $self->exit_targeter("Hold is expired") + if $self->handle_expired_hold; + + my @copy_ids = $self->get_hold_copies; + + return $self->handle_no_copies unless @copy_ids; + + $logger->info("targeter: Hold $hold_id has ". + scalar(@copy_ids)." potential copies"); +} + + + diff --git a/Open-ILS/src/support-scripts/test-scripts/hold_targeter.pl b/Open-ILS/src/support-scripts/test-scripts/hold_targeter.pl new file mode 100755 index 0000000000..54fa8635f2 --- /dev/null +++ b/Open-ILS/src/support-scripts/test-scripts/hold_targeter.pl @@ -0,0 +1,20 @@ +#!/usr/bin/perl +#---------------------------------------------------------------- +# Simple cstore example +#---------------------------------------------------------------- + +require '../oils_header.pl'; +use strict; use warnings; +use OpenSRF::AppSession; +use OpenILS::Utils::Fieldmapper; +use OpenILS::Utils::HoldTargeter; + +my $config = shift; # path to opensrf_core.xml +osrf_connect($config); # connect to jabber + +my $hold_id = shift; + +my $targeter = OpenILS::Utils::HoldTargeter->new; +$targeter->init; +$targeter->target_hold($hold_id); +