Merge branch 'master' of git+ssh://yeti.esilibrary.com/home/evergreen/evergreen-equin...
authorBill Erickson <erickson@esilibrary.com>
Mon, 31 Jan 2011 14:54:05 +0000 (09:54 -0500)
committerBill Erickson <erickson@esilibrary.com>
Mon, 31 Jan 2011 14:54:05 +0000 (09:54 -0500)
1  2 
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGWeb.pm

index 0000000,c95bafe..f2a2006
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,3356 +1,3363 @@@
 -      my($self, $client, $auth, $hold_id) = @_;
+ # ---------------------------------------------------------------
+ # Copyright (C) 2005  Georgia Public Library Service 
+ # Bill Erickson <highfalutin@gmail.com>
+ # This program is free software; you can redistribute it and/or
+ # modify it under the terms of the GNU General Public License
+ # as published by the Free Software Foundation; either version 2
+ # of the License, or (at your option) any later version.
+ # This program is distributed in the hope that it will be useful,
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ # GNU General Public License for more details.
+ # ---------------------------------------------------------------
+ package OpenILS::Application::Circ::Holds;
+ use base qw/OpenILS::Application/;
+ use strict; use warnings;
+ use OpenILS::Application::AppUtils;
+ use DateTime;
+ use Data::Dumper;
+ use OpenSRF::EX qw(:try);
+ use OpenILS::Perm;
+ use OpenILS::Event;
+ use OpenSRF::Utils;
+ use OpenSRF::Utils::Logger qw(:logger);
+ use OpenILS::Utils::CStoreEditor q/:funcs/;
+ use OpenILS::Utils::PermitHold;
+ use OpenSRF::Utils::SettingsClient;
+ use OpenILS::Const qw/:const/;
+ use OpenILS::Application::Circ::Transit;
+ 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;
+ __PACKAGE__->register_method(
+     method    => "create_hold_batch",
+     api_name  => "open-ils.circ.holds.create.batch",
+     stream => 1,
+     signature => {
+         desc => q/@see open-ils.circ.holds.create.batch/,
+         params => [
+             { desc => 'Authentication token', type => 'string' },
+             { desc => 'Array of hold objects', type => 'array' }
+         ],
+         return => {
+             desc => 'Array of hold ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
+         },
+     }
+ );
+ __PACKAGE__->register_method(
+     method    => "create_hold_batch",
+     api_name  => "open-ils.circ.holds.create.override.batch",
+     stream => 1,
+     signature => {
+         desc  => '@see open-ils.circ.holds.create.batch',
+     }
+ );
+ sub create_hold_batch {
+       my( $self, $conn, $auth, $hold_list ) = @_;
+     (my $method = $self->api_name) =~ s/\.batch//og;
+     foreach (@$hold_list) {
+         my ($res) = $self->method_lookup($method)->run($auth, $_);
+         $conn->respond($res);
+     }
+     return undef;
+ }
+ __PACKAGE__->register_method(
+     method    => "create_hold",
+     api_name  => "open-ils.circ.holds.create",
+     signature => {
+         desc => "Create a new hold for an item.  From a permissions perspective, " .
+                 "the login session is used as the 'requestor' of the hold.  "      . 
+                 "The hold recipient is determined by the 'usr' setting within the hold object. " .
+                 'First we verify the requestor has holds request permissions.  '         .
+                 'Then we verify that the recipient is allowed to make the given hold.  ' .
+                 'If not, we see if the requestor has "override" capabilities.  If not, ' .
+                 'a permission exception is returned.  If permissions allow, we cycle '   .
+                 'through the set of holds objects and create.  '                         .
+                 'If the recipient does not have permission to place multiple holds '     .
+                 'on a single title and said operation is attempted, a permission '       .
+                 'exception is returned',
+         params => [
+             { desc => 'Authentication token',               type => 'string' },
+             { desc => 'Hold object for hold to be created',
+                 type => 'object', class => 'ahr' }
+         ],
+         return => {
+             desc => 'New ahr ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
+         },
+     }
+ );
+ __PACKAGE__->register_method(
+     method    => "create_hold",
+     api_name  => "open-ils.circ.holds.create.override",
+     notes     => '@see open-ils.circ.holds.create',
+     signature => {
+         desc  => "If the recipient is not allowed to receive the requested hold, " .
+                  "call this method to attempt the override",
+         params => [
+             { desc => 'Authentication token',               type => 'string' },
+             {
+                 desc => 'Hold object for hold to be created',
+                 type => 'object', class => 'ahr'
+             }
+         ],
+         return => {
+             desc => 'New hold (ahr) ID on success, -1 on missing arg, event (or ref to array of events) on error(s)',
+         },
+     }
+ );
+ sub create_hold {
+       my( $self, $conn, $auth, $hold ) = @_;
+     return -1 unless $hold;
+       my $e = new_editor(authtoken=>$auth, xact=>1);
+       return $e->die_event unless $e->checkauth;
+       my $override = 1 if $self->api_name =~ /override/;
+     my @events;
+     my $requestor = $e->requestor;
+     my $recipient = $requestor;
+     if( $requestor->id ne $hold->usr ) {
+         # Make sure the requestor is allowed to place holds for 
+         # the recipient if they are not the same people
+         $recipient = $e->retrieve_actor_user($hold->usr)  or return $e->die_event;
+         $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->die_event;
+     }
+     # If the related org setting tells us to, block if patron privs have expired
+     my $expire_setting = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_BLOCK_HOLD_FOR_EXPIRED_PATRON);
+     if ($expire_setting) {
+         my $expire = DateTime::Format::ISO8601->new->parse_datetime(
+             cleanse_ISO8601($recipient->expire_date));
+         push( @events, OpenILS::Event->new(
+             'PATRON_ACCOUNT_EXPIRED',
+             "payload" => {"fail_part" => "actor.usr.privs_expired"}
+             )) if( CORE::time > $expire->epoch ) ;
+     }
+     # Now make sure the recipient is allowed to receive the specified hold
+     my $porg = $recipient->home_ou;
+     my $rid  = $e->requestor->id;
+     my $t    = $hold->hold_type;
+     # See if a duplicate hold already exists
+     my $sargs = {
+         usr                   => $recipient->id, 
+         hold_type     => $t, 
+         fulfillment_time => undef, 
+         target                => $hold->target,
+         cancel_time   => undef,
+     };
+     $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
+         
+     my $existing = $e->search_action_hold_request($sargs); 
+     push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
+     my $checked_out = hold_item_is_checked_out($e, $recipient->id, $hold->hold_type, $hold->target);
+     push( @events, OpenILS::Event->new('HOLD_ITEM_CHECKED_OUT')) if $checked_out;
+     if ( $t eq OILS_HOLD_TYPE_METARECORD ) {
+         return $e->die_event unless $e->allowed('MR_HOLDS',     $porg);
+     } elsif ( $t eq OILS_HOLD_TYPE_TITLE ) {
+         return $e->die_event unless $e->allowed('TITLE_HOLDS',  $porg);
+     } elsif ( $t eq OILS_HOLD_TYPE_VOLUME ) {
+         return $e->die_event unless $e->allowed('VOLUME_HOLDS', $porg);
+     } elsif ( $t eq OILS_HOLD_TYPE_ISSUANCE ) {
+         return $e->die_event unless $e->allowed('ISSUANCE_HOLDS', $porg);
+     } elsif ( $t eq OILS_HOLD_TYPE_COPY ) {
+         return $e->die_event unless $e->allowed('COPY_HOLDS',   $porg);
+     } elsif ( $t eq OILS_HOLD_TYPE_FORCE ) {
+         return $e->die_event unless $e->allowed('COPY_HOLDS',   $porg);
+     } elsif ( $t eq OILS_HOLD_TYPE_RECALL ) {
+         return $e->die_event unless $e->allowed('COPY_HOLDS',   $porg);
+     }
+     if( @events ) {
+         if (!$override) {
+             $e->rollback;
+             return \@events;
+         }
+         for my $evt (@events) {
+             next unless $evt;
+             my $name = $evt->{textcode};
+             return $e->die_event unless $e->allowed("$name.override", $porg);
+         }
+     }
+     # set the configured expire time
+     unless($hold->expire_time) {
+         my $interval = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_HOLD_EXPIRE);
+         if($interval) {
+             my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
+             $hold->expire_time($U->epoch2ISO8601($date->epoch));
+         }
+     }
+     $hold->requestor($e->requestor->id); 
+     $hold->request_lib($e->requestor->ws_ou);
+     $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
+     $hold = $e->create_action_hold_request($hold) or return $e->die_event;
+       $e->commit;
+       $conn->respond_complete($hold->id);
+     $U->storagereq(
+         'open-ils.storage.action.hold_request.copy_targeter', 
+         undef, $hold->id ) unless $U->is_true($hold->frozen);
+       return undef;
+ }
+ # makes sure that a user has permission to place the type of requested hold
+ # returns the Perm exception if not allowed, returns undef if all is well
+ sub _check_holds_perm {
+       my($type, $user_id, $org_id) = @_;
+       my $evt;
+       if ($type eq "M") {
+               $evt = $apputils->check_perms($user_id, $org_id, "MR_HOLDS"    );
+       } elsif ($type eq "T") {
+               $evt = $apputils->check_perms($user_id, $org_id, "TITLE_HOLDS" );
+       } elsif($type eq "V") {
+               $evt = $apputils->check_perms($user_id, $org_id, "VOLUME_HOLDS");
+       } elsif($type eq "C") {
+               $evt = $apputils->check_perms($user_id, $org_id, "COPY_HOLDS"  );
+       }
+     return $evt if $evt;
+       return undef;
+ }
+ # tests if the given user is allowed to place holds on another's behalf
+ sub _check_request_holds_perm {
+       my $user_id = shift;
+       my $org_id  = shift;
+       if (my $evt = $apputils->check_perms(
+               $user_id, $org_id, "REQUEST_HOLDS")) {
+               return $evt;
+       }
+ }
+ my $ses_is_req_note = 'The login session is the requestor.  If the requestor is different from the user, ' .
+                       'then the requestor must have VIEW_HOLD permissions';
+ __PACKAGE__->register_method(
+     method    => "retrieve_holds_by_id",
+     api_name  => "open-ils.circ.holds.retrieve_by_id",
+     signature => {
+         desc   => "Retrieve the hold, with hold transits attached, for the specified ID.  $ses_is_req_note",
+         params => [
+             { desc => 'Authentication token', type => 'string' },
+             { desc => 'Hold ID',              type => 'number' }
+         ],
+         return => {
+             desc => 'Hold object with transits attached, event on error',
+         }
+     }
+ );
+ sub retrieve_holds_by_id {
+       my($self, $client, $auth, $hold_id) = @_;
+       my $e = new_editor(authtoken=>$auth);
+       $e->checkauth or return $e->event;
+       $e->allowed('VIEW_HOLD') or return $e->event;
+       my $holds = $e->search_action_hold_request(
+               [
+                       { id =>  $hold_id , fulfillment_time => undef }, 
+                       { 
+                 order_by => { ahr => "request_time" },
+                 flesh => 1,
+                 flesh_fields => {ahr => ['notes']}
+             }
+               ]
+       );
+       flesh_hold_transits($holds);
+       flesh_hold_notices($holds, $e);
+       return $holds;
+ }
+ __PACKAGE__->register_method(
+     method    => "retrieve_holds",
+     api_name  => "open-ils.circ.holds.retrieve",
+     signature => {
+         desc   => "Retrieves all the holds, with hold transits attached, for the specified user.  $ses_is_req_note",
+         params => [
+             { desc => 'Authentication token', type => 'string'  },
+             { desc => 'User ID',              type => 'integer' }
+         ],
+         return => {
+             desc => 'list of holds, event on error',
+         }
+    }
+ );
+ __PACKAGE__->register_method(
+     method        => "retrieve_holds",
+     api_name      => "open-ils.circ.holds.id_list.retrieve",
+     authoritative => 1,
+     signature     => {
+         desc   => "Retrieves all the hold IDs, for the specified user.  $ses_is_req_note",
+         params => [
+             { desc => 'Authentication token', type => 'string'  },
+             { desc => 'User ID',              type => 'integer' }
+         ],
+         return => {
+             desc => 'list of holds, event on error',
+         }
+    }
+ );
+ __PACKAGE__->register_method(
+     method        => "retrieve_holds",
+     api_name      => "open-ils.circ.holds.canceled.retrieve",
+     authoritative => 1,
+     signature     => {
+         desc   => "Retrieves all the cancelled holds for the specified user.  $ses_is_req_note",
+         params => [
+             { desc => 'Authentication token', type => 'string'  },
+             { desc => 'User ID',              type => 'integer' }
+         ],
+         return => {
+             desc => 'list of holds, event on error',
+         }
+    }
+ );
+ __PACKAGE__->register_method(
+     method        => "retrieve_holds",
+     api_name      => "open-ils.circ.holds.canceled.id_list.retrieve",
+     authoritative => 1,
+     signature     => {
+         desc   => "Retrieves list of cancelled hold IDs for the specified user.  $ses_is_req_note",
+         params => [
+             { desc => 'Authentication token', type => 'string'  },
+             { desc => 'User ID',              type => 'integer' }
+         ],
+         return => {
+             desc => 'list of hold IDs, event on error',
+         }
+    }
+ );
+ sub retrieve_holds {
+     my ($self, $client, $auth, $user_id) = @_;
+     my $e = new_editor(authtoken=>$auth);
+     return $e->event unless $e->checkauth;
+     $user_id = $e->requestor->id unless defined $user_id;
+     my $notes_filter = {staff => 'f'};
+     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
+     unless($user_id == $e->requestor->id) {
+         if($e->allowed('VIEW_HOLD', $user->home_ou)) {
+             $notes_filter = {staff => 't'}
+         } else {
+             my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
+                 $e, $user_id, $e->requestor->id, 'hold.view');
+             return $e->event unless $allowed;
+         }
+     } else {
+         # staff member looking at his/her own holds can see staff and non-staff notes
+         $notes_filter = {} if $e->allowed('VIEW_HOLD', $user->home_ou);
+     }
+     my $holds_query = {
+         select => {ahr => ['id']},
+         from => 'ahr', 
+         where => {usr => $user_id, fulfillment_time => undef}
+     };
+     if($self->api_name =~ /canceled/) {
+         # Fetch the canceled holds
+         # order cancelled holds by cancel time, most recent first
+         $holds_query->{order_by} = [{class => 'ahr', field => 'cancel_time', direction => 'desc'}];
+         my $cancel_age;
+         my $cancel_count = $U->ou_ancestor_setting_value(
+                 $e->requestor->ws_ou, 'circ.holds.canceled.display_count', $e);
+         unless($cancel_count) {
+             $cancel_age = $U->ou_ancestor_setting_value(
+                 $e->requestor->ws_ou, 'circ.holds.canceled.display_age', $e);
+             # if no settings are defined, default to last 10 cancelled holds
+             $cancel_count = 10 unless $cancel_age;
+         }
+         if($cancel_count) { # limit by count
+             $holds_query->{where}->{cancel_time} = {'!=' => undef};
+             $holds_query->{limit} = $cancel_count;
+         } elsif($cancel_age) { # limit by age
+             # find all of the canceled holds that were canceled within the configured time frame
+             my $date = DateTime->now->subtract(seconds => OpenSRF::Utils::interval_to_seconds($cancel_age));
+             $date = $U->epoch2ISO8601($date->epoch);
+             $holds_query->{where}->{cancel_time} = {'>=' => $date};
+         }
+     } else {
+         # order non-cancelled holds by ready-for-pickup, then active, followed by suspended
+         $holds_query->{order_by} = {ahr => ['shelf_time', 'frozen', 'request_time']};
+         $holds_query->{where}->{cancel_time} = undef;
+     }
+     my $hold_ids = $e->json_query($holds_query);
+     $hold_ids = [ map { $_->{id} } @$hold_ids ];
+     return $hold_ids if $self->api_name =~ /id_list/;
+     my @holds;
+     for my $hold_id ( @$hold_ids ) {
+         my $hold = $e->retrieve_action_hold_request($hold_id);
+         $hold->notes($e->search_action_hold_request_note({hold => $hold_id, %$notes_filter}));
+         $hold->transit(
+             $e->search_action_hold_transit_copy([
+                 {hold => $hold->id},
+                 {order_by => {ahtc => 'source_send_time desc'}, limit => 1}])->[0]
+         );
+         push(@holds, $hold);
+     }
+     return \@holds;
+ }
+ __PACKAGE__->register_method(
+     method   => 'user_hold_count',
+     api_name => 'open-ils.circ.hold.user.count'
+ );
+ sub user_hold_count {
+     my ( $self, $conn, $auth, $userid ) = @_;
+     my $e = new_editor( authtoken => $auth );
+     return $e->event unless $e->checkauth;
+     my $patron = $e->retrieve_actor_user($userid)
+         or return $e->event;
+     return $e->event unless $e->allowed( 'VIEW_HOLD', $patron->home_ou );
+     return __user_hold_count( $self, $e, $userid );
+ }
+ sub __user_hold_count {
+     my ( $self, $e, $userid ) = @_;
+     my $holds = $e->search_action_hold_request(
+         {
+             usr              => $userid,
+             fulfillment_time => undef,
+             cancel_time      => undef,
+         },
+         { idlist => 1 }
+     );
+     return scalar(@$holds);
+ }
+ __PACKAGE__->register_method(
+     method   => "retrieve_holds_by_pickup_lib",
+     api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
+     notes    => 
+       "Retrieves all the holds, with hold transits attached, for the specified pickup_ou id."
+ );
+ __PACKAGE__->register_method(
+     method   => "retrieve_holds_by_pickup_lib",
+     api_name => "open-ils.circ.holds.id_list.retrieve_by_pickup_lib",
+     notes    => "Retrieves all the hold ids for the specified pickup_ou id. "
+ );
+ sub retrieve_holds_by_pickup_lib {
+     my ($self, $client, $login_session, $ou_id) = @_;
+     #FIXME -- put an appropriate permission check here
+     #my( $user, $target, $evt ) = $apputils->checkses_requestor(
+     # $login_session, $user_id, 'VIEW_HOLD' );
+     #return $evt if $evt;
+       my $holds = $apputils->simplereq(
+               'open-ils.cstore',
+               "open-ils.cstore.direct.action.hold_request.search.atomic",
+               { 
+                       pickup_lib =>  $ou_id , 
+                       fulfillment_time => undef,
+                       cancel_time => undef
+               }, 
+               { order_by => { ahr => "request_time" } }
+     );
+     if ( ! $self->api_name =~ /id_list/ ) {
+         flesh_hold_transits($holds);
+         return $holds;
+     }
+     # else id_list
+     return [ map { $_->id } @$holds ];
+ }
+ __PACKAGE__->register_method(
+     method   => "uncancel_hold",
+     api_name => "open-ils.circ.hold.uncancel"
+ );
+ sub uncancel_hold {
+       my($self, $client, $auth, $hold_id) = @_;
+       my $e = new_editor(authtoken=>$auth, xact=>1);
+       return $e->die_event unless $e->checkauth;
+       my $hold = $e->retrieve_action_hold_request($hold_id)
+               or return $e->die_event;
+     return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
+     if ($hold->fulfillment_time) {
+         $e->rollback;
+         return 0;
+     }
+     unless ($hold->cancel_time) {
+         $e->rollback;
+         return 1;
+     }
+     # if configured to reset the request time, also reset the expire time
+     if($U->ou_ancestor_setting_value(
+         $hold->request_lib, 'circ.holds.uncancel.reset_request_time', $e)) {
+         $hold->request_time('now');
+         my $interval = $U->ou_ancestor_setting_value($hold->request_lib, OILS_SETTING_HOLD_EXPIRE);
+         if($interval) {
+             my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
+             $hold->expire_time($U->epoch2ISO8601($date->epoch));
+         }
+     }
+     $hold->clear_cancel_time;
+     $hold->clear_cancel_cause;
+     $hold->clear_cancel_note;
+     $e->update_action_hold_request($hold) or return $e->die_event;
+     $e->commit;
+     $U->storagereq('open-ils.storage.action.hold_request.copy_targeter', undef, $hold_id);
+     return 1;
+ }
+ __PACKAGE__->register_method(
+     method    => "cancel_hold",
+     api_name  => "open-ils.circ.hold.cancel",
+     signature => {
+         desc   => 'Cancels the specified hold.  The login session is the requestor.  If the requestor is different from the usr field ' .
+                   'on the hold, the requestor must have CANCEL_HOLDS permissions. The hold may be either the hold object or the hold id',
+         param  => [
+             {desc => 'Authentication token',  type => 'string'},
+             {desc => 'Hold ID',               type => 'number'},
+             {desc => 'Cause of Cancellation', type => 'string'},
+             {desc => 'Note',                  type => 'string'}
+         ],
+         return => {
+             desc => '1 on success, event on error'
+         }
+     }
+ );
+ sub cancel_hold {
+       my($self, $client, $auth, $holdid, $cause, $note) = @_;
+       my $e = new_editor(authtoken=>$auth, xact=>1);
+       return $e->die_event unless $e->checkauth;
+       my $hold = $e->retrieve_action_hold_request($holdid)
+               or return $e->die_event;
+       if( $e->requestor->id ne $hold->usr ) {
+               return $e->die_event unless $e->allowed('CANCEL_HOLDS');
+       }
+       if ($hold->cancel_time) {
+         $e->rollback;
+         return 1;
+     }
+       # If the hold is captured, reset the copy status
+       if( $hold->capture_time and $hold->current_copy ) {
+               my $copy = $e->retrieve_asset_copy($hold->current_copy)
+                       or return $e->die_event;
+               if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
+          $logger->info("canceling hold $holdid whose item is on the holds shelf");
+ #                     $logger->info("setting copy to status 'reshelving' on hold cancel");
+ #                     $copy->status(OILS_COPY_STATUS_RESHELVING);
+ #                     $copy->editor($e->requestor->id);
+ #                     $copy->edit_date('now');
+ #                     $e->update_asset_copy($copy) or return $e->event;
+               } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
+                       my $hid = $hold->id;
+                       $logger->warn("! canceling hold [$hid] that is in transit");
+                       my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
+                       if( $transid ) {
+                               my $trans = $e->retrieve_action_transit_copy($transid);
+                               # Leave the transit alive, but  set the copy status to 
+                               # reshelving so it will be properly reshelved when it gets back home
+                               if( $trans ) {
+                                       $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
+                                       $e->update_action_transit_copy($trans) or return $e->die_event;
+                               }
+                       }
+               }
+       }
+       $hold->cancel_time('now');
+     $hold->cancel_cause($cause);
+     $hold->cancel_note($note);
+       $e->update_action_hold_request($hold)
+               or return $e->die_event;
+       delete_hold_copy_maps($self, $e, $hold->id);
+       $e->commit;
+     $U->create_events_for_hook('hold_request.cancel.staff', $hold, $hold->pickup_lib)
+         if $e->requestor->id != $hold->usr;
+       return 1;
+ }
+ sub delete_hold_copy_maps {
+       my $class  = shift;
+       my $editor = shift;
+       my $holdid = shift;
+       my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
+       for(@$maps) {
+               $editor->delete_action_hold_copy_map($_) 
+                       or return $editor->event;
+       }
+       return undef;
+ }
+ my $update_hold_desc = 'The login session is the requestor. '       .
+    'If the requestor is different from the usr field on the hold, ' .
+    'the requestor must have UPDATE_HOLDS permissions. '             .
+    'If supplying a hash of hold data, "id" must be included. '      .
+    'The hash is ignored if a hold object is supplied, '             .
+    'so you should supply only one kind of hold data argument.'      ;
+ __PACKAGE__->register_method(
+     method    => "update_hold",
+     api_name  => "open-ils.circ.hold.update",
+     signature => {
+         desc   => "Updates the specified hold.  $update_hold_desc",
+         params => [
+             {desc => 'Authentication token',         type => 'string'},
+             {desc => 'Hold Object',                  type => 'object'},
+             {desc => 'Hash of values to be applied', type => 'object'}
+         ],
+         return => {
+             desc => 'Hold ID on success, event on error',
+             # type => 'number'
+         }
+     }
+ );
+ __PACKAGE__->register_method(
+     method    => "batch_update_hold",
+     api_name  => "open-ils.circ.hold.update.batch",
+     stream    => 1,
+     signature => {
+         desc   => "Updates the specified hold(s).  $update_hold_desc",
+         params => [
+             {desc => 'Authentication token',                    type => 'string'},
+             {desc => 'Array of hold obejcts',                   type => 'array' },
+             {desc => 'Array of hashes of values to be applied', type => 'array' }
+         ],
+         return => {
+             desc => 'Hold ID per success, event per error',
+         }
+     }
+ );
+ sub update_hold {
+       my($self, $client, $auth, $hold, $values) = @_;
+     my $e = new_editor(authtoken=>$auth, xact=>1);
+     return $e->die_event unless $e->checkauth;
+     my $resp = update_hold_impl($self, $e, $hold, $values);
+     if ($U->event_code($resp)) {
+         $e->rollback;
+         return $resp;
+     }
+     $e->commit;     # FIXME: update_hold_impl already does $e->commit  ??
+     return $resp;
+ }
+ sub batch_update_hold {
+       my($self, $client, $auth, $hold_list, $values_list) = @_;
+     my $e = new_editor(authtoken=>$auth);
+     return $e->die_event unless $e->checkauth;
+     my $count = ($hold_list) ? scalar(@$hold_list) : scalar(@$values_list);     # FIXME: we don't know for sure that we got $values_list.  we could have neither list.
+     $hold_list   ||= [];
+     $values_list ||= [];      # FIXME: either move this above $count declaration, or send an event if both lists undef.  Probably the latter.
+ # FIXME: Failing over to [] guarantees warnings for "Use of unitialized value" in update_hold_impl call.
+ # FIXME: We should be sure we only call update_hold_impl with hold object OR hash, not both.
+     for my $idx (0..$count-1) {
+         $e->xact_begin;
+         my $resp = update_hold_impl($self, $e, $hold_list->[$idx], $values_list->[$idx]);
+         $e->xact_commit unless $U->event_code($resp);
+         $client->respond($resp);
+     }
+     $e->disconnect;
+     return undef;       # not in the register return type, assuming we should always have at least one list populated
+ }
+ sub update_hold_impl {
+     my($self, $e, $hold, $values) = @_;
+     unless($hold) {
+         $hold = $e->retrieve_action_hold_request($values->{id})
+             or return $e->die_event;
+         for my $k (keys %$values) {
+             if (defined $values->{$k}) {
+                 $hold->$k($values->{$k});
+             } else {
+                 my $f = "clear_$k"; $hold->$f();
+             }
+         }
+     }
+     my $orig_hold = $e->retrieve_action_hold_request($hold->id)
+         or return $e->die_event;
+     # don't allow the user to be changed
+     return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
+     if($hold->usr ne $e->requestor->id) {
+         # if the hold is for a different user, make sure the 
+         # requestor has the appropriate permissions
+         my $usr = $e->retrieve_actor_user($hold->usr)
+             or return $e->die_event;
+         return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
+     }
+     # --------------------------------------------------------------
+     # Changing the request time is like playing God
+     # --------------------------------------------------------------
+     if($hold->request_time ne $orig_hold->request_time) {
+         return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
+         return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
+     }
+     # --------------------------------------------------------------
+     # if the hold is on the holds shelf or in transit and the pickup 
+     # lib changes we need to create a new transit.
+     # --------------------------------------------------------------
+     if($orig_hold->pickup_lib ne $hold->pickup_lib) {
+         my $status = _hold_status($e, $hold);
+         if($status == 3) { # in transit
+             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $orig_hold->pickup_lib);
+             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $hold->pickup_lib);
+             $logger->info("updating pickup lib for hold ".$hold->id." while already in transit");
+             # update the transit to reflect the new pickup location
+                       my $transit = $e->search_action_hold_transit_copy(
+                 {hold=>$hold->id, dest_recv_time => undef})->[0] 
+                 or return $e->die_event;
+             $transit->prev_dest($transit->dest); # mark the previous destination on the transit
+             $transit->dest($hold->pickup_lib);
+             $e->update_action_hold_transit_copy($transit) or return $e->die_event;
+         } elsif($status == 4) { # on holds shelf
+             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
+             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
+             $logger->info("updating pickup lib for hold ".$hold->id." while on holds shelf");
+             # create the new transit
+             my $evt = transit_hold($e, $orig_hold, $hold, $e->retrieve_asset_copy($hold->current_copy));
+             return $evt if $evt;
+         }
+     } 
+     update_hold_if_frozen($self, $e, $hold, $orig_hold);
+     $e->update_action_hold_request($hold) or return $e->die_event;
+     $e->commit;
+     # a change to mint-condition changes the set of potential copies, so retarget the hold;
+     if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
+         _reset_hold($self, $e->requestor, $hold) 
+     }
+     return $hold->id;
+ }
+ sub transit_hold {
+     my($e, $orig_hold, $hold, $copy) = @_;
+     my $src  = $orig_hold->pickup_lib;
+     my $dest = $hold->pickup_lib;
+     $logger->info("putting hold into transit on pickup_lib update");
+     my $transit = Fieldmapper::action::hold_transit_copy->new;
+     $transit->hold($hold->id);
+     $transit->source($src);
+     $transit->dest($dest);
+     $transit->target_copy($copy->id);
+     $transit->source_send_time('now');
+     $transit->copy_status(OILS_COPY_STATUS_ON_HOLDS_SHELF);
+     $copy->status(OILS_COPY_STATUS_IN_TRANSIT);
+     $copy->editor($e->requestor->id);
+     $copy->edit_date('now');
+     $e->create_action_hold_transit_copy($transit) or return $e->die_event;
+     $e->update_asset_copy($copy) or return $e->die_event;
+     return undef;
+ }
+ # if the hold is frozen, this method ensures that the hold is not "targeted", 
+ # that is, it clears the current_copy and prev_check_time to essentiallly 
+ # reset the hold.  If it is being activated, it runs the targeter in the background
+ sub update_hold_if_frozen {
+     my($self, $e, $hold, $orig_hold) = @_;
+     return if $hold->capture_time;
+     if($U->is_true($hold->frozen)) {
+         $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
+         $hold->clear_current_copy;
+         $hold->clear_prev_check_time;
+     } else {
+         if($U->is_true($orig_hold->frozen)) {
+             $logger->info("Running targeter on activated hold ".$hold->id);
+             $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
+         }
+     }
+ }
+ __PACKAGE__->register_method(
+     method    => "hold_note_CUD",
+     api_name  => "open-ils.circ.hold_request.note.cud",
+     signature => {
+         desc   => 'Create, update or delete a hold request note.  If the operator (from Auth. token) '
+                 . 'is not the owner of the hold, the UPDATE_HOLD permission is required',
+         params => [
+             { desc => 'Authentication token', type => 'string' },
+             { desc => 'Hold note object',     type => 'object' }
+         ],
+         return => {
+             desc => 'Returns the note ID, event on error'
+         },
+     }
+ );
+ sub hold_note_CUD {
+       my($self, $conn, $auth, $note) = @_;
+     my $e = new_editor(authtoken => $auth, xact => 1);
+     return $e->die_event unless $e->checkauth;
+     my $hold = $e->retrieve_action_hold_request($note->hold)
+         or return $e->die_event;
+     if($hold->usr ne $e->requestor->id) {
+         my $usr = $e->retrieve_actor_user($hold->usr);
+         return $e->die_event unless $e->allowed('UPDATE_HOLD', $usr->home_ou);
+         $note->staff('t') if $note->isnew;
+     }
+     if($note->isnew) {
+         $e->create_action_hold_request_note($note) or return $e->die_event;
+     } elsif($note->ischanged) {
+         $e->update_action_hold_request_note($note) or return $e->die_event;
+     } elsif($note->isdeleted) {
+         $e->delete_action_hold_request_note($note) or return $e->die_event;
+     }
+     $e->commit;
+     return $note->id;
+ }
+ __PACKAGE__->register_method(
+     method    => "retrieve_hold_status",
+     api_name  => "open-ils.circ.hold.status.retrieve",
+     signature => {
+         desc   => 'Calculates the current status of the hold. The requestor must have '      .
+                   'VIEW_HOLD permissions if the hold is for a user other than the requestor' ,
+         param  => [
+             { desc => 'Hold ID', type => 'number' }
+         ],
+         return => {
+             # type => 'number',     # event sometimes
+             desc => <<'END_OF_DESC'
+ Returns event on error or:
+ -1 on error (for now),
+  1 for 'waiting for copy to become available',
+  2 for 'waiting for copy capture',
+  3 for 'in transit',
+  4 for 'arrived',
+  5 for 'hold-shelf-delay'
+  6 for 'canceled'
+ END_OF_DESC
+         }
+     }
+ );
+ sub retrieve_hold_status {
+       my($self, $client, $auth, $hold_id) = @_;
+       my $e = new_editor(authtoken => $auth);
+       return $e->event unless $e->checkauth;
+       my $hold = $e->retrieve_action_hold_request($hold_id)
+               or return $e->event;
+       if( $e->requestor->id != $hold->usr ) {
+               return $e->event unless $e->allowed('VIEW_HOLD');
+       }
+       return _hold_status($e, $hold);
+ }
+ sub _hold_status {
+       my($e, $hold) = @_;
+     if ($hold->cancel_time) {
+         return 6;
+     }
+       return 1 unless $hold->current_copy;
+       return 2 unless $hold->capture_time;
+       my $copy = $hold->current_copy;
+       unless( ref $copy ) {
+               $copy = $e->retrieve_asset_copy($hold->current_copy)
+                       or return $e->event;
+       }
+       return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
+       if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
+         my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
+         return 4 unless $hs_wait_interval;
+         # if a hold_shelf_status_delay interval is defined and start_time plus 
+         # the interval is greater than now, consider the hold to be in the virtual 
+         # "on its way to the holds shelf" status. Return 5.
+         my $transit    = $e->search_action_hold_transit_copy({hold => $hold->id})->[0];
+         my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
+         $start_time    = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time));
+         my $end_time   = $start_time->add(seconds => OpenSRF::Utils::interval_to_seconds($hs_wait_interval));
+         return 5 if $end_time > DateTime->now;
+         return 4;
+     }
+     return -1;  # error
+ }
+ __PACKAGE__->register_method(
+     method    => "retrieve_hold_queue_stats",
+     api_name  => "open-ils.circ.hold.queue_stats.retrieve",
+     signature => {
+         desc   => 'Returns summary data about the state of a hold',
+         params => [
+             { desc => 'Authentication token',  type => 'string'},
+             { desc => 'Hold ID', type => 'number'},
+         ],
+         return => {
+             desc => q/Summary object with keys: 
+                 total_holds : total holds in queue
+                 queue_position : current queue position
+                 potential_copies : number of potential copies for this hold
+                 estimated_wait : estimated wait time in days
+                 status : hold status  
+                      -1 => error or unexpected state,
+                      1 => 'waiting for copy to become available',
+                      2 => 'waiting for copy capture',
+                      3 => 'in transit',
+                      4 => 'arrived',
+                      5 => 'hold-shelf-delay'
+             /,
+             type => 'object'
+         }
+     }
+ );
+ sub retrieve_hold_queue_stats {
+     my($self, $conn, $auth, $hold_id) = @_;
+       my $e = new_editor(authtoken => $auth);
+       return $e->event unless $e->checkauth;
+       my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
+       if($e->requestor->id != $hold->usr) {
+               return $e->event unless $e->allowed('VIEW_HOLD');
+       }
+     return retrieve_hold_queue_status_impl($e, $hold);
+ }
+ sub retrieve_hold_queue_status_impl {
+     my $e = shift;
+     my $hold = shift;
+     # The holds queue is defined as the distinct set of holds that share at 
+     # least one potential copy with the context hold, plus any holds that
+     # share the same hold type and target.  The latter part exists to
+     # accomodate holds that currently have no potential copies
+     my $q_holds = $e->json_query({
+         # fetch cut_in_line and request_time since they're in the order_by
+         # and we're asking for distinct values
+         select => {ahr => ['id', 'cut_in_line', 'request_time']},
+         from   => { ahr => 'ahcm' },
+         order_by => [
+             {
+                 "class" => "ahr",
+                 "field" => "cut_in_line",
+                 "transform" => "coalesce",
+                 "params" => [ 0 ],
+                 "direction" => "desc"
+             },
+             { "class" => "ahr", "field" => "request_time" }
+         ],
+         distinct => 1,
+         where => {
+             '+ahcm' => {
+                 target_copy => {
+                     in => {
+                         select => {ahcm => ['target_copy']},
+                         from   => 'ahcm',
+                         where  => {hold => $hold->id}
+                     } 
+                 } 
+             }
+         }
+     });
+     if (!@$q_holds) { # none? maybe we don't have a map ... 
+         $q_holds = $e->json_query({
+             select => {ahr => ['id', 'cut_in_line', 'request_time']},
+             from   => 'ahr',
+             order_by => [
+                 {
+                     "class" => "ahr",
+                     "field" => "cut_in_line",
+                     "transform" => "coalesce",
+                     "params" => [ 0 ],
+                     "direction" => "desc"
+                 },
+                 { "class" => "ahr", "field" => "request_time" }
+             ],
+             where    => {
+                 hold_type => $hold->hold_type, 
+                 target    => $hold->target 
+            } 
+         });
+     }
+     my $qpos = 1;
+     for my $h (@$q_holds) {
+         last if $h->{id} == $hold->id;
+         $qpos++;
+     }
+     my $hold_data = $e->json_query({
+         select => {
+             acp => [ {column => 'id', transform => 'count', aggregate => 1, alias => 'count'} ],
+             ccm => [ {column =>'avg_wait_time'} ]
+         }, 
+         from => {
+             ahcm => {
+                 acp => {
+                     join => {
+                         ccm => {type => 'left'}
+                     }
+                 }
+             }
+         }, 
+         where => {'+ahcm' => {hold => $hold->id} }
+     });
+     my $user_org = $e->json_query({select => {au => ['home_ou']}, from => 'au', where => {id => $hold->usr}})->[0]->{home_ou};
+     my $default_wait = $U->ou_ancestor_setting_value($user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL);
+     my $min_wait = $U->ou_ancestor_setting_value($user_org, 'circ.holds.min_estimated_wait_interval');
+     $min_wait = OpenSRF::Utils::interval_to_seconds($min_wait || '0 seconds');
+     $default_wait ||= '0 seconds';
+     # Estimated wait time is the average wait time across the set 
+     # of potential copies, divided by the number of potential copies
+     # times the queue position.  
+     my $combined_secs = 0;
+     my $num_potentials = 0;
+     for my $wait_data (@$hold_data) {
+         my $count += $wait_data->{count};
+         $combined_secs += $count * 
+             OpenSRF::Utils::interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
+         $num_potentials += $count;
+     }
+     my $estimated_wait = -1;
+     if($num_potentials) {
+         my $avg_wait = $combined_secs / $num_potentials;
+         $estimated_wait = $qpos * ($avg_wait / $num_potentials);
+         $estimated_wait = $min_wait if $estimated_wait < $min_wait and $estimated_wait != -1;
+     }
+     return {
+         total_holds      => scalar(@$q_holds),
+         queue_position   => $qpos,
+         potential_copies => $num_potentials,
+         status           => _hold_status( $e, $hold ),
+         estimated_wait   => int($estimated_wait)
+     };
+ }
+ sub fetch_open_hold_by_current_copy {
+       my $class = shift;
+       my $copyid = shift;
+       my $hold = $apputils->simplereq(
+               'open-ils.cstore', 
+               'open-ils.cstore.direct.action.hold_request.search.atomic',
+               { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
+       return $hold->[0] if ref($hold);
+       return undef;
+ }
+ sub fetch_related_holds {
+       my $class = shift;
+       my $copyid = shift;
+       return $apputils->simplereq(
+               'open-ils.cstore', 
+               'open-ils.cstore.direct.action.hold_request.search.atomic',
+               { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
+ }
+ __PACKAGE__->register_method(
+     method    => "hold_pull_list",
+     api_name  => "open-ils.circ.hold_pull_list.retrieve",
+     signature => {
+         desc   => 'Returns (reference to) a list of holds that need to be "pulled" by a given location. ' .
+                   'The location is determined by the login session.',
+         params => [
+             { desc => 'Limit (optional)',  type => 'number'},
+             { desc => 'Offset (optional)', type => 'number'},
+         ],
+         return => {
+             desc => 'reference to a list of holds, or event on failure',
+         }
+     }
+ );
+ __PACKAGE__->register_method(
+     method    => "hold_pull_list",
+     api_name  => "open-ils.circ.hold_pull_list.id_list.retrieve",
+     signature => {
+         desc   => 'Returns (reference to) a list of holds IDs that need to be "pulled" by a given location. ' .
+                   'The location is determined by the login session.',
+         params => [
+             { desc => 'Limit (optional)',  type => 'number'},
+             { desc => 'Offset (optional)', type => 'number'},
+         ],
+         return => {
+             desc => 'reference to a list of holds, or event on failure',
+         }
+     }
+ );
+ __PACKAGE__->register_method(
+     method    => "hold_pull_list",
+     api_name  => "open-ils.circ.hold_pull_list.retrieve.count",
+     signature => {
+         desc   => 'Returns a count of holds that need to be "pulled" by a given location. ' .
+                   'The location is determined by the login session.',
+         params => [
+             { desc => 'Limit (optional)',  type => 'number'},
+             { desc => 'Offset (optional)', type => 'number'},
+         ],
+         return => {
+             desc => 'Holds count (integer), or event on failure',
+             # type => 'number'
+         }
+     }
+ );
+ sub hold_pull_list {
+       my( $self, $conn, $authtoken, $limit, $offset ) = @_;
+       my( $reqr, $evt ) = $U->checkses($authtoken);
+       return $evt if $evt;
+       my $org = $reqr->ws_ou || $reqr->home_ou;
+       # the perm locaiton shouldn't really matter here since holds
+       # will exist all over and VIEW_HOLDS should be universal
+       $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
+       return $evt if $evt;
+     if($self->api_name =~ /count/) {
+               my $count = $U->storagereq(
+                       'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
+                       $org, $limit, $offset ); 
+         $logger->info("Grabbing pull list for org unit $org with $count items");
+         return $count;
+     } elsif( $self->api_name =~ /id_list/ ) {
+               return $U->storagereq(
+                       'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
+                       $org, $limit, $offset ); 
+       } else {
+               return $U->storagereq(
+                       'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
+                       $org, $limit, $offset ); 
+       }
+ }
+ __PACKAGE__->register_method(
+     method    => "print_hold_pull_list",
+     api_name  => "open-ils.circ.hold_pull_list.print",
+     signature => {
+         desc   => 'Returns an HTML-formatted holds pull list',
+         params => [
+             { desc => 'Authtoken', type => 'string'},
+             { desc => 'Org unit ID.  Optional, defaults to workstation org unit', type => 'number'},
+         ],
+         return => {
+             desc => 'HTML string',
+             type => 'string'
+         }
+     }
+ );
+ sub print_hold_pull_list {
+     my($self, $client, $auth, $org_id) = @_;
+     my $e = new_editor(authtoken=>$auth);
+     return $e->event unless $e->checkauth;
+     $org_id = (defined $org_id) ? $org_id : $e->requestor->ws_ou;
+     return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
+     my $hold_ids = $U->storagereq(
+         'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
+         $org_id, 10000);
+     return undef unless @$hold_ids;
+     $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
+     # Holds will /NOT/ be in order after this ...
+     my $holds = $e->search_action_hold_request({id => $hold_ids}, {substream => 1});
+     $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
+     # ... so we must resort.
+     my $hold_map = +{map { $_->id => $_ } @$holds};
+     my $sorted_holds = [];
+     push @$sorted_holds, $hold_map->{$_} foreach @$hold_ids;
+     return $U->fire_object_event(
+         undef, "ahr.format.pull_list", $sorted_holds,
+         $org_id, undef, undef, $client
+     );
+ }
+ __PACKAGE__->register_method(
+     method    => "print_hold_pull_list_stream",
+     stream   => 1,
+     api_name  => "open-ils.circ.hold_pull_list.print.stream",
+     signature => {
+         desc   => 'Returns a stream of fleshed holds',
+         params => [
+             { desc => 'Authtoken', type => 'string'},
+             { desc => 'Hash of optional param: Org unit ID (defaults to workstation org unit), limit, offset, sort (array of: acplo.position, call_number, request_time)',
+               type => 'object'
+             },
+         ],
+         return => {
+             desc => 'A stream of fleshed holds',
+             type => 'object'
+         }
+     }
+ );
+ sub print_hold_pull_list_stream {
+     my($self, $client, $auth, $params) = @_;
+     my $e = new_editor(authtoken=>$auth);
+     return $e->die_event unless $e->checkauth;
+     delete($$params{org_id}) unless (int($$params{org_id}));
+     delete($$params{limit}) unless (int($$params{limit}));
+     delete($$params{offset}) unless (int($$params{offset}));
+     delete($$params{chunk_size}) unless (int($$params{chunk_size}));
+     delete($$params{chunk_size}) if  ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
+     $$params{chunk_size} ||= 10;
+     $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
+     return $e->die_event unless $e->allowed('VIEW_HOLD', $$params{org_id });
+     my $sort = [];
+     if ($$params{sort} && @{ $$params{sort} }) {
+         for my $s (@{ $$params{sort} }) {
+             if ($s eq 'acplo.position') {
+                 push @$sort, {
+                     "class" => "acplo", "field" => "position",
+                     "transform" => "coalesce", "params" => [999]
+                 };
+             } elsif ($s eq 'call_number') {
+                 push @$sort, {"class" => "acn", "field" => "label"};
+             } elsif ($s eq 'request_time') {
+                 push @$sort, {"class" => "ahr", "field" => "request_time"};
+             }
+         }
+     } else {
+         push @$sort, {"class" => "ahr", "field" => "request_time"};
+     }
+     my $holds_ids = $e->json_query(
+         {
+             "select" => {"ahr" => ["id"]},
+             "from" => {
+                 "ahr" => {
+                     "acp" => { 
+                         "field" => "id",
+                         "fkey" => "current_copy",
+                         "filter" => {
+                             "circ_lib" => $$params{org_id}, "status" => [0,7]
+                         },
+                         "join" => {
+                             "acn" => {
+                                 "field" => "id",
+                                 "fkey" => "call_number" 
+                             },
+                             "acplo" => {
+                                 "field" => "org",
+                                 "fkey" => "circ_lib", 
+                                 "type" => "left",
+                                 "filter" => {
+                                     "location" => {"=" => {"+acp" => "location"}}
+                                 }
+                             }
+                         }
+                     }
+                 }
+             },
+             "where" => {
+                 "+ahr" => {
+                     "capture_time" => undef,
+                     "cancel_time" => undef,
+                     "-or" => [
+                         {"expire_time" => undef },
+                         {"expire_time" => {">" => "now"}}
+                     ]
+                 }
+             },
+             (@$sort ? (order_by => $sort) : ()),
+             ($$params{limit} ? (limit => $$params{limit}) : ()),
+             ($$params{offset} ? (offset => $$params{offset}) : ())
+         }, {"substream" => 1}
+     ) or return $e->die_event;
+     $logger->info("about to stream back " . scalar(@$holds_ids) . " holds");
+     my @chunk;
+     for my $hid (@$holds_ids) {
+         push @chunk, $e->retrieve_action_hold_request([
+             $hid->{"id"}, {
+                 "flesh" => 3,
+                 "flesh_fields" => {
+                     "ahr" => ["usr", "current_copy"],
+                     "au"  => ["card"],
+                     "acp" => ["location", "call_number"],
+                     "acn" => ["record"]
+                 }
+             }
+         ]);
+         if (@chunk >= $$params{chunk_size}) {
+             $client->respond( \@chunk );
+             @chunk = ();
+         }
+     }
+     $client->respond_complete( \@chunk ) if (@chunk);
+     $e->disconnect;
+     return undef;
+ }
+ __PACKAGE__->register_method(
+     method        => 'fetch_hold_notify',
+     api_name      => 'open-ils.circ.hold_notification.retrieve_by_hold',
+     authoritative => 1,
+     signature     => q/ 
+ Returns a list of hold notification objects based on hold id.
+ @param authtoken The loggin session key
+ @param holdid The id of the hold whose notifications we want to retrieve
+ @return An array of hold notification objects, event on error.
+ /
+ );
+ sub fetch_hold_notify {
+       my( $self, $conn, $authtoken, $holdid ) = @_;
+       my( $requestor, $evt ) = $U->checkses($authtoken);
+       return $evt if $evt;
+       my ($hold, $patron);
+       ($hold, $evt) = $U->fetch_hold($holdid);
+       return $evt if $evt;
+       ($patron, $evt) = $U->fetch_user($hold->usr);
+       return $evt if $evt;
+       $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
+       return $evt if $evt;
+       $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
+       return $U->cstorereq(
+               'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
+ }
+ __PACKAGE__->register_method(
+     method    => 'create_hold_notify',
+     api_name  => 'open-ils.circ.hold_notification.create',
+     signature => q/
+ Creates a new hold notification object
+ @param authtoken The login session key
+ @param notification The hold notification object to create
+ @return ID of the new object on success, Event on error
+ /
+ );
+ sub create_hold_notify {
+    my( $self, $conn, $auth, $note ) = @_;
+    my $e = new_editor(authtoken=>$auth, xact=>1);
+    return $e->die_event unless $e->checkauth;
+    my $hold = $e->retrieve_action_hold_request($note->hold)
+       or return $e->die_event;
+    my $patron = $e->retrieve_actor_user($hold->usr) 
+       or return $e->die_event;
+    return $e->die_event unless 
+       $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
+    $note->notify_staff($e->requestor->id);
+    $e->create_action_hold_notification($note) or return $e->die_event;
+    $e->commit;
+    return $note->id;
+ }
+ __PACKAGE__->register_method(
+     method    => 'create_hold_note',
+     api_name  => 'open-ils.circ.hold_note.create',
+     signature => q/
+               Creates a new hold request note object
+               @param authtoken The login session key
+               @param note The hold note object to create
+               @return ID of the new object on success, Event on error
+               /
+ );
+ sub create_hold_note {
+    my( $self, $conn, $auth, $note ) = @_;
+    my $e = new_editor(authtoken=>$auth, xact=>1);
+    return $e->die_event unless $e->checkauth;
+    my $hold = $e->retrieve_action_hold_request($note->hold)
+       or return $e->die_event;
+    my $patron = $e->retrieve_actor_user($hold->usr) 
+       or return $e->die_event;
+    return $e->die_event unless 
+       $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn.  Probably want something more specific
+    $e->create_action_hold_request_note($note) or return $e->die_event;
+    $e->commit;
+    return $note->id;
+ }
+ __PACKAGE__->register_method(
+     method    => 'reset_hold',
+     api_name  => 'open-ils.circ.hold.reset',
+     signature => q/
+               Un-captures and un-targets a hold, essentially returning
+               it to the state it was in directly after it was placed,
+               then attempts to re-target the hold
+               @param authtoken The login session key
+               @param holdid The id of the hold
+       /
+ );
+ sub reset_hold {
+       my( $self, $conn, $auth, $holdid ) = @_;
+       my $reqr;
+       my ($hold, $evt) = $U->fetch_hold($holdid);
+       return $evt if $evt;
+       ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
+       return $evt if $evt;
+       $evt = _reset_hold($self, $reqr, $hold);
+       return $evt if $evt;
+       return 1;
+ }
+ __PACKAGE__->register_method(
+     method   => 'reset_hold_batch',
+     api_name => 'open-ils.circ.hold.reset.batch'
+ );
+ sub reset_hold_batch {
+     my($self, $conn, $auth, $hold_ids) = @_;
+     my $e = new_editor(authtoken => $auth);
+     return $e->event unless $e->checkauth;
+     for my $hold_id ($hold_ids) {
+         my $hold = $e->retrieve_action_hold_request(
+             [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}]) 
+             or return $e->event;
+           next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
+         _reset_hold($self, $e->requestor, $hold);
+     }
+     return 1;
+ }
+ sub _reset_hold {
+       my ($self, $reqr, $hold) = @_;
+       my $e = new_editor(xact =>1, requestor => $reqr);
+       $logger->info("reseting hold ".$hold->id);
+       my $hid = $hold->id;
+       if( $hold->capture_time and $hold->current_copy ) {
+               my $copy = $e->retrieve_asset_copy($hold->current_copy)
+                       or return $e->die_event;
+               if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
+                       $logger->info("setting copy to status 'reshelving' on hold retarget");
+                       $copy->status(OILS_COPY_STATUS_RESHELVING);
+                       $copy->editor($e->requestor->id);
+                       $copy->edit_date('now');
+                       $e->update_asset_copy($copy) or return $e->die_event;
+               } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
+                       # We don't want the copy to remain "in transit"
+                       $copy->status(OILS_COPY_STATUS_RESHELVING);
+                       $logger->warn("! reseting hold [$hid] that is in transit");
+                       my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
+                       if( $transid ) {
+                               my $trans = $e->retrieve_action_transit_copy($transid);
+                               if( $trans ) {
+                                       $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
+                                       my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
+                                       $logger->info("Transit abort completed with result $evt");
+                                       unless ("$evt" eq 1) {
+                         $e->rollback;
+                                           return $evt;
+                     }
+                               }
+                       }
+               }
+       }
+       $hold->clear_capture_time;
+       $hold->clear_current_copy;
+       $hold->clear_shelf_time;
+       $hold->clear_shelf_expire_time;
+       $e->update_action_hold_request($hold) or return $e->die_event;
+       $e->commit;
+       $U->storagereq(
+               'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
+       return undef;
+ }
+ __PACKAGE__->register_method(
+     method    => 'fetch_open_title_holds',
+     api_name  => 'open-ils.circ.open_holds.retrieve',
+     signature => q/
+               Returns a list ids of un-fulfilled holds for a given title id
+               @param authtoken The login session key
+               @param id the id of the item whose holds we want to retrieve
+               @param type The hold type - M, T, I, V, C, F, R
+       /
+ );
+ sub fetch_open_title_holds {
+       my( $self, $conn, $auth, $id, $type, $org ) = @_;
+       my $e = new_editor( authtoken => $auth );
+       return $e->event unless $e->checkauth;
+       $type ||= "T";
+       $org  ||= $e->requestor->ws_ou;
+ #     return $e->search_action_hold_request(
+ #             { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
+       # XXX make me return IDs in the future ^--
+       my $holds = $e->search_action_hold_request(
+               { 
+                       target                          => $id, 
+                       cancel_time                     => undef, 
+                       hold_type                       => $type, 
+                       fulfillment_time        => undef 
+               }
+       );
+       flesh_hold_transits($holds);
+       return $holds;
+ }
+ sub flesh_hold_transits {
+       my $holds = shift;
+       for my $hold ( @$holds ) {
+               $hold->transit(
+                       $apputils->simplereq(
+                               'open-ils.cstore',
+                               "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
+                               { hold => $hold->id },
+                               { order_by => { ahtc => 'id desc' }, limit => 1 }
+                       )->[0]
+               );
+       }
+ }
+ sub flesh_hold_notices {
+       my( $holds, $e ) = @_;
+       $e ||= new_editor();
+       for my $hold (@$holds) {
+               my $notices = $e->search_action_hold_notification(
+                       [
+                               { hold => $hold->id },
+                               { order_by => { anh => 'notify_time desc' } },
+                       ],
+                       {idlist=>1}
+               );
+               $hold->notify_count(scalar(@$notices));
+               if( @$notices ) {
+                       my $n = $e->retrieve_action_hold_notification($$notices[0])
+                               or return $e->event;
+                       $hold->notify_time($n->notify_time);
+               }
+       }
+ }
+ __PACKAGE__->register_method(
+     method    => 'fetch_captured_holds',
+     api_name  => 'open-ils.circ.captured_holds.on_shelf.retrieve',
+     stream    => 1,
+     signature => q/
+               Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
+               @param authtoken The login session key
+               @param org The org id of the location in question
+       /
+ );
+ __PACKAGE__->register_method(
+     method    => 'fetch_captured_holds',
+     api_name  => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
+     stream    => 1,
+     signature => q/
+               Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
+               @param authtoken The login session key
+               @param org The org id of the location in question
+       /
+ );
+ __PACKAGE__->register_method(
+     method    => 'fetch_captured_holds',
+     api_name  => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
+     stream    => 1,
+     signature => q/
+               Returns list ids of shelf-expired un-fulfilled holds for a given title id
+               @param authtoken The login session key
+               @param org The org id of the location in question
+       /
+ );
+ sub fetch_captured_holds {
+       my( $self, $conn, $auth, $org ) = @_;
+       my $e = new_editor(authtoken => $auth);
+       return $e->die_event unless $e->checkauth;
+       return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
+       $org ||= $e->requestor->ws_ou;
+     my $query = { 
+         select => { ahr => ['id'] },
+         from   => {
+             ahr => {
+                 acp => {
+                     field => 'id',
+                     fkey  => 'current_copy'
+                 },
+             }
+         }, 
+         where => {
+             '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
+             '+ahr' => {
+                 capture_time     => { "!=" => undef },
+                 current_copy     => { "!=" => undef },
+                 fulfillment_time => undef,
+                 pickup_lib       => $org,
+                 cancel_time      => undef,
+               }
+         }
+     };
+     if($self->api_name =~ /expired/) {
+         $query->{'where'}->{'+ahr'}->{'shelf_expire_time'} = {'<' => 'now'};
+         $query->{'where'}->{'+ahr'}->{'shelf_time'} = {'!=' => undef};
+     }
+     my $hold_ids = $e->json_query( $query );
+     for my $hold_id (@$hold_ids) {
+         if($self->api_name =~ /id_list/) {
+             $conn->respond($hold_id->{id});
+             next;
+         } else {
+             $conn->respond(
+                 $e->retrieve_action_hold_request([
+                     $hold_id->{id},
+                     {
+                         flesh => 1,
+                         flesh_fields => {ahr => ['notifications', 'transit', 'notes']},
+                         order_by => {anh => 'notify_time desc'}
+                     }
+                 ])
+             );
+         }
+     }
+     return undef;
+ }
+ __PACKAGE__->register_method(
+     method    => "print_expired_holds_stream",
+     api_name  => "open-ils.circ.captured_holds.expired.print.stream",
+     stream    => 1
+ );
+ sub print_expired_holds_stream {
+     my ($self, $client, $auth, $params) = @_;
+     # No need to check specific permissions: we're going to call another method
+     # that will do that.
+     my $e = new_editor("authtoken" => $auth);
+     return $e->die_event unless $e->checkauth;
+     delete($$params{org_id}) unless (int($$params{org_id}));
+     delete($$params{limit}) unless (int($$params{limit}));
+     delete($$params{offset}) unless (int($$params{offset}));
+     delete($$params{chunk_size}) unless (int($$params{chunk_size}));
+     delete($$params{chunk_size}) if  ($$params{chunk_size} && $$params{chunk_size} > 50); # keep the size reasonable
+     $$params{chunk_size} ||= 10;
+     $$params{org_id} = (defined $$params{org_id}) ? $$params{org_id}: $e->requestor->ws_ou;
+     my @hold_ids = $self->method_lookup(
+         "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
+     )->run($auth, $params->{"org_id"});
+     if (!@hold_ids) {
+         $e->disconnect;
+         return;
+     } elsif (defined $U->event_code($hold_ids[0])) {
+         $e->disconnect;
+         return $hold_ids[0];
+     }
+     $logger->info("about to stream back up to " . scalar(@hold_ids) . " expired holds");
+     while (@hold_ids) {
+         my @hid_chunk = splice @hold_ids, 0, $params->{"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"]
+             },
+             "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}}
+         }) or return $e->die_event;
+         $client->respond($result_chunk);
+     }
+     $e->disconnect;
+     undef;
+ }
+ __PACKAGE__->register_method(
+     method    => "check_title_hold_batch",
+     api_name  => "open-ils.circ.title_hold.is_possible.batch",
+     stream    => 1,
+     signature => {
+         desc  => '@see open-ils.circ.title_hold.is_possible.batch',
+         params => [
+             { desc => 'Authentication token',     type => 'string'},
+             { desc => 'Array of Hash of named parameters', type => 'array'},
+         ],
+         return => {
+             desc => 'Array of response objects',
+             type => 'array'
+         }
+     }
+ );
+ sub check_title_hold_batch {
+     my($self, $client, $authtoken, $param_list) = @_;
+     foreach (@$param_list) {
+         my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_);
+         $client->respond($res);
+     }
+     return undef;
+ }
+ __PACKAGE__->register_method(
+     method    => "check_title_hold",
+     api_name  => "open-ils.circ.title_hold.is_possible",
+     signature => {
+         desc  => 'Determines if a hold were to be placed by a given user, ' .
+              'whether or not said hold would have any potential copies to fulfill it.' .
+              'The named paramaters of the second argument include: ' .
+              'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
+              'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' , 
+         params => [
+             { desc => 'Authentication token',     type => 'string'},
+             { desc => 'Hash of named parameters', type => 'object'},
+         ],
+         return => {
+             desc => 'List of new message IDs (empty if none)',
+             type => 'array'
+         }
+     }
+ );
+ =head3 check_title_hold (token, hash)
+ The named fields in the hash are: 
+  patronid     - ID of the hold recipient  (required)
+  depth        - hold range depth          (default 0)
+  pickup_lib   - destination for hold, fallback value for selection_ou
+  selection_ou - ID of org_unit establishing hard and soft hold boundary settings
+  issuanceid   - ID of the issuance to be held, required for Issuance level hold
+  titleid      - ID (BRN) of the title to be held, required for Title level hold
+  volume_id    - required for Volume level hold
+  copy_id      - required for Copy level hold
+  mrid         - required for Meta-record level hold
+  hold_type    - T, C (or R or F), I, V or M for Title, Copy, Issuance, Volume or Meta-record  (default "T")
+ All key/value pairs are passed on to do_possibility_checks.
+ =cut
+ # FIXME: better params checking.  what other params are required, if any?
+ # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
+ # FIXME: for example, $depth gets a default value, but then $$params{depth} is still 
+ # used in conditionals, where it may be undefined, causing a warning.
+ # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
+ sub check_title_hold {
+     my( $self, $client, $authtoken, $params ) = @_;
+     my $e = new_editor(authtoken=>$authtoken);
+     return $e->event unless $e->checkauth;
+     my %params       = %$params;
+     my $depth        = $params{depth}        || 0;
+     my $selection_ou = $params{selection_ou} || $params{pickup_lib};
+       my $patron = $e->retrieve_actor_user($params{patronid})
+               or return $e->event;
+       if( $e->requestor->id ne $patron->id ) {
+               return $e->event unless 
+                       $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
+       }
+       return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
+       my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
+               or return $e->event;
+     my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
+     my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
+     my @status = ();
+     my $return_depth = $hard_boundary; # default depth to return on success
+     if(defined $soft_boundary and $depth < $soft_boundary) {
+         # work up the tree and as soon as we find a potential copy, use that depth
+         # also, make sure we don't go past the hard boundary if it exists
+         # our min boundary is the greater of user-specified boundary or hard boundary
+         my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?  
+             $hard_boundary : $depth;
+         my $depth = $soft_boundary;
+         while($depth >= $min_depth) {
+             $logger->info("performing hold possibility check with soft boundary $depth");
+             @status = do_possibility_checks($e, $patron, $request_lib, $depth, %params);
+             if ($status[0]) {
+                 $return_depth = $depth;
+                 last;
+             }
+             $depth--;
+         }
+     } elsif(defined $hard_boundary and $depth < $hard_boundary) {
+         # there is no soft boundary, enforce the hard boundary if it exists
+         $logger->info("performing hold possibility check with hard boundary $hard_boundary");
+         @status = do_possibility_checks($e, $patron, $request_lib, $hard_boundary, %params);
+     } else {
+         # no boundaries defined, fall back to user specifed boundary or no boundary
+         $logger->info("performing hold possibility check with no boundary");
+         @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
+     }
+     if ($status[0]) {
+         return {
+             "success" => 1,
+             "depth" => $return_depth,
+             "local_avail" => $status[1]
+         };
+     } elsif ($status[2]) {
+         my $n = scalar @{$status[2]};
+         return {"success" => 0, "last_event" => $status[2]->[$n - 1]};
+     } else {
+         return {"success" => 0};
+     }
+ }
+ sub do_possibility_checks {
+     my($e, $patron, $request_lib, $depth, %params) = @_;
+     my $issuanceid   = $params{issuanceid}      || "";
+     my $titleid      = $params{titleid}      || "";
+     my $volid        = $params{volume_id};
+     my $copyid       = $params{copy_id};
+     my $mrid         = $params{mrid}         || "";
+     my $pickup_lib   = $params{pickup_lib};
+     my $hold_type    = $params{hold_type}    || 'T';
+     my $selection_ou = $params{selection_ou} || $pickup_lib;
+       my $copy;
+       my $volume;
+       my $title;
+       if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
+         return $e->event unless $copy   = $e->retrieve_asset_copy($copyid);
+         return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
+         return $e->event unless $title  = $e->retrieve_biblio_record_entry($volume->record);
+         return verify_copy_for_hold( 
+             $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib
+         );
+       } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
+               return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
+               return $e->event unless $title  = $e->retrieve_biblio_record_entry($volume->record);
+               return _check_volume_hold_is_possible(
+                       $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
+         );
+       } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
+               return _check_title_hold_is_possible(
+                       $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
+         );
+       } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
+               return _check_issuance_hold_is_possible(
+                       $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
+         );
+       } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
+               my $maps = $e->search_metabib_metarecord_source_map({metarecord=>$mrid});
+               my @recs = map { $_->source } @$maps;
+               my @status = ();
+               for my $rec (@recs) {
+                       @status = _check_title_hold_is_possible(
+                               $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
+                       );
+                       last if $status[1];
+               }
+               return @status;
+       }
+ #   else { Unrecognized hold_type ! }   # FIXME: return error? or 0?
+ }
+ my %prox_cache;
+ sub create_ranged_org_filter {
+     my($e, $selection_ou, $depth) = @_;
+     # find the orgs from which this hold may be fulfilled, 
+     # based on the selection_ou and depth
+     my $top_org = $e->search_actor_org_unit([
+         {parent_ou => undef}, 
+         {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
+     my %org_filter;
+     return () if $depth == $top_org->ou_type->depth;
+     my $org_list = $U->storagereq('open-ils.storage.actor.org_unit.descendants.atomic', $selection_ou, $depth);
+     %org_filter = (circ_lib => []);
+     push(@{$org_filter{circ_lib}}, $_->id) for @$org_list;
+     $logger->info("hold org filter at depth $depth and selection_ou ".
+         "$selection_ou created list of @{$org_filter{circ_lib}}");
+     return %org_filter;
+ }
+ sub _check_title_hold_is_possible {
+     my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
+    
+     my $e = new_editor();
+     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
+     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
+     my $copies = $e->json_query(
+         { 
+             select => { acp => ['id', 'circ_lib'] },
+               from => {
+                 acp => {
+                     acn => {
+                         field  => 'id',
+                         fkey   => 'call_number',
+                         'join' => {
+                             bre => {
+                                 field  => 'id',
+                                 filter => { id => $titleid },
+                                 fkey   => 'record'
+                             }
+                         }
+                     },
+                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
+                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   }
+                 }
+             }, 
+             where => {
+                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
+             }
+         }
+     );
+     $logger->info("title possible found ".scalar(@$copies)." potential copies");
+     return (
+         0, 0, [
+             new OpenILS::Event(
+                 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
+                 "payload" => {"fail_part" => "no_ultimate_items"}
+             )
+         ]
+     ) unless @$copies;
+     # -----------------------------------------------------------------------
+     # sort the copies into buckets based on their circ_lib proximity to 
+     # the patron's home_ou.  
+     # -----------------------------------------------------------------------
+     my $home_org = $patron->home_ou;
+     my $req_org = $request_lib->id;
+     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
+     $prox_cache{$home_org} = 
+         $e->search_actor_org_unit_proximity({from_org => $home_org})
+         unless $prox_cache{$home_org};
+     my $home_prox = $prox_cache{$home_org};
+     my %buckets;
+     my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
+     push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
+     my @keys = sort { $a <=> $b } keys %buckets;
+     if( $home_org ne $req_org ) {
+       # -----------------------------------------------------------------------
+       # shove the copies close to the request_lib into the primary buckets 
+       # directly before the farthest away copies.  That way, they are not 
+       # given priority, but they are checked before the farthest copies.
+       # -----------------------------------------------------------------------
+         $prox_cache{$req_org} = 
+             $e->search_actor_org_unit_proximity({from_org => $req_org})
+             unless $prox_cache{$req_org};
+         my $req_prox = $prox_cache{$req_org};
+         my %buckets2;
+         my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
+         push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
+         my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
+         my $new_key = $highest_key - 0.5; # right before the farthest prox
+         my @keys2   = sort { $a <=> $b } keys %buckets2;
+         for my $key (@keys2) {
+             last if $key >= $highest_key;
+             push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
+         }
+     }
+     @keys = sort { $a <=> $b } keys %buckets;
+     my $title;
+     my %seen;
+     my @status;
+     OUTER: for my $key (@keys) {
+       my @cps = @{$buckets{$key}};
+       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
+       for my $copyid (@cps) {
+          next if $seen{$copyid};
+          $seen{$copyid} = 1; # there could be dupes given the merged buckets
+          my $copy = $e->retrieve_asset_copy($copyid);
+          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
+          unless($title) { # grab the title if we don't already have it
+             my $vol = $e->retrieve_asset_call_number(
+                [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
+             $title = $vol->record;
+          }
+    
+          @status = verify_copy_for_hold(
+             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib);
+          last OUTER if $status[0];
+       }
+     }
+     return @status;
+ }
+ sub _check_issuance_hold_is_possible {
+     my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
+    
+     my $e = new_editor();
+     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
+     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
+     my $copies = $e->json_query(
+         { 
+             select => { acp => ['id', 'circ_lib'] },
+               from => {
+                 acp => {
+                     sitem => {
+                         field  => 'unit',
+                         fkey   => 'id',
+                         filter => { issuance => $issuanceid }
+                     },
+                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
+                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   }
+                 }
+             }, 
+             where => {
+                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
+             },
+             distinct => 1
+         }
+     );
+     $logger->info("issuance possible found ".scalar(@$copies)." potential copies");
+     my $empty_ok;
+     if (!@$copies) {
+         $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
+         $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
+         return (
+             0, 0, [
+                 new OpenILS::Event(
+                     "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
+                     "payload" => {"fail_part" => "no_ultimate_items"}
+                 )
+             ]
+         ) unless $empty_ok;
+         return (1, 0);
+     }
+     # -----------------------------------------------------------------------
+     # sort the copies into buckets based on their circ_lib proximity to 
+     # the patron's home_ou.  
+     # -----------------------------------------------------------------------
+     my $home_org = $patron->home_ou;
+     my $req_org = $request_lib->id;
+     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
+     $prox_cache{$home_org} = 
+         $e->search_actor_org_unit_proximity({from_org => $home_org})
+         unless $prox_cache{$home_org};
+     my $home_prox = $prox_cache{$home_org};
+     my %buckets;
+     my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
+     push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
+     my @keys = sort { $a <=> $b } keys %buckets;
+     if( $home_org ne $req_org ) {
+       # -----------------------------------------------------------------------
+       # shove the copies close to the request_lib into the primary buckets 
+       # directly before the farthest away copies.  That way, they are not 
+       # given priority, but they are checked before the farthest copies.
+       # -----------------------------------------------------------------------
+         $prox_cache{$req_org} = 
+             $e->search_actor_org_unit_proximity({from_org => $req_org})
+             unless $prox_cache{$req_org};
+         my $req_prox = $prox_cache{$req_org};
+         my %buckets2;
+         my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
+         push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
+         my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
+         my $new_key = $highest_key - 0.5; # right before the farthest prox
+         my @keys2   = sort { $a <=> $b } keys %buckets2;
+         for my $key (@keys2) {
+             last if $key >= $highest_key;
+             push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
+         }
+     }
+     @keys = sort { $a <=> $b } keys %buckets;
+     my $title;
+     my %seen;
+     my @status;
+     OUTER: for my $key (@keys) {
+       my @cps = @{$buckets{$key}};
+       $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
+       for my $copyid (@cps) {
+          next if $seen{$copyid};
+          $seen{$copyid} = 1; # there could be dupes given the merged buckets
+          my $copy = $e->retrieve_asset_copy($copyid);
+          $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
+          unless($title) { # grab the title if we don't already have it
+             my $vol = $e->retrieve_asset_call_number(
+                [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
+             $title = $vol->record;
+          }
+    
+          @status = verify_copy_for_hold(
+             $patron, $requestor, $title, $copy, $pickup_lib, $request_lib);
+          last OUTER if $status[0];
+       }
+     }
+     if (!$status[0]) {
+         if (!defined($empty_ok)) {
+             $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_issuance_ok');
+             $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
+         }
+         return (1,0) if ($empty_ok);
+     }
+     return @status;
+ }
+ sub _check_volume_hold_is_possible {
+       my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
+     my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
+       my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
+       $logger->info("checking possibility of volume hold for volume ".$vol->id);
+     return (
+         0, 0, [
+             new OpenILS::Event(
+                 "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
+                 "payload" => {"fail_part" => "no_ultimate_items"}
+             )
+         ]
+     ) unless @$copies;
+     my @status;
+       for my $copy ( @$copies ) {
+         @status = verify_copy_for_hold(
+                       $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
+         last if $status[0];
+       }
+       return @status;
+ }
+ sub verify_copy_for_hold {
+       my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
+       $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
+     my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
+               {       patron                          => $patron, 
+                       requestor                       => $requestor, 
+                       copy                            => $copy,
+                       title                           => $title, 
+                       title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
+                       pickup_lib                      => $pickup_lib,
+                       request_lib                     => $request_lib,
+             new_hold            => 1,
+             show_event_list     => 1
+               } 
+       );
+     return (
+         (not scalar @$permitted), # true if permitted is an empty arrayref
+         (
+               ($copy->circ_lib == $pickup_lib) and 
+             ($copy->status == OILS_COPY_STATUS_AVAILABLE)
+         ),
+         $permitted
+     );
+ }
+ sub find_nearest_permitted_hold {
+     my $class  = shift;
+     my $editor = shift;     # CStoreEditor object
+     my $copy   = shift;     # copy to target
+     my $user   = shift;     # staff
+     my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
+       
+     my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
+     my $bc = $copy->barcode;
+       # find any existing holds that already target this copy
+       my $old_holds = $editor->search_action_hold_request(
+               {       current_copy => $copy->id, 
+                       cancel_time  => undef, 
+                       capture_time => undef 
+               } 
+       );
+       # hold->type "R" means we need this copy
+       for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
+     my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
+       $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
+         " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
+       my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
+       # search for what should be the best holds for this copy to fulfill
+       my $best_holds = $U->storagereq(
+         "open-ils.storage.action.hold_request.nearest_hold.atomic", 
+               $user->ws_ou, $copy->id, 10, $hold_stall_interval, $fifo );
+       unless(@$best_holds) {
+               if( my $hold = $$old_holds[0] ) {
+                       $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
+                       return ($hold);
+               }
+               $logger->info("circulator: no suitable holds found for copy $bc");
+               return (undef, $evt);
+       }
+       my $best_hold;
+       # for each potential hold, we have to run the permit script
+       # to make sure the hold is actually permitted.
+     my %reqr_cache;
+     my %org_cache;
+       for my $holdid (@$best_holds) {
+               next unless $holdid;
+               $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
+               my $hold = $editor->retrieve_action_hold_request($holdid) or next;
+               my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
+               my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
+               $reqr_cache{$hold->requestor} = $reqr;
+               $org_cache{$hold->request_lib} = $rlib;
+               # see if this hold is permitted
+               my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
+                       {       patron_id                       => $hold->usr,
+                               requestor                       => $reqr,
+                               copy                            => $copy,
+                               pickup_lib                      => $hold->pickup_lib,
+                               request_lib                     => $rlib,
+                               retarget                        => 1
+                       } 
+               );
+               if( $permitted ) {
+                       $best_hold = $hold;
+                       last;
+               }
+       }
+       unless( $best_hold ) { # no "good" permitted holds were found
+               if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
+                       $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
+                       return ($hold);
+               }
+               # we got nuthin
+               $logger->info("circulator: no suitable holds found for copy $bc");
+               return (undef, $evt);
+       }
+       $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
+       # indicate a permitted hold was found
+       return $best_hold if $check_only;
+       # we've found a permitted hold.  we need to "grab" the copy 
+       # to prevent re-targeted holds (next part) from re-grabbing the copy
+       $best_hold->current_copy($copy->id);
+       $editor->update_action_hold_request($best_hold) 
+               or return (undef, $editor->event);
+     my @retarget;
+       # re-target any other holds that already target this copy
+       for my $old_hold (@$old_holds) {
+               next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
+               $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
+             $old_hold->id." after a better hold [".$best_hold->id."] was found");
+         $old_hold->clear_current_copy;
+         $old_hold->clear_prev_check_time;
+         $editor->update_action_hold_request($old_hold) 
+             or return (undef, $editor->event);
+         push(@retarget, $old_hold->id);
+       }
+       return ($best_hold, undef, (@retarget) ? \@retarget : undef);
+ }
+ __PACKAGE__->register_method(
+     method   => 'all_rec_holds',
+     api_name => 'open-ils.circ.holds.retrieve_all_from_title',
+ );
+ sub all_rec_holds {
+       my( $self, $conn, $auth, $title_id, $args ) = @_;
+       my $e = new_editor(authtoken=>$auth);
+       $e->checkauth or return $e->event;
+       $e->allowed('VIEW_HOLD') or return $e->event;
+       $args ||= {};
+     $args->{fulfillment_time} = undef; #  we don't want to see old fulfilled holds
+       $args->{cancel_time} = undef;
+       my $resp = { volume_holds => [], copy_holds => [], metarecord_holds => [] };
+     my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
+     if($mr_map) {
+         $resp->{metarecord_holds} = $e->search_action_hold_request(
+             {   hold_type => OILS_HOLD_TYPE_METARECORD,
+                 target => $mr_map->metarecord,
+                 %$args 
+             }, {idlist => 1}
+         );
+     }
+       $resp->{title_holds} = $e->search_action_hold_request(
+               { 
+                       hold_type => OILS_HOLD_TYPE_TITLE, 
+                       target => $title_id, 
+                       %$args 
+               }, {idlist=>1} );
+       my $vols = $e->search_asset_call_number(
+               { record => $title_id, deleted => 'f' }, {idlist=>1});
+       return $resp unless @$vols;
+       $resp->{volume_holds} = $e->search_action_hold_request(
+               { 
+                       hold_type => OILS_HOLD_TYPE_VOLUME, 
+                       target => $vols,
+                       %$args }, 
+               {idlist=>1} );
+       my $copies = $e->search_asset_copy(
+               { call_number => $vols, deleted => 'f' }, {idlist=>1});
+       return $resp unless @$copies;
+       $resp->{copy_holds} = $e->search_action_hold_request(
+               { 
+                       hold_type => OILS_HOLD_TYPE_COPY,
+                       target => $copies,
+                       %$args }, 
+               {idlist=>1} );
+       return $resp;
+ }
+ __PACKAGE__->register_method(
+     method        => 'uber_hold',
+     authoritative => 1,
+     api_name      => 'open-ils.circ.hold.details.retrieve'
+ );
+ sub uber_hold {
 -    return uber_hold_impl($e, $hold_id);
++      my($self, $client, $auth, $hold_id, $args) = @_;
+       my $e = new_editor(authtoken=>$auth);
+       $e->checkauth or return $e->event;
 -      my($self, $client, $auth, $hold_ids) = @_;
++    return uber_hold_impl($e, $hold_id, $args);
+ }
+ __PACKAGE__->register_method(
+     method        => 'batch_uber_hold',
+     authoritative => 1,
+     stream        => 1,
+     api_name      => 'open-ils.circ.hold.details.batch.retrieve'
+ );
+ sub batch_uber_hold {
 -    $client->respond(uber_hold_impl($e, $_)) for @$hold_ids;
++      my($self, $client, $auth, $hold_ids, $args) = @_;
+       my $e = new_editor(authtoken=>$auth);
+       $e->checkauth or return $e->event;
 -    my($e, $hold_id) = @_;
++    $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
+     return undef;
+ }
+ sub uber_hold_impl {
 -      my $card = $e->retrieve_actor_card($user->card)
 -              or return $e->event;
++    my($e, $hold_id, $args) = @_;
+       my $resp = {};
++    $args ||= {};
+       my $hold = $e->retrieve_action_hold_request(
+               [
+                       $hold_id,
+                       {
+                               flesh => 1,
+                               flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
+                       }
+               ]
+       ) or return $e->event;
+     if($hold->usr->id ne $e->requestor->id) {
+         # A user is allowed to see his/her own holds
+           $e->allowed('VIEW_HOLD') or return $e->event;
+         $hold->notes( # filter out any non-staff ("private") notes
+             [ grep { !$U->is_true($_->staff) } @{$hold->notes} ] );
+     } else {
+         # caller is asking for own hold, but may not have permission to view staff notes
+           unless($e->allowed('VIEW_HOLD')) {
+             $hold->notes( # filter out any staff notes
+                 [ grep { $U->is_true($_->staff) } @{$hold->notes} ] );
+         }
+     }
+       my $user = $hold->usr;
+       $hold->usr($user->id);
 -      my( $mvr, $volume, $copy ) = find_hold_mvr($e, $hold);
 -      flesh_hold_notices([$hold], $e);
 -      flesh_hold_transits([$hold]);
++      my( $mvr, $volume, $copy, $issuance, $bre ) = find_hold_mvr($e, $hold, $args->{suppress_mvr});
 -    return {
++      flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
++      flesh_hold_transits([$hold]) unless $args->{suppress_transits};
+     my $details = retrieve_hold_queue_status_impl($e, $hold);
 -        mvr            => $mvr,
 -        patron_first   => $user->first_given_name,
 -        patron_last    => $user->family_name,
 -        patron_barcode => $card->barcode,
 -        patron_alias   => $user->alias,
++    my $resp = {
+         hold           => $hold,
+         copy           => $copy,
+         volume         => $volume,
 -      my( $e, $hold ) = @_;
+         %$details
+     };
++
++    $resp->{mvr} = $mvr unless $args->{suppress_mvr};
++    unless($args->{suppress_patron_details}) {
++          my $card = $e->retrieve_actor_card($user->card) or return $e->event;
++        $resp->{patron_first}   = $user->first_given_name,
++        $resp->{patron_last}    = $user->family_name,
++        $resp->{patron_barcode} = $card->barcode,
++        $resp->{patron_alias}   = $user->alias,
++    };
++
++    $resp->{bre} = $bre if $args->{include_bre};
++
++    return $resp;
+ }
+ # -----------------------------------------------------
+ # Returns the MVR object that represents what the
+ # hold is all about
+ # -----------------------------------------------------
+ sub find_hold_mvr {
 -      return ( $U->record_to_mvr($title), $volume, $copy, $issuance );
++      my( $e, $hold, $no_mvr ) = @_;
+       my $tid;
+       my $copy;
+       my $volume;
+     my $issuance;
+       if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
+               my $mr = $e->retrieve_metabib_metarecord($hold->target)
+                       or return $e->event;
+               $tid = $mr->master_record;
+       } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
+               $tid = $hold->target;
+       } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
+               $volume = $e->retrieve_asset_call_number($hold->target)
+                       or return $e->event;
+               $tid = $volume->record;
+     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
+         $issuance = $e->retrieve_serial_issuance([
+             $hold->target,
+             {flesh => 1, flesh_fields => {siss => [ qw/subscription/ ]}}
+         ]) or return $e->event;
+         $tid = $issuance->subscription->record_entry;
+       } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
+               $copy = $e->retrieve_asset_copy([
+             $hold->target, 
+             {flesh => 1, flesh_fields => {acp => ['call_number']}}
+         ]) or return $e->event;
+         
+               $volume = $copy->call_number;
+               $tid = $volume->record;
+       }
+       if(!$copy and ref $hold->current_copy ) {
+               $copy = $hold->current_copy;
+               $hold->current_copy($copy->id);
+       }
+       if(!$volume and $copy) {
+               $volume = $e->retrieve_asset_call_number($copy->call_number);
+       }
+     # TODO return metarcord mvr for M holds
+       my $title = $e->retrieve_biblio_record_entry($tid);
++      return ( ($no_mvr) ? undef : $U->record_to_mvr($title), $volume, $copy, $issuance, $title );
+ }
+ __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',
+     stream    => 1,
+     api_name  => 'open-ils.circ.hold.clear_shelf.process',
+     signature => {
+         desc => q/
+             1. Find all holds that have expired on the holds shelf
+             2. Cancel the holds
+             3. If a clear-shelf status is configured, put targeted copies into this status
+             4. Divide copies into 3 groups: items to transit, items to reshelve, and items
+                 that are needed for holds.  No subsequent action is taken on the holds
+                 or items after grouping.
+         /
+     }
+ );
+ sub clear_shelf_process {
+       my($self, $client, $auth, $org_id) = @_;
+       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;
+     my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
+     # Find holds on the shelf that have been there too long
+     my $hold_ids = $e->search_action_hold_request(
+         {   shelf_expire_time => {'<' => 'now'},
+             pickup_lib        => $org_id,
+             cancel_time       => undef,
+             fulfillment_time  => undef,
+             shelf_time        => {'!=' => undef},
+             capture_time      => {'!=' => undef},
+             current_copy      => {'!=' => undef},
+         },
+         { 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");
+         
+         my $hold = $e->retrieve_action_hold_request([
+             $hold_id, {   
+                 flesh => 1,
+                 flesh_fields => {ahr => ['current_copy']}
+             }
+         ]);
+         $hold->cancel_time('now');
+         $hold->cancel_cause(2); # Hold Shelf expiration
+         $e->update_action_hold_request($hold) or return $e->die_event;
+         my $copy = $hold->current_copy;
+         if($copy_status or $copy_status == 0) {
+             # if a clear-shelf copy status is defined, update the copy
+             $copy->status($copy_status);
+             $copy->edit_date('now');
+             $copy->editor($e->requestor->id);
+             $e->update_asset_copy($copy) or return $e->die_event;
+         }
+         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) {
+                 push(@{$cache_data{hold}}, $hold->id); # copy is needed for a hold
+             } elsif($copy->circ_lib != $e->requestor->ws_ou) {
+                 push(@{$cache_data{transit}}, $hold->id); # copy needs to transit
+             } else {
+                 push(@{$cache_data{shelf}}, $hold->id); # copy needs to go back to the shelf
+             }
+         }
+         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?
+         # tell the client we're done
+         $client->respond_complete({cache_key => $cache_key});
+         # 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
+         $client->respond_complete;
+     }
+ }
+ __PACKAGE__->register_method(
+     method    => 'usr_hold_summary',
+     api_name  => 'open-ils.circ.holds.user_summary',
+     signature => q/
+         Returns a summary of holds statuses for a given user
+     /
+ );
+ sub usr_hold_summary {
+     my($self, $conn, $auth, $user_id) = @_;
+       my $e = new_editor(authtoken=>$auth);
+       $e->checkauth or return $e->event;
+       $e->allowed('VIEW_HOLD') or return $e->event;
+     my $holds = $e->search_action_hold_request(
+         {  
+             usr =>  $user_id , 
+             fulfillment_time => undef,
+             cancel_time      => undef,
+         }
+     );
+     my %summary = (1 => 0, 2 => 0, 3 => 0, 4 => 0);
+     $summary{_hold_status($e, $_)} += 1 for @$holds;
+     return \%summary;
+ }
+ __PACKAGE__->register_method(
+     method    => 'hold_has_copy_at',
+     api_name  => 'open-ils.circ.hold.has_copy_at',
+     signature => {
+         desc   => 
+                 'Returns the ID of the found copy and name of the shelving location if there is ' .
+                 'an available copy at the specified org unit.  Returns empty hash otherwise.  '   .
+                 'The anticipated use for this method is to determine whether an item is '         .
+                 'available at the library where the user is placing the hold (or, alternatively, '.
+                 'at the pickup library) to encourage bypassing the hold placement and just '      .
+                 'checking out the item.' ,
+         params => {
+             { desc => 'Authentication Token', type => 'string' },
+             { desc => 'Method Arguments.  Options include: hold_type, hold_target, org_unit.  ' 
+                     . 'hold_type is the hold type code (T, V, C, M, ...).  '
+                     . 'hold_target is the identifier of the hold target object.  ' 
+                     . 'org_unit is org unit ID.', 
+               type => 'object' 
+             },
+         },
+         return => { 
+             desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
+             type => 'object' 
+         }
+     }
+ );
+ sub hold_has_copy_at {
+     my($self, $conn, $auth, $args) = @_;
+       my $e = new_editor(authtoken=>$auth);
+       $e->checkauth or return $e->event;
+     my $hold_type   = $$args{hold_type};
+     my $hold_target = $$args{hold_target};
+     my $org_unit    = $$args{org_unit};
+     my $query = {
+         select => {acp => ['id'], acpl => ['name']},
+         from   => {
+             acp => {
+                 acpl => {field => 'id', filter => { holdable => 't'}, fkey => 'location'},
+                 ccs  => {field => 'id', filter => { holdable => 't'}, fkey => 'status'  }
+             }
+         },
+         where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit}},
+         limit => 1
+     };
+     if($hold_type eq 'C') {
+         $query->{where}->{'+acp'}->{id} = $hold_target;
+     } elsif($hold_type eq 'V') {
+         $query->{where}->{'+acp'}->{call_number} = $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)->[0] or return {};
+     return {copy => $res->{id}, location => $res->{name}} if $res;
+ }
+ # returns true if the user already has an item checked out 
+ # that could be used to fulfill the requested hold.
+ sub hold_item_is_checked_out {
+     my($e, $user_id, $hold_type, $hold_target) = @_;
+     my $query = {
+         select => {acp => ['id']},
+         from   => {acp => {}},
+         where  => {
+             '+acp' => {
+                 id => {
+                     in => { # copies for circs the user has checked out
+                         select => {circ => ['target_copy']},
+                         from   => 'circ',
+                         where  => {
+                             usr => $user_id,
+                             checkin_time => undef,
+                             '-or' => [
+                                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
+                                 {stop_fines => undef}
+                             ],
+                         }
+                     }
+                 }
+             }
+         },
+         limit => 1
+     };
+     if($hold_type eq 'C' || $hold_type eq 'R' || $hold_type eq 'F') {
+         $query->{where}->{'+acp'}->{id}->{in}->{where}->{'target_copy'} = $hold_target;
+     } elsif($hold_type eq 'V') {
+         $query->{where}->{'+acp'}->{call_number} = $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},
+                         }
+                     }
+                 }
+             }
+         };
+     }
+     return $e->json_query($query)->[0];
+ }
+ __PACKAGE__->register_method(
+     method    => 'change_hold_title',
+     api_name  => 'open-ils.circ.hold.change_title',
+     signature => {
+         desc => q/
+             Updates all title level holds targeting the specified bibs to point a new bib./,
+         params => [
+             { desc => 'Authentication Token', type => 'string' },
+             { desc => 'New Target Bib Id',    type => 'number' },
+             { desc => 'Old Target Bib Ids',   type => 'array'  },
+         ],
+         return => { desc => '1 on success' }
+     }
+ );
+ sub change_hold_title {
+     my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
+     my $e = new_editor(authtoken=>$auth, xact=>1);
+     return $e->die_event unless $e->checkauth;
+     my $holds = $e->search_action_hold_request(
+         [
+             {
+                 cancel_time      => undef,
+                 fulfillment_time => undef,
+                 hold_type        => 'T',
+                 target           => $bib_ids
+             },
+             {
+                 flesh        => 1,
+                 flesh_fields => { ahr => ['usr'] }
+             }
+         ],
+         { substream => 1 }
+     );
+     for my $hold (@$holds) {
+         $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
+         $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
+         $hold->target( $new_bib_id );
+         $e->update_action_hold_request($hold) or return $e->die_event;
+     }
+     $e->commit;
+     return 1;
+ }
+ __PACKAGE__->register_method(
+     method    => 'rec_hold_count',
+     api_name  => 'open-ils.circ.bre.holds.count',
+     signature => {
+         desc => q/Returns the total number of holds that target the 
+             selected bib record or its associated copies and call_numbers/,
+         params => [
+             { desc => 'Bib ID', type => 'number' },
+         ],
+         return => {desc => 'Hold count', type => 'number'}
+     }
+ );
+ __PACKAGE__->register_method(
+     method    => 'rec_hold_count',
+     api_name  => 'open-ils.circ.mmr.holds.count',
+     signature => {
+         desc => q/Returns the total number of holds that target the 
+             selected metarecord or its associated copies, call_numbers, and bib records/,
+         params => [
+             { desc => 'Metarecord ID', type => 'number' },
+         ],
+         return => {desc => 'Hold count', type => 'number'}
+     }
+ );
+ sub rec_hold_count {
+     my($self, $conn, $target_id) = @_;
+     my $mmr_join = {
+         mmrsm => {
+             field => 'id',
+             fkey => 'source',
+             filter => {metarecord => $target_id}
+         }
+     };
+     my $bre_join = {
+         bre => {
+             field => 'id',
+             filter => { id => $target_id },
+             fkey => 'record'
+         }
+     };
+     if($self->api_name =~ /mmr/) {
+         delete $bre_join->{bre}->{filter};
+         $bre_join->{bre}->{join} = $mmr_join;
+     }
+     my $cn_join = {
+         acn => {
+             field => 'id',
+             fkey => 'call_number',
+             join => $bre_join
+         }
+     };
+     my $query = {
+         select => {ahr => [{column => 'id', transform => 'count', alias => 'count'}]},
+         from => 'ahr',
+         where => {
+             '+ahr' => {
+                 cancel_time => undef, 
+                 fulfillment_time => undef,
+                 '-or' => [
+                     {
+                         '-and' => {
+                             hold_type => 'C',
+                             target => {
+                                 in => {
+                                     select => {acp => ['id']},
+                                     from => { acp => $cn_join }
+                                 }
+                             }
+                         }
+                     },
+                     {
+                         '-and' => {
+                             hold_type => 'V',
+                             target => {
+                                 in => {
+                                     select => {acn => ['id']},
+                                     from => {acn => $bre_join}
+                                 }
+                             }
+                         }
+                     },
+                     {
+                         '-and' => {
+                             hold_type => 'T',
+                             target => $target_id
+                         }
+                     }
+                 ]
+             }
+         }
+     };
+     if($self->api_name =~ /mmr/) {
+         $query->{where}->{'+ahr'}->{'-or'}->[2] = {
+             '-and' => {
+                 hold_type => 'T',
+                 target => {
+                     in => {
+                         select => {bre => ['id']},
+                         from => {bre => $mmr_join}
+                     }
+                 }
+             }
+         };
+         $query->{where}->{'+ahr'}->{'-or'}->[3] = {
+             '-and' => {
+                 hold_type => 'M',
+                 target => $target_id
+             }
+         };
+     }
+     return new_editor()->json_query($query)->[0]->{count};
+ }
+ 1;
index 0000000,42eb6ff..e804562
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,230 +1,272 @@@
 -        warn "Invalid web config $web_config_file";
+ package OpenILS::WWW::EGWeb;
+ use strict; use warnings;
+ use Template;
+ use XML::Simple;
+ use XML::LibXML;
+ use File::stat;
+ use Apache2::Const -compile => qw(OK DECLINED HTTP_INTERNAL_SERVER_ERROR);
+ use Apache2::Log;
+ use OpenSRF::EX qw(:try);
++use OpenILS::Utils::CStoreEditor;
+ use constant OILS_HTTP_COOKIE_SKIN => 'oils:skin';
+ use constant OILS_HTTP_COOKIE_THEME => 'oils:theme';
+ use constant OILS_HTTP_COOKIE_LOCALE => 'oils:locale';
+ my $web_config;
+ my $web_config_file;
+ my $web_config_edit_time;
+ sub import {
+     my $self = shift;
+     $web_config_file = shift;
+     unless(-r $web_config_file) {
 -    $ctx->{page_args} = $page_args;
 -    $r->content_type('text/html; encoding=utf8');
++        warn "Invalid web config $web_config_file\n";
+         return;
+     }
+     check_web_config();
+ }
+ sub handler {
+     my $r = shift;
+     check_web_config($r); # option to disable this
+     my $ctx = load_context($r);
+     my $base = $ctx->{base_path};
++
++    $r->content_type('text/html; encoding=utf8');
++
+     my($template, $page_args, $as_xml) = find_template($r, $base, $ctx);
++    $ctx->{page_args} = $page_args;
++
++    my $stat = run_context_loader($r, $ctx);
++
++    return $stat unless $stat == Apache2::Const::OK;
+     return Apache2::Const::DECLINED unless $template;
+     $template = $ctx->{skin} . "/$template";
 -      my $e = shift;
+     my $tt = Template->new({
+         OUTPUT => ($as_xml) ?  sub { parse_as_xml($r, $ctx, @_); } : $r,
+         INCLUDE_PATH => $ctx->{template_paths},
++        DEBUG => $ctx->{debug_template}
+     });
+     unless($tt->process($template, {ctx => $ctx})) {
+         $r->log->warn('Template error: ' . $tt->error);
+         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+     }
+     return Apache2::Const::OK;
+ }
++
++sub run_context_loader {
++    my $r = shift;
++    my $ctx = shift;
++
++    my $stat = Apache2::Const::OK;
++
++    my $loader = $r->dir_config('OILSWebContextLoader');
++    return $stat unless $loader;
++
++    eval {
++        $loader->use;
++        $stat = $loader->new($r, $ctx)->load;
++    };
++
++    if($@) {
++        $r->log->error("Context Loader error: $@");
++        return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
++    }
++
++    $r->log->info("context loader resulted in status $stat");
++    return $stat;
++}
++
+ sub parse_as_xml {
+     my $r = shift;
+     my $ctx = shift;
+     my $data = shift;
+     my $success = 0;
+     try { 
+         my $doc = XML::LibXML->new->parse_string($data); 
+         $data = $doc->documentElement->toStringC14N;
+         $data = $ctx->{final_dtd} . "\n" . $data;
+         $success = 1;
+     } otherwise {
 -    my $ctx = $web_config->{ctx};
++          my $e = shift;
+         my $err = "Invalid XML: $e";
+         $r->log->error($err);
+         $r->content_type('text/plain; encoding=utf8');
+         $r->print("\n$err\n\n$data");
+     };
+     $r->print($data) if ($success);
+ }
+ sub load_context {
+     my $r = shift;
+     my $cgi = CGI->new;
 -    $ctx->{force_valid_xml} = ($data->{force_valid_xml} =~ /true/io) ? 1 : 0;
++    my $ctx = {}; # new context for each page load
++    $ctx->{$_} = $web_config->{base_ctx}->{$_} for keys %{$web_config->{base_ctx}};
+     $ctx->{hostname} = $r->hostname;
+     $ctx->{base_url} = $cgi->url(-base => 1);
+     $ctx->{skin} = $cgi->cookie(OILS_HTTP_COOKIE_SKIN) || 'default';
+     $ctx->{theme} = $cgi->cookie(OILS_HTTP_COOKIE_THEME) || 'default';
+     $ctx->{locale} = 
+         $cgi->cookie(OILS_HTTP_COOKIE_LOCALE) || 
+         parse_accept_lang($r->headers_in->get('Accept-Language')) || 'en-US';
+     $r->log->debug('skin = ' . $ctx->{skin} . ' : theme = ' . 
+         $ctx->{theme} . ' : locale = ' . $ctx->{locale});
++
++    my $mprefix = $ctx->{media_prefix};
++    if($mprefix and $mprefix !~ /^http/ and $mprefix !~ /^\//) {
++        # if a hostname is provided /w no protocol, match the protocol to the current page
++        $ctx->{media_prefix} = ($cgi->https) ? "https://$mprefix" : "http://$mprefix";
++    }
++
++
+     return $ctx;
+ }
+ # turn Accept-Language into sometihng EG can understand
+ sub parse_accept_lang {
+     my $al = shift;
+     return undef unless $al;
+     my ($locale) = split(/,/, $al);
+     ($locale) = split(/;/, $locale);
+     return undef unless $locale;
+     $locale =~ s/-(.*)/eval '-'.uc("$1")/e;
+     return $locale;
+ }
+ # Given a URI, finds the configured template and any extra page 
+ # arguments (trailing path info).  Any extra data is returned
+ # as page arguments, in the form of an array, one item per 
+ # /-separated URI component
+ sub find_template {
+     my $r = shift;
+     my $base = shift;
+     my $ctx = shift;
+     my $skin = $ctx->{skin};
+     my $path = $r->uri;
+     $path =~ s/$base//og;
+     my @parts = split('/', $path);
+     my $template = '';
+     my $page_args = [];
+     my $as_xml = $ctx->{force_valid_xml};
+     my $handler = $web_config->{handlers};
+     while(@parts) {
+         my $part = shift @parts;
+         next unless $part;
+         my $t = $handler->{$part};
+         if(ref($t) eq 'PathConfig') {
+             $template = $t->{template};
+             $as_xml = ($t->{as_xml} and $t->{as_xml} =~ /true/io) || $as_xml;
+             $page_args = [@parts];
+             last;
+         } else {
+             $handler = $t;
+         }
+     }
+     unless($template) { # no template configured
+         # see if we can magically find the template based on the path and default extension
+         my $ext = $ctx->{default_template_extension};
+         my @parts = split('/', $path);
+         my $localpath = $path;
+         my @args;
+         while(@parts) {
+             last unless $localpath;
+             for my $tpath (@{$ctx->{template_paths}}) {
+                 my $fpath = "$tpath/$skin/$localpath.$ext";
+                 $r->log->debug("looking at possible template $fpath");
+                 if(-r $fpath) {
+                     $template = "$localpath.$ext";
+                     last;
+                 }
+             }
+             last if $template;
+             push(@args, pop @parts);
+             $localpath = '/'.join('/', @parts);
+         } 
+         $page_args = [@args];
+         # no template configured or found
+         unless($template) {
+             $r->log->warn("No template configured for path $path");
+             return ();
+         }
+     }
+     $r->log->debug("template = $template : page args = @$page_args");
+     return ($template, $page_args, $as_xml);
+ }
+ # if the web configuration file has never been loaded or has
+ # changed since the last load, reload it
+ sub check_web_config {
+     my $r = shift;
+     my $epoch = stat($web_config_file)->mtime;
+     unless($web_config_edit_time and $web_config_edit_time == $epoch) {
+         $r->log->debug("Reloading web config after edit...") if $r;
+         $web_config_edit_time = $epoch;
+         $web_config = parse_config($web_config_file);
+     }
+ }
+ sub parse_config {
+     my $cfg_file = shift;
+     my $data = XML::Simple->new->XMLin($cfg_file);
+     my $ctx = {};
+     my $handlers = {};
+     $ctx->{media_prefix} = (ref $data->{media_prefix}) ? '' : $data->{media_prefix};
+     $ctx->{base_path} = (ref $data->{base_path}) ? '' : $data->{base_path};
+     $ctx->{template_paths} = [];
 -    return {ctx => $ctx, handlers => $handlers};
++    $ctx->{force_valid_xml} = ( ($data->{force_valid_xml}||'') =~ /true/io) ? 1 : 0;
++    $ctx->{debug_template} = ( ($data->{debug_template}||'')  =~ /true/io) ? 1 : 0;
+     $ctx->{default_template_extension} = $data->{default_template_extension} || 'tt2';
+     $ctx->{web_dir} = $data->{web_dir};
+     my $tpaths = $data->{template_paths}->{path};
+     $tpaths = [$tpaths] unless ref $tpaths;
+     push(@{$ctx->{template_paths}}, $_) for @$tpaths;
+     for my $handler (@{$data->{handlers}->{handler}}) {
+         my @parts = split('/', $handler->{path});
+         my $h = $handlers;
+         my $pcount = scalar(@parts);
+         for(my $i = 0; $i < $pcount; $i++) {
+             my $p = $parts[$i];
+             unless(defined $h->{$p}) {
+                 if($i == $pcount - 1) {
+                     $h->{$p} = PathConfig->new(%$handler);
+                     last;
+                 } else {
+                     $h->{$p} = {};
+                 }
+             }
+             $h = $h->{$p};
+         }
+     }
++    return {base_ctx => $ctx, handlers => $handlers};
+ }
+ package PathConfig;
+ sub new {
+     my($class, %args) = @_;
+     return bless(\%args, $class);
+ }
+ 1;