hold targeter reify experiment
authorBill Erickson <berickxx@gmail.com>
Wed, 8 Jun 2016 19:21:22 +0000 (15:21 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 8 Jun 2016 19:21:22 +0000 (15:21 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm
Open-ILS/src/perlmods/lib/OpenILS/Utils/PermitHold.pm

index 7167e28..9375d67 100644 (file)
@@ -71,6 +71,7 @@ use OpenSRF::AppSession;
 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) = @_;
@@ -172,10 +173,6 @@ sub exit_targeter {
     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.
@@ -448,7 +445,27 @@ sub build_copy_maps {
     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.
@@ -510,18 +527,6 @@ sub inspect_previous_target {
 
     $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;
 
@@ -530,19 +535,23 @@ sub inspect_previous_target {
 
     $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;
@@ -558,6 +567,85 @@ sub check_no_copies {
     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) = @_;
@@ -584,9 +672,20 @@ sub target {
     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;
 }
 
 
index f2153a8..8fae5a7 100644 (file)
@@ -34,15 +34,19 @@ sub indb_hold_permit {
         ref($$params{patron}) ? $$params{patron}->id : $$params{patron_id};
     my $request_lib = 
         ref($$params{request_lib}) ? $$params{request_lib}->id : $$params{request_lib};
+    my $copy_id = 
+        ref($$params{copy}) ? $$params{copy}->id : $$params{copy_id};
+    my $requestor_id = 
+        ref($$params{requestor}) ? $$params{requestor}->id : $$params{requestor_id};
 
     my $HOLD_TEST = {
         from => [
             $function,
             $$params{pickup_lib}, 
             $request_lib,
-            $$params{copy}->id, 
+            $copy_id,
             $patron_id,
-            $$params{requestor}->id 
+            $requestor_id
         ]
     };