use OpenSRF::Utils::Logger qw(:logger);
use OpenILS::Application::AppUtils;
use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Utils::PermitHold;
sub new {
my ($class, %args) = @_;
return 0;
}
-sub create_prox_list {
-
-}
-
# Cancel expired holds and kick off the A/T no-target event. Returns
# true (i.e. keep going) if the hold is not expired. Returns false if
# the hold is canceled or a non-recoverable error occcurred.
return 1;
}
-sub build_copy_prox_list {
+# Returns a map of proximity values to arrays of copy hashes.
+# The copy hash arrays are weighted consistent with the org unit hold
+# target weight, meaning that a given copy may appear more than once
+# in its proximity list.
+sub compile_weighted_proximity_map {
+ my $self = shift;
+
+ my %prox_map;
+ for my $copy_hash (@{$self->copy_hashes}) {
+ my $prox = $self->copy_proximity_map->{$copy_hash->{id}};
+ $prox_map{$prox} ||= [];
+
+ my $weight = $self->parent->get_ou_setting(
+ $copy_hash->{circ_lib},
+ 'circ.holds.org_unit_target_weight') || 1;
+
+ # Each copy is added to the list once per target weight.
+ push(@{$prox_map{$prox}}, $copy_hash) foreach (1 .. $weight);
+ }
+
+ return $self->{weighted_prox_map} = \%prox_map;
}
# Returns true if filtering completed without error, false otherwise.
$self->{had_previous_copy} = 1;
- # TODO: is this step really necessary here?? Not if we always set
- # or clear the value later. Confirm.
- # Clear the previous copy regardless of
- # whether we can use it again later.
- $self->clear_current_copy;
-
- if (!$self->editor->update_action_hold_request($hold)) {
- my $evt = $self->editor->die_event;
- return $self->exit_targeter(
- "Error updating hold request: ".$evt->{textcode}, 1);
- }
-
# See if the previous copy is in our list of valid copies.
my ($prev) = grep {$_->{id} eq $prev_id} @copies;
$self->{valid_previous_copy} = $prev;
- # Remove the previous copy from the working set of potential copies
- # if there are other copies we can focus on. If there are no other
- # copies, treat the previous copy like any other.
+ # If there are other copies we can focus on first, remove the
+ # previous copy from the working set of potential copies. (The
+ # previous copy may be revisited later as needed). Otherwise,
+ # treat the previous copy as the only potential copy.
$self->copy_hashes([grep {$_->{id} ne $prev_id} @copies])
if scalar(@copies) > 1;
return 1;
}
-sub check_no_copies {
+# Returns true if we have at least one potential copy, thus targeting
+# should continue. Otherwise, the hold is updated to reflect that
+# there is no target and returns false, to stop targeting.
+sub handle_no_copies {
my $self = shift;
- return 1 if @{$self->copy_hashes};
+ return 1 if @{$self->copy_hashes} || $self->{valid_previous_copy};
my $hold = $self->hold;
$hold->clear_current_copy;
return $self->exit_targeter("Hold has no targetable copies");
}
+# Force and recall holds bypass validity tests. Returns the first
+# (and presumably only) copy in our list of valid copies when a
+# F or R hold is encountered. Returns undef otherwise.
+sub attempt_force_recall_target {
+ my $self = shift;
+ return $self->copy_hashes->[0] if
+ $self->hold->hold_type eq 'R' || $self->hold->hold_type = 'F';
+ return undef;
+}
+
+sub attempt_min_proxy_copy_target {
+ my $self = shift;
+}
+
+sub attempt_remote_copy_target {
+ my $self = shift;
+}
+
+sub attempt_prev_copy_retarget {
+ my $self = shift;
+
+ $self->{valid_previous_copy} = undef;
+}
+
+# Returns the closest copy by proximity that is a confirmed valid
+# targetable copy.
+sub find_nearest_copy {
+ my $self = shift;
+ my %prox_map = %{$self->{weighted_prox_map}};
+ my $hold = $self->hold;
+
+ for my $prox (sort {$a <=> $b} keys %prox_map) {
+ my @copies = @{$prox_map{$prox}};
+ next unless @copies;
+
+ my $rand = int(rand(scalar(@copies)));
+ my %seen = ();
+
+ while (my ($c) = splice(@copies, $rand, 1)) {
+ next if $seen{$c->{id}};
+
+ return $c if OpenILS::Utils::PermitHold::permit_copy_hold({
+ patron_id => $hold->usr,
+ copy_id => $c->{id},
+ requestor => $hold->requestor,
+ request_lib => $hold->request_lib,
+ pickup_lib => $hold->pickup_lib,
+ retarget => 1
+ });
+
+ $seen{$c->{id}} = 1;
+
+ last unless(@copies);
+ $rand = int(rand(scalar(@copies)));
+ }
+ }
+
+ return undef;
+}
+
+sub apply_copy_target {
+ my ($self, $copy) = @_;
+ my $e = $self->editor;
+ my $hold = $self->hold;
+
+ $hold->current_copy($copy->{id});
+ $hold->prev_check_time('now');
+
+ if (!$e->update_action_hold_request($hold)) {
+ my $evt = $self->editor->die_event;
+ return $self->exit_targeter(
+ "Error updating hold request: ".$evt->{textcode}, 1);
+ }
+
+ $e->commit;
+ $self->{success} = 1;
+ return $self->exit_targeter("Hold successfully targeted");
+}
+
# Target a single hold request
sub target {
my ($self, $hold_id) = @_;
return unless $self->inspect_previous_target;
return unless $self->filter_copies_by_status;
return unless $self->filter_closed_date_copies;
- return unless $self->check_no_copies;
+ return unless $self->handle_no_copies;
+ return unless $self->compile_weighted_proximity_map;
- $e->commit;
+ my $copy = $self->attempt_force_recall_target ||
+ $self->attempt_min_prox_copy_target ||
+ $self->attempt_remote_copy_target ||
+ $self->attempt_prev_copy_retarget;
+
+ return $self->apply_copy_target($copy) if $copy;
+
+ # No targetable copy was found. Remove he copy data and fire the
+ # no-copy handler to update the hold accordingly.
+ $self->copy_hashes([]);
+ $self->handle_no_copies;
}