From fceeea8feb3ae6dab953e10eef7cb85bced3cd27 Mon Sep 17 00:00:00 2001
From: Mike Rylander
Date: Fri, 6 Sep 2019 14:49:52 -0400
Subject: [PATCH] LP#1838995: Hold group buckets
This feature allows staff to add multiple users to a named hold group
bucket and place title-level holds for a record for that entire set of users.
Users can be added to such a hold group bucket from either the patron
search result interface, via the Add to Bucket dropdown, or through a dedicated
Hold Groups interface available from the Circulation menu. Adding new
patrons to a hold group bucket will require staff have the PLACE_HOLD
permission.
Holds can be placed for the users in a hold group bucket either directly from
the normal staff-place hold interface in the embedded OPAC, or by supplying the
record ID within the hold group bucket interface. In the latter case, the
list of users for which a hold was attempted but failed to be placed can be
downloaded by staff in order to address any placement issues. Placing a
hold group bucket hold will requires staff have the MANAGE_HOLD_GROUPS
permission, which is new with this development.
In the event of a mistaken hold group hold, staff with the
MANAGE_HOLD_GROUPS permission will have the ability to cancel all unfulfilled
holds created as part of a hold group hold event.
A link to the title's hold interface is available from the list of hold group
hold events in the dedicated hold group hold interface.
Signed-off-by: Mike Rylander
Signed-off-by: Dawn Dale
Signed-off-by: Chauncey Montgomery
Signed-off-by: Galen Charlton
---
Open-ILS/examples/fm_IDL.xml | 41 +
.../lib/OpenILS/Application/Actor/Container.pm | 32 +-
.../perlmods/lib/OpenILS/Application/Circ/Holds.pm | 244 ++++++
.../src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm | 3 +
.../lib/OpenILS/WWW/EGCatLoader/Account.pm | 132 ++-
.../perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm | 35 +
Open-ILS/src/sql/Pg/090.schema.action.sql | 16 +
Open-ILS/src/sql/Pg/950.data.seed-values.sql | 98 ++-
.../src/sql/Pg/upgrade/XXXX.data.hold_buckets.sql | 125 +++
.../opac/myopac/hold_subscriptions.tt2 | 51 ++
.../templates-bootstrap/opac/parts/myopac/base.tt2 | 3 +
.../templates-bootstrap/opac/parts/place_hold.tt2 | 22 +
Open-ILS/src/templates/opac/myopac/ebook_holds.tt2 | 5 +
.../templates/opac/myopac/ebook_holds_ready.tt2 | 5 +
.../src/templates/opac/myopac/hold_history.tt2 | 5 +
.../templates/opac/myopac/hold_subscriptions.tt2 | 76 ++
Open-ILS/src/templates/opac/myopac/holds.tt2 | 5 +
Open-ILS/src/templates/opac/parts/place_hold.tt2 | 26 +-
.../staff/cat/bucket/batch_hold/index.tt2 | 82 ++
.../staff/cat/bucket/batch_hold/t_bucket_info.tt2 | 21 +
.../staff/cat/bucket/batch_hold/t_event.tt2 | 42 +
.../staff/cat/bucket/batch_hold/t_event_create.tt2 | 30 +
.../staff/cat/bucket/batch_hold/t_grid_menu.tt2 | 20 +
.../staff/cat/bucket/batch_hold/t_list.tt2 | 22 +
.../staff/cat/bucket/batch_hold/t_pending.tt2 | 58 ++
.../staff/cat/bucket/batch_hold/t_view.tt2 | 29 +
.../staff/cat/bucket/share/t_bucket_create.tt2 | 13 +-
.../staff/cat/bucket/share/t_bucket_edit.tt2 | 11 +-
Open-ILS/src/templates/staff/circ/patron/index.tt2 | 8 +-
.../staff/circ/patron/t_hold_subscriptions.tt2 | 21 +
.../staff/circ/patron/t_search_results.tt2 | 4 +
Open-ILS/src/templates/staff/navbar.tt2 | 6 +
Open-ILS/web/js/ui/default/opac/staff.js | 132 ++-
.../ui/default/staff/cat/bucket/batch_hold/app.js | 889 +++++++++++++++++++++
.../web/js/ui/default/staff/circ/patron/app.js | 85 +-
.../js/ui/default/staff/circ/patron/bucket/app.js | 2 +-
.../js/ui/default/staff/services/user-bucket.js | 14 +
.../Circulation/hold-subscriptions.adoc | 26 +
38 files changed, 2356 insertions(+), 83 deletions(-)
create mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX.data.hold_buckets.sql
create mode 100644 Open-ILS/src/templates-bootstrap/opac/myopac/hold_subscriptions.tt2
create mode 100644 Open-ILS/src/templates/opac/myopac/hold_subscriptions.tt2
create mode 100644 Open-ILS/src/templates/staff/cat/bucket/batch_hold/index.tt2
create mode 100644 Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_bucket_info.tt2
create mode 100644 Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_event.tt2
create mode 100644 Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_event_create.tt2
create mode 100644 Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_grid_menu.tt2
create mode 100644 Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_list.tt2
create mode 100644 Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_pending.tt2
create mode 100644 Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_view.tt2
create mode 100644 Open-ILS/src/templates/staff/circ/patron/t_hold_subscriptions.tt2
create mode 100644 Open-ILS/web/js/ui/default/staff/cat/bucket/batch_hold/app.js
create mode 100644 docs/RELEASE_NOTES_NEXT/Circulation/hold-subscriptions.adoc
diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 7913925a1a..42cad63635 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -7557,6 +7557,47 @@ SELECT usr,
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Container.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Container.pm
index e468a0b789..46b988829f 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Container.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Container.pm
@@ -142,7 +142,10 @@ sub _bucket_flesh {
unless($bkt->owner eq $e->requestor->id) {
my $owner = $e->retrieve_actor_user($bkt->owner)
or return $e->die_event;
- return $e->event unless $e->allowed('VIEW_CONTAINER', $owner->home_ou);
+ return $e->event unless (
+ $e->allowed('VIEW_CONTAINER', $owner->home_ou) or
+ $e->allowed('VIEW_CONTAINER', $bkt->owning_lib)
+ );
}
}
@@ -335,6 +338,7 @@ __PACKAGE__->register_method(
{desc => 'Authentication token', type => 'string'},
{desc => 'Container class. Can be "copy", "callnumber", "biblio", or "user"', type => 'string'},
{desc => 'Item or items. Can either be a single container item object, or an array of them', type => 'object'},
+ {desc => 'Duplicate check. Avoid adding an item that is already in a container', type => 'bool'},
],
return => {
desc => 'The ID of the newly created item(s). In batch context, an array of IDs is returned'
@@ -344,7 +348,7 @@ __PACKAGE__->register_method(
sub item_create {
- my( $self, $client, $authtoken, $class, $item ) = @_;
+ my( $self, $client, $authtoken, $class, $item, $dupe_check ) = @_;
my $e = new_editor(xact=>1, authtoken=>$authtoken);
return $e->die_event unless $e->checkauth;
@@ -369,21 +373,45 @@ sub item_create {
my $stat;
if( $class eq 'copy' ) {
+ next if (
+ $dupe_check &&
+ $e->search_container_copy_bucket_item(
+ {bucket => $one_item->bucket, target_copy => $one_item->target_copy}
+ )->[0]
+ );
return $e->die_event unless
$stat = $e->create_container_copy_bucket_item($one_item);
}
if( $class eq 'callnumber' ) {
+ next if (
+ $dupe_check &&
+ $e->search_container_call_number_bucket_item(
+ {bucket => $one_item->bucket, target_call_number => $one_item->target_call_number}
+ )->[0]
+ );
return $e->die_event unless
$stat = $e->create_container_call_number_bucket_item($one_item);
}
if( $class eq 'biblio' ) {
+ next if (
+ $dupe_check &&
+ $e->search_container_biblio_record_entry_bucket_item(
+ {bucket => $one_item->bucket, target_biblio_record_entry => $one_item->target_biblio_record_entry}
+ )->[0]
+ );
return $e->die_event unless
$stat = $e->create_container_biblio_record_entry_bucket_item($one_item);
}
if( $class eq 'user') {
+ next if (
+ $dupe_check &&
+ $e->search_container_user_bucket_item(
+ {bucket => $one_item->bucket, target_user => $one_item->target_user}
+ )->[0]
+ );
return $e->die_event unless
$stat = $e->create_container_user_bucket_item($one_item);
}
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
index 25f2633487..d0b5136640 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
@@ -17,6 +17,7 @@
package OpenILS::Application::Circ::Holds;
use base qw/OpenILS::Application/;
use strict; use warnings;
+use List::Util qw(shuffle);
use OpenILS::Application::AppUtils;
use DateTime;
use Data::Dumper;
@@ -165,6 +166,249 @@ sub test_and_create_hold_batch {
return undef;
}
+__PACKAGE__->register_method(
+ method => "test_and_create_batch_hold_event",
+ api_name => "open-ils.circ.holds.test_and_create.subscription_batch",
+ stream => 1,
+ signature => {
+ desc => q/This is for batch creating a set of holds where every field is identical except for the target users./,
+ params => [
+ { desc => 'Authentication token', type => 'string' },
+ { desc => 'Hash of named parameters. Same as for open-ils.circ.title_hold.is_possible, though the pertinent target field is automatically populated based on the hold_type and the specified list of target users.', type => 'object'},
+ { desc => 'Container ID of the user bucket holding target users', type => 'number' },
+ { desc => 'target object ID (clarified by hold_type in param hash)', type => 'number' },
+ { desc => 'Randomize flag, set to 0 for "not randomized"', type => 'bool' }
+ ],
+ return => {
+ desc => 'Stream of objects structured as {total=>X, count=>Y, target=>Z, patronid=>A, result=>$hold_id} on success, -1 on missing arg, event (or stream of events on "result" key of object) on error(s)',
+ },
+ }
+);
+
+__PACKAGE__->register_method(
+ method => "test_and_create_batch_hold_event",
+ api_name => "open-ils.circ.holds.test_and_create.subscription_batch.override",
+ stream => 1,
+ signature => {
+ desc => '@see open-ils.circ.holds.test_and_create.subscription_batch',
+ }
+);
+
+
+sub test_and_create_batch_hold_event {
+ my( $self, $conn, $auth, $params, $target_bucket, $target_id, $randomize, $oargs ) = @_;
+
+
+ $randomize //= 1; # default to random hold creation order
+ $$params{hold_type} //= 'T'; # default to title holds
+
+ my $override = 0;
+ if ($self->api_name =~ /override/) {
+ $override = 1;
+ $oargs = { all => 1 } unless defined $oargs;
+ $$params{oargs} = $oargs; # for is_possible checking.
+ }
+
+ my $e = new_editor(authtoken=>$auth);
+ return $e->die_event unless $e->checkauth;
+ $$params{'requestor'} = $e->requestor->id;
+
+
+ my $org = $e->requestor->ws_ou || $e->requestor->home_ou;
+ # the perm locaiton shouldn't really matter here since holds
+ # will exist all over and MANAGE_HOLD_GROUPS should be universal
+ my $evt = $U->check_perms($e->requestor->id, $org, 'MANAGE_HOLD_GROUPS');
+ return $evt if $evt;
+
+ my $rand_setting = $U->ou_ancestor_setting_value($org, 'holds.subscription.randomize');
+ $randomize = $rand_setting if (defined $rand_setting);
+
+ my $target_field;
+ if ($$params{'hold_type'} eq 'T') { $target_field = 'titleid'; }
+ elsif ($$params{'hold_type'} eq 'C') { $target_field = 'copy_id'; }
+ elsif ($$params{'hold_type'} eq 'R') { $target_field = 'copy_id'; }
+ elsif ($$params{'hold_type'} eq 'F') { $target_field = 'copy_id'; }
+ elsif ($$params{'hold_type'} eq 'I') { $target_field = 'issuanceid'; }
+ elsif ($$params{'hold_type'} eq 'V') { $target_field = 'volume_id'; }
+ elsif ($$params{'hold_type'} eq 'M') { $target_field = 'mrid'; }
+ elsif ($$params{'hold_type'} eq 'P') { $target_field = 'partid'; }
+ else { return undef; }
+
+ # Check for a valid record.
+ # XXX For now, because only title holds are allowed, we'll add only that check.
+ my $target_check = $e->json_query({
+ select => {bre => ['id']},
+ from => 'bre',
+ where => {deleted => 'f', id => $target_id}
+ });
+ return {error=>'invalid_target'} if (!@$target_check);
+
+ my $formats_map = delete($$params{holdable_formats_map}) || {};
+
+ my $target_list = $e->search_container_user_bucket_item({bucket => $target_bucket});
+ @$target_list = shuffle(@$target_list) if $randomize;
+
+ # Record the request...
+ $e->xact_begin;
+ my $bhe = Fieldmapper::action::batch_hold_event->new;
+ $bhe->isnew(1);
+ $bhe->staff($e->requestor->id);
+ $bhe->bucket($target_bucket);
+ $bhe->target($target_id);
+ $bhe->hold_type($$params{hold_type});
+ $bhe = $e->create_action_batch_hold_event($bhe) or return $e->die_event;
+ $e->xact_commit;
+
+ my $total = scalar(@$target_list);
+ my $count = 0;
+ $conn->respond({total => $total, count => $count});
+
+ my $triggers = OpenSRF::AppSession->connect('open-ils.trigger');
+ foreach (@$target_list) {
+ $count++;
+ $$params{$target_field} = $target_id;
+ $$params{patronid} = $_->target_user;
+
+ my $usr = $e->retrieve_actor_user([
+ $$params{patronid},
+ {
+ flesh => 1,
+ flesh_fields => {
+ au => ['settings']
+ }
+ }
+ ]);
+ my $user_setting_map = {
+ map { $_->name => OpenSRF::Utils::JSON->JSON2perl($_->value) }
+ @{ $usr->settings }
+ };
+
+ $$params{pickup_lib} = $$user_setting_map{'opac.default_pickup_location'} || $usr->home_ou;
+
+ if ($user_setting_map->{'opac.hold_notify'} =~ /email/) {
+ $$params{email_notify} = 1;
+ } else {
+ delete $$params{email_notify};
+ }
+
+ if ($user_setting_map->{'opac.default_phone'} && $user_setting_map->{'opac.hold_notify'} =~ /phone/) {
+ $$params{phone_notify} = $user_setting_map->{'opac.default_phone'};
+ } else {
+ delete $$params{phone_notify};
+ }
+
+ if ($user_setting_map->{'opac.default_sms_carrier'}
+ && $user_setting_map->{'opac.default_sms_notify'}
+ && $user_setting_map->{'opac.hold_notify'} =~ /sms/) {
+ $$params{sms_carrier} = $user_setting_map->{'opac.default_sms_carrier'};
+ $$params{sms_notify} = $user_setting_map->{'opac.default_sms_notify'};
+ } else {
+ delete $$params{sms_carrier};
+ delete $$params{sms_notify};
+ }
+
+ # copy the requested formats from the target->formats map
+ # into the top-level formats attr for each hold ... empty for now, T holds only
+ $$params{holdable_formats} = $formats_map->{$_};
+
+ my $res;
+ ($res) = $self->method_lookup(
+ 'open-ils.circ.title_hold.is_possible')->run($auth, $params, $override ? $oargs : {});
+ if ($res->{'success'} == 1) {
+
+ $params->{'depth'} = $res->{'depth'} if $res->{'depth'};
+
+ # Remove oargs from params so holds can be created.
+ if ($$params{oargs}) {
+ delete $$params{oargs};
+ }
+
+ my $ahr = construct_hold_request_object($params);
+ my ($res2) = $self->method_lookup(
+ $override
+ ? 'open-ils.circ.holds.create.override'
+ : 'open-ils.circ.holds.create'
+ )->run($auth, $ahr, $oargs);
+ $res2 = {
+ total => $total, count => $count,
+ 'patronid' => $$params{patronid},
+ 'target' => $$params{$target_field},
+ 'result' => $res2
+ };
+ $conn->respond($res2);
+
+ unless (ref($res2->{result})) { # success returns a hold id only
+ $e->xact_begin;
+ my $bhem = Fieldmapper::action::batch_hold_event_map->new;
+ $bhem->isnew(1);
+ $bhem->batch_hold_event($bhe->id);
+ $bhem->hold($res2->{result});
+ $e->create_action_batch_hold_event_map($bhem) or return $e->die_event;
+ $e->xact_commit;
+
+ my $hold = $e->retrieve_action_hold_request($bhem->hold);
+ $triggers->request('open-ils.trigger.event.autocreate', 'hold_request.success', $hold, $hold->pickup_lib);
+ }
+
+ } else {
+ $res = {
+ total => $total, count => $count,
+ 'target' => $$params{$target_field},
+ 'failedpatronid' => $$params{patronid},
+ 'result' => $res
+ };
+ $conn->respond($res);
+ }
+ }
+ $triggers->kill_me;
+ return undef;
+}
+
+__PACKAGE__->register_method(
+ method => "rollback_batch_hold_event",
+ api_name => "open-ils.circ.holds.rollback.subscription_batch",
+ stream => 1,
+ signature => {
+ desc => q/This is for batch creating a set of holds where every field is identical except for the target users./,
+ params => [
+ { desc => 'Authentication token', type => 'string' },
+ { desc => 'Hold Group Event ID to roll back', type => 'number' },
+ ],
+ return => {
+ desc => 'Stream of objects structured as {total=>X, count=>Y} on success, event on error',
+ },
+ }
+);
+
+sub rollback_batch_hold_event {
+ my( $self, $conn, $auth, $event_id ) = @_;
+
+ my $e = new_editor(authtoken=>$auth,xact=>1);
+ return $e->die_event unless $e->checkauth;
+
+ my $org = $e->requestor->ws_ou || $e->requestor->home_ou;
+ my $evt = $U->check_perms($e->requestor->id, $org, 'MANAGE_HOLD_GROUPS');
+ return $evt if $evt;
+
+ my $batch_event = $e->retrieve_action_batch_hold_event($event_id);
+ my $target_list = $e->search_action_batch_hold_event_map({batch_hold_event => $event_id});
+
+ my $total = scalar(@$target_list);
+ my $count = 0;
+ $conn->respond({total => $total, count => $count});
+
+ for my $target (@$target_list) {
+ $count++;
+ $self->method_lookup('open-ils.circ.hold.cancel')->run($auth, $target->hold, 8);
+ $conn->respond({ total => $total, count => $count });
+ }
+
+ $batch_event->cancelled('now');
+ $e->update_action_batch_hold_event($batch_event);
+ $e->commit;
+ return undef;
+}
+
sub construct_hold_request_object {
my ($params) = @_;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
index 111ea8b4d8..ef231feabe 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
@@ -253,6 +253,7 @@ sub load {
$self->load_current_curbside_libs;
return $self->load_myopac_holds if $path =~ m|opac/myopac/holds|;
+ return $self->load_myopac_hold_subscriptions if $path =~ m|opac/myopac/hold_subscriptions|;
return $self->load_myopac_circs if $path =~ m|opac/myopac/circs|;
return $self->load_myopac_messages if $path =~ m|opac/myopac/messages|;
return $self->load_myopac_payment_form if $path =~ m|opac/myopac/main_payment_form|;
@@ -409,6 +410,8 @@ sub load_common {
$self->staff_saved_searches_set_expansion_state if $ctx->{is_staff};
$self->load_eg_cache_hash;
$self->load_copy_location_groups;
+ $self->load_my_hold_subscriptions;
+ $self->load_hold_subscriptions if $ctx->{is_staff};
$self->staff_saved_searches_set_expansion_state if $ctx->{is_staff};
$self->load_search_filter_groups($ctx->{search_ou});
$self->load_org_util_funcs;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
index 4da79eb67d..f77c1e0a8c 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
@@ -1430,6 +1430,30 @@ sub load_myopac_holds {
return defined($hold_handle_result) ? $hold_handle_result : Apache2::Const::OK;
}
+sub load_myopac_hold_subscriptions {
+ my $self = shift;
+ my $e = $self->editor;
+ my $ctx = $self->ctx;
+
+ my $sub_remove = $self->cgi->param('remove');
+
+ if ($sub_remove and $ctx->{user}->id) {
+ my $sub_entries = $self->editor->search_container_user_bucket_item(
+ { bucket => $sub_remove, target_user => $ctx->{user}->id }
+ );
+
+ $self->editor->xact_begin;
+ $self->editor->delete_container_user_bucket_item($_) for @$sub_entries;
+ $self->editor->xact_commit;
+
+ return $self->generic_redirect(
+ $self->ctx->{proto} . '://' . $self->ctx->{hostname} . $self->ctx->{opac_root} . '/myopac/hold_subscriptions'
+ );
+ }
+
+ return Apache2::Const::OK;
+}
+
my $data_filler;
sub load_place_hold {
@@ -1443,6 +1467,7 @@ sub load_place_hold {
my @targets = uniq $cgi->param('hold_target');
my @parts = $cgi->param('part');
+ $ctx->{hold_subscription} = $cgi->param('hold_subscription');
$ctx->{hold_type} = $cgi->param('hold_type');
$ctx->{default_pickup_lib} = $e->requestor->home_ou; # unless changed below
$ctx->{email_notify} = $cgi->param('email_notify');
@@ -1671,8 +1696,8 @@ sub load_place_hold {
$_->{marc_xml} = XML::LibXML->new->parse_string($_->{record}->marc) for @hold_data;
my $pickup_lib = $cgi->param('pickup_lib');
- # no pickup lib means no holds placement
- return Apache2::Const::OK unless $pickup_lib;
+ # no pickup lib means no holds placement, except for subscriptions
+ return Apache2::Const::OK unless $pickup_lib || $ctx->{hold_subscription};
$ctx->{hold_attempt_made} = 1;
@@ -1685,17 +1710,26 @@ sub load_place_hold {
my $usr = $e->requestor->id;
- if ($ctx->{is_staff} and !$cgi->param("hold_usr_is_requestor")) {
- # find the real hold target
+ if ($ctx->{is_staff}) {
+ $logger->info("Staff initiated hold");
+ if (!$cgi->param("hold_usr_is_requestor")) {
+ # find the real hold target
- $usr = $U->simplereq(
- 'open-ils.actor',
- "open-ils.actor.user.retrieve_id_by_barcode_or_username",
- $e->authtoken, $cgi->param("hold_usr"));
+ $usr = $U->simplereq(
+ 'open-ils.actor',
+ "open-ils.actor.user.retrieve_id_by_barcode_or_username",
+ $e->authtoken, $cgi->param("hold_usr"));
+
+ if (defined $U->event_code($usr)) {
+ $ctx->{hold_failed} = 1;
+ $ctx->{hold_failed_event} = $usr;
+ }
+ }
- if (defined $U->event_code($usr)) {
- $ctx->{hold_failed} = 1;
- $ctx->{hold_failed_event} = $usr;
+ if ($ctx->{hold_subscription}) {
+ # this is a batch event, hold "user" is a bucket id
+ $logger->info("Hold Group Event requested for user bucket: " . $ctx->{hold_subscription});
+ $usr = $e->retrieve_container_user_bucket($ctx->{hold_subscription});
}
}
@@ -1761,22 +1795,32 @@ sub attempt_hold_placement {
my $ctx = $self->ctx;
my $e = $self->editor;
+ my $user_container = undef;
+ if (ref($usr)) { # $usr is actually a container for a subscription...
+ $user_container = $usr->id;
+ return unless ($hold_type eq 'T'); # Only T-hold subscriptions for now.
+ }
+
# First see if we should warn/block for any holds that
- # might have locally available items.
- for my $hdata (@hold_data) {
- my ($local_block, $local_alert) = $self->local_avail_concern(
- $hdata->{target_id}, $hold_type, $pickup_lib);
+ # might have locally available items for non-subscriptions.
+ if (!$user_container) {
+ for my $hdata (@hold_data) {
+ my ($local_block, $local_alert) = $self->local_avail_concern(
+ $hdata->{target_id}, $hold_type, $pickup_lib);
- if ($local_block) {
- $hdata->{hold_failed} = 1;
- $hdata->{hold_local_block} = 1;
- } elsif ($local_alert) {
- $hdata->{hold_failed} = 1;
- $hdata->{hold_local_alert} = 1;
+ if ($local_block) {
+ $hdata->{hold_failed} = 1;
+ $hdata->{hold_local_block} = 1;
+ } elsif ($local_alert) {
+ $hdata->{hold_failed} = 1;
+ $hdata->{hold_local_alert} = 1;
+ }
}
}
- my $method = 'open-ils.circ.holds.test_and_create.batch';
+ my $method = $user_container
+ ? 'open-ils.circ.holds.test_and_create.subscription_batch'
+ : 'open-ils.circ.holds.test_and_create.batch';
if ($cgi->param('override')) {
$method .= '.override';
@@ -1801,17 +1845,31 @@ sub attempt_hold_placement {
}
my $bses = OpenSRF::AppSession->create('open-ils.circ');
- my $breq = $bses->request(
- $method,
- $e->authtoken,
- $data_filler->({
- patronid => $usr,
- pickup_lib => $pickup_lib,
- hold_type => $hold_type,
- holdable_formats_map => $holdable_formats,
- }),
- \@create_targets
- );
+
+ my @create_params = ();
+
+ if ($user_container) {
+ @create_params = (
+ $data_filler->({
+ hold_type => $hold_type,
+ holdable_formats_map => $holdable_formats, # currently always unset, only T holds
+ }),
+ $user_container,
+ $create_targets[0],
+ );
+ } else {
+ @create_params = (
+ $data_filler->({
+ patronid => $usr,
+ pickup_lib => $pickup_lib,
+ hold_type => $hold_type,
+ holdable_formats_map => $holdable_formats,
+ }),
+ \@create_targets
+ );
+ }
+
+ my $breq = $bses->request($method, $e->authtoken, @create_params);
while (my $resp = $breq->recv) {
@@ -1823,6 +1881,13 @@ sub attempt_hold_placement {
last;
}
+ # subscription batch create sends an initial response to assist with client-side counting
+ next if (
+ $user_container and
+ defined($$resp{count}) and $$resp{count} == 0
+ and defined($$resp{total}) and $$resp{total} > 0
+ );
+
# Skip those that had the hold_success or hold_failed fields set for duplicate holds placement.
my ($hdata) = grep {$_->{target_id} eq $resp->{target} && !($_->{hold_failed} || $_->{hold_success})} @hold_data;
my $result = $resp->{result};
@@ -1836,7 +1901,6 @@ sub attempt_hold_placement {
if(not ref $result and $result > 0) {
# successul hold returns the hold ID
-
$hdata->{hold_success} = $result;
} else {
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
index 730586a128..f741bcaa01 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
@@ -3,6 +3,7 @@ use strict; use warnings;
use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
use File::Spec;
use Time::HiRes qw/time sleep/;
+use List::MoreUtils qw(uniq);
use OpenSRF::Utils::Cache;
use OpenSRF::Utils::Logger qw/$logger/;
use OpenILS::Utils::CStoreEditor qw/:funcs/;
@@ -701,6 +702,40 @@ sub load_copy_location_groups {
$ctx->{copy_location_groups} = \%buckets;
}
+sub load_hold_subscriptions {
+ my $self = shift;
+ my $ctx = $self->ctx;
+
+ return unless $ctx->{authtoken};
+
+ my $e = new_editor(authtoken => $ctx->{authtoken});
+ $e->personality('open-ils.pcrud'); # use pcrud mode to filter appropriately
+
+ $ctx->{hold_subscriptions} =
+ $e->search_container_user_bucket([
+ { btype => 'hold_subscription' },
+ { order_by => {cub => 'name'} }
+ ]);
+
+}
+
+sub load_my_hold_subscriptions {
+ my $self = shift;
+ my $ctx = $self->ctx;
+
+ return unless $ctx->{authtoken};
+
+ my $sub_entries = $self->editor->search_container_user_bucket_item(
+ { target_user => $ctx->{user}->id }
+ );
+
+ my $sub_ids = [ uniq map { $_->bucket } @$sub_entries ];
+ $ctx->{my_hold_subscriptions} = scalar(@$sub_ids) ?
+ $self->editor->search_container_user_bucket(
+ {btype => 'hold_subscription', id => $sub_ids, pub => 't'}
+ ) : [];
+}
+
sub set_file_download_headers {
my $self = shift;
my $filename = shift;
diff --git a/Open-ILS/src/sql/Pg/090.schema.action.sql b/Open-ILS/src/sql/Pg/090.schema.action.sql
index 960e6ee840..813ab0ba98 100644
--- a/Open-ILS/src/sql/Pg/090.schema.action.sql
+++ b/Open-ILS/src/sql/Pg/090.schema.action.sql
@@ -1762,5 +1762,21 @@ CREATE TABLE action.curbside (
notes TEXT
);
+CREATE TABLE action.batch_hold_event (
+ id SERIAL PRIMARY KEY,
+ staff INT NOT NULL REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ bucket INT NOT NULL REFERENCES container.user_bucket (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ target INT NOT NULL,
+ hold_type TEXT NOT NULL DEFAULT 'T', -- maybe different hold types in the future...
+ run_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+ cancelled TIMESTAMP WITH TIME ZONE
+);
+
+CREATE TABLE action.batch_hold_event_map (
+ id SERIAL PRIMARY KEY,
+ batch_hold_event INT NOT NULL REFERENCES action.batch_hold_event (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ hold INT NOT NULL REFERENCES action.hold_request (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
COMMIT;
diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 781781b649..55072f46f0 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -1950,7 +1950,9 @@ INSERT INTO permission.perm_list ( id, code, description ) VALUES
( 626, 'VIEW_BOOKING_RESERVATION_ATTR_MAP', oils_i18n_gettext(626,
'View booking reservation attribute maps', 'ppl', 'description')),
( 627, 'SSO_ADMIN', oils_i18n_gettext(627,
- 'Modify patron SSO settings', 'ppl', 'description'))
+ 'Modify patron SSO settings', 'ppl', 'description')),
+ ( 628, 'MANAGE_HOLD_GROUPS', oils_i18n_gettext(628,
+ 'Manage batch (subscription) hold events', 'ppl', 'description'))
;
@@ -2900,6 +2902,7 @@ INSERT INTO action.hold_request_cancel_cause (id,label) VALUES (4, oils_i18n_get
INSERT INTO action.hold_request_cancel_cause (id,label) VALUES (5, oils_i18n_gettext(5, 'Staff forced', 'ahrcc', 'label'));
INSERT INTO action.hold_request_cancel_cause (id,label) VALUES (6, oils_i18n_gettext(6, 'Patron via OPAC', 'ahrcc', 'label'));
INSERT INTO action.hold_request_cancel_cause (id,label) VALUES (7, oils_i18n_gettext(7, 'Patron via SIP', 'ahrcc', 'label'));
+INSERT INTO action.hold_request_cancel_cause (id,label) VALUES (8, oils_i18n_gettext(8, 'Hold Group Event rollback', 'ahrcc', 'label'));
SELECT SETVAL('action.hold_request_cancel_cause_id_seq', 100);
@@ -5838,6 +5841,7 @@ INSERT INTO container.user_bucket_type (code,label) VALUES ('folks:hold.view', o
INSERT INTO container.user_bucket_type (code,label) VALUES ('folks:hold.cancel', oils_i18n_gettext('folks:hold.cancel', 'Cancel Holds', 'cubt', 'label'));
INSERT INTO container.user_bucket_type (code,label) SELECT code,label FROM container.copy_bucket_type where code = 'staff_client';
+INSERT INTO container.user_bucket_type (code,label) VALUES ('hold_subscription', oils_i18n_gettext('hold_subscription', 'Hold Group Container', 'cubt', 'label'));
----------------------------------
-- MARC21 record structure data --
@@ -17372,6 +17376,78 @@ VALUES (currval('action_trigger.event_definition_id_seq'), 'home_ou'),
(currval('action_trigger.event_definition_id_seq'), 'home_ou.mailing_address'),
(currval('action_trigger.event_definition_id_seq'), 'home_ou.billing_address');
+INSERT INTO action_trigger.event_definition (active, owner, name, hook, validator, reactor, delay, delay_field, group_field, cleanup_success, template)
+ VALUES ('f', 1, 'Hold Group Hold Placed for Patron Email Notification', 'hold_request.success', 'NOOP_True', 'SendEmail', '30 minutes', 'request_time', 'usr', 'CreateHoldNotification',
+$$
+[%- USE date -%]
+[%- user = target.0.usr -%]
+To: [%- params.recipient_email || user.email %]
+From: [%- params.sender_email || default_sender %]
+Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %]
+Subject: Subcription Hold placed for you
+Auto-Submitted: auto-generated
+
+Dear [% user.family_name %], [% user.first_given_name %]
+The following items have been placed on hold for you:
+
+[% FOR hold IN target %]
+ [%- copy_details = helpers.get_copy_bib_basics(hold.current_copy.id) -%]
+ Title: [% copy_details.title %]
+ Author: [% copy_details.author %]
+ Call Number: [% hold.current_copy.call_number.label %]
+ Barcode: [% hold.current_copy.barcode %]
+ Library: [% hold.pickup_lib.name %]
+[% END %]
+
+$$);
+
+INSERT INTO action_trigger.environment (event_def, path ) VALUES
+( currval('action_trigger.event_definition_id_seq'), 'usr' ),
+( currval('action_trigger.event_definition_id_seq'), 'pickup_lib' ),
+( currval('action_trigger.event_definition_id_seq'), 'current_copy.call_number' );
+
+INSERT INTO action_trigger.event_definition (
+ active, owner, name, hook, validator, reactor, cleanup_success,
+ delay, delay_field, group_field, template
+) VALUES (
+ false, 1, 'Hold Group Hold Placed for Patron SMS Notification', 'hold_request.success', 'NOOP_True',
+ 'SendSMS', 'CreateHoldNotification', '00:30:00', 'shelf_time', 'sms_notify',
+ '[%- USE date -%]
+[%- user = target.0.usr -%]
+From: [%- params.sender_email || default_sender %]
+Date: [%- date.format(date.now, ''%a, %d %b %Y %T -0000'', gmt => 1) %]
+To: [%- params.recipient_email || helpers.get_sms_gateway_email(target.0.sms_carrier,target.0.sms_notify) %]
+Subject: [% target.size %] subscription hold(s) placed for you
+Auto-Submitted: auto-generated
+
+[% FOR hold IN target %][%-
+ bibxml = helpers.xml_doc( hold.current_copy.call_number.record.marc );
+ title = "";
+ FOR part IN bibxml.findnodes(''//*[@tag="245"]/*[@code="a"]'');
+ title = title _ part.textContent;
+ END;
+ author = bibxml.findnodes(''//*[@tag="100"]/*[@code="a"]'').textContent;
+%][% hold.usr.first_given_name %]:[% title %] @ [% hold.pickup_lib.name %]
+[% END %]
+'
+);
+
+INSERT INTO action_trigger.environment (
+ event_def,
+ path
+) VALUES (
+ currval('action_trigger.event_definition_id_seq'),
+ 'current_copy.call_number.record.simple_record'
+), (
+ currval('action_trigger.event_definition_id_seq'),
+ 'usr'
+), (
+ currval('action_trigger.event_definition_id_seq'),
+ 'pickup_lib.billing_address'
+);
+
+INSERT INTO action_trigger.event_params (event_def, param, value)
+ VALUES (currval('action_trigger.event_definition_id_seq'), 'check_sms_notify', 1);
INSERT INTO config.org_unit_setting_type
(name, grp, label, description, datatype)
@@ -20348,6 +20424,26 @@ VALUES
'string', 627)
;
+INSERT INTO config.org_unit_setting_type
+ (name, label, description, grp, datatype)
+VALUES (
+ 'holds.subscription.randomize',
+ oils_i18n_gettext(
+ 'holds.subscription.randomize',
+ 'Randomize group hold order',
+ 'coust',
+ 'label'
+ ),
+ oils_i18n_gettext(
+ 'holds.subscription.randomize',
+ 'When placing a batch group hold, randomize the order of the patrons receiving the holds so they are not always in the same order.',
+ 'coust',
+ 'description'
+ ),
+ 'holds',
+ 'bool'
+);
+
INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
VALUES (
'eg.print.config.default', 'gui', 'object',
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.hold_buckets.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.hold_buckets.sql
new file mode 100644
index 0000000000..1260f6e215
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.hold_buckets.sql
@@ -0,0 +1,125 @@
+
+BEGIN;
+
+CREATE TABLE action.batch_hold_event (
+ id SERIAL PRIMARY KEY,
+ staff INT NOT NULL REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ bucket INT NOT NULL REFERENCES container.user_bucket (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ target INT NOT NULL,
+ hold_type TEXT NOT NULL DEFAULT 'T', -- maybe different hold types in the future...
+ run_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+ cancelled TIMESTAMP WITH TIME ZONE
+);
+
+CREATE TABLE action.batch_hold_event_map (
+ id SERIAL PRIMARY KEY,
+ batch_hold_event INT NOT NULL REFERENCES action.batch_hold_event (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ hold INT NOT NULL REFERENCES action.hold_request (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+INSERT INTO container.user_bucket_type (code,label) VALUES ('hold_subscription','Hold Group Container');
+
+INSERT INTO config.org_unit_setting_type
+ (name, label, description, grp, datatype)
+VALUES (
+ 'holds.subscription.randomize',
+ oils_i18n_gettext(
+ 'holds.subscription.randomize',
+ 'Randomize group hold order',
+ 'coust',
+ 'label'
+ ),
+ oils_i18n_gettext(
+ 'holds.subscription.randomize',
+ 'When placing a batch group hold, randomize the order of the patrons receiving the holds so they are not always in the same order.',
+ 'coust',
+ 'description'
+ ),
+ 'holds',
+ 'bool'
+);
+
+-- Committer! Verify permission id before pushing!
+INSERT INTO permission.perm_list (id,code,description)
+ VALUES ( 623, 'MANAGE_HOLD_GROUPS', oils_i18n_gettext(623, 'Manage hold groups and hold group events', 'ppl', 'description'));
+
+INSERT INTO action.hold_request_cancel_cause (id,label)
+ VALUES ( 8, oils_i18n_gettext(8, 'Hold Group Event rollback', 'ahrcc', 'label'));
+
+INSERT INTO action_trigger.event_definition (active, owner, name, hook, validator, reactor, delay, delay_field, group_field, cleanup_success, template)
+ VALUES ('f', 1, 'Hold Group Hold Placed for Patron Email Notification', 'hold_request.success', 'NOOP_True', 'SendEmail', '30 minutes', 'request_time', 'usr', 'CreateHoldNotification',
+$$
+[%- USE date -%]
+[%- user = target.0.usr -%]
+To: [%- params.recipient_email || user.email %]
+From: [%- params.sender_email || default_sender %]
+Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %]
+Subject: Subcription Hold placed for you
+Auto-Submitted: auto-generated
+
+Dear [% user.family_name %], [% user.first_given_name %]
+The following items have been placed on hold for you:
+
+[% FOR hold IN target %]
+ [%- copy_details = helpers.get_copy_bib_basics(hold.current_copy.id) -%]
+ Title: [% copy_details.title %]
+ Author: [% copy_details.author %]
+ Call Number: [% hold.current_copy.call_number.label %]
+ Barcode: [% hold.current_copy.barcode %]
+ Library: [% hold.pickup_lib.name %]
+[% END %]
+
+$$);
+
+INSERT INTO action_trigger.environment (event_def, path ) VALUES
+( currval('action_trigger.event_definition_id_seq'), 'usr' ),
+( currval('action_trigger.event_definition_id_seq'), 'pickup_lib' ),
+( currval('action_trigger.event_definition_id_seq'), 'current_copy.call_number' );
+
+
+INSERT INTO action_trigger.event_definition (
+ active, owner, name, hook, validator, reactor, cleanup_success,
+ delay, delay_field, group_field, template
+) VALUES (
+ false, 1, 'Hold Group Hold Placed for Patron SMS Notification', 'hold_request.success', 'NOOP_True',
+ 'SendSMS', 'CreateHoldNotification', '00:30:00', 'shelf_time', 'sms_notify',
+ '[%- USE date -%]
+[%- user = target.0.usr -%]
+From: [%- params.sender_email || default_sender %]
+Date: [%- date.format(date.now, ''%a, %d %b %Y %T -0000'', gmt => 1) %]
+To: [%- params.recipient_email || helpers.get_sms_gateway_email(target.0.sms_carrier,target.0.sms_notify) %]
+Subject: [% target.size %] subscription hold(s) placed for you
+Auto-Submitted: auto-generated
+
+[% FOR hold IN target %][%-
+ bibxml = helpers.xml_doc( hold.current_copy.call_number.record.marc );
+ title = "";
+ FOR part IN bibxml.findnodes(''//*[@tag="245"]/*[@code="a"]'');
+ title = title _ part.textContent;
+ END;
+ author = bibxml.findnodes(''//*[@tag="100"]/*[@code="a"]'').textContent;
+%][% hold.usr.first_given_name %]:[% title %] @ [% hold.pickup_lib.name %]
+[% END %]
+'
+);
+
+INSERT INTO action_trigger.environment (
+ event_def,
+ path
+) VALUES (
+ currval('action_trigger.event_definition_id_seq'),
+ 'current_copy.call_number.record.simple_record'
+), (
+ currval('action_trigger.event_definition_id_seq'),
+ 'usr'
+), (
+ currval('action_trigger.event_definition_id_seq'),
+ 'pickup_lib.billing_address'
+);
+
+INSERT INTO action_trigger.event_params (event_def, param, value)
+ VALUES (currval('action_trigger.event_definition_id_seq'), 'check_sms_notify', 1);
+
+
+COMMIT;
+
diff --git a/Open-ILS/src/templates-bootstrap/opac/myopac/hold_subscriptions.tt2 b/Open-ILS/src/templates-bootstrap/opac/myopac/hold_subscriptions.tt2
new file mode 100644
index 0000000000..d5d6c076d8
--- /dev/null
+++ b/Open-ILS/src/templates-bootstrap/opac/myopac/hold_subscriptions.tt2
@@ -0,0 +1,51 @@
+[% PROCESS "opac/parts/header.tt2";
+ PROCESS "opac/parts/misc_util.tt2";
+ PROCESS "opac/parts/hold_status.tt2";
+ PROCESS "opac/parts/myopac/column_sort_support.tt2";
+ WRAPPER "opac/parts/myopac/base.tt2";
+ myopac_page = "holds";
+ parent="holds";
+%]
+
+
+
+
+
+
+ [% IF ctx.my_hold_subscriptions.size AND ctx.my_hold_subscriptions.size < 1 %]
+
+ [% l('No subscriptions found.') %]
+
+ [% ELSE %]
+
+ [% END %]
+
+
+[% END %]
diff --git a/Open-ILS/src/templates-bootstrap/opac/parts/myopac/base.tt2 b/Open-ILS/src/templates-bootstrap/opac/parts/myopac/base.tt2
index 0374be1548..14e425a95e 100755
--- a/Open-ILS/src/templates-bootstrap/opac/parts/myopac/base.tt2
+++ b/Open-ILS/src/templates-bootstrap/opac/parts/myopac/base.tt2
@@ -32,6 +32,9 @@
IF (ctx.show_reservations_tab == 'true');
myopac_pages.push({children => 0, parent => "parent", url => "reservations", text => l(" Reservations"), name => l("Reservations")});
END;
+ IF ctx.my_hold_subscriptions.size > 0;
+ myopac_pages.push({children => 0, parent => "holds", url => "hold_subscriptions", text => l(" Hold Groups"), name => l("Hold Groups")});
+ END;
skin_root = "../"
%]
diff --git a/Open-ILS/src/templates-bootstrap/opac/parts/place_hold.tt2 b/Open-ILS/src/templates-bootstrap/opac/parts/place_hold.tt2
index 652a0a1606..b4dc99b897 100755
--- a/Open-ILS/src/templates-bootstrap/opac/parts/place_hold.tt2
+++ b/Open-ILS/src/templates-bootstrap/opac/parts/place_hold.tt2
@@ -129,6 +129,28 @@ function maybeToggleNumCopies(obj) {
[% l("Place this hold for me ([_1] [_2])", ctx.user.first_given_name, ctx.user.family_name) | html %]
+ [% IF CGI.param('hold_type') == 'T' AND ctx.hold_subscriptions.size > 0 AND NOT CGI.param('from_basket') %]
+
+
+
+
+
+ [% END %]
+
+
[% END %]
[% END %]
diff --git a/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2
index bbf3b7fc6b..492178546c 100644
--- a/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2
+++ b/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2
@@ -26,6 +26,11 @@
+ [% IF ctx.my_hold_subscriptions.size > 0 %]
+
+ [% END %]
diff --git a/Open-ILS/src/templates/opac/myopac/ebook_holds_ready.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_holds_ready.tt2
index 578abeb7c9..97b6ee2343 100644
--- a/Open-ILS/src/templates/opac/myopac/ebook_holds_ready.tt2
+++ b/Open-ILS/src/templates/opac/myopac/ebook_holds_ready.tt2
@@ -26,6 +26,11 @@
+ [% IF ctx.my_hold_subscriptions.size > 0 %]
+
+ [% END %]
diff --git a/Open-ILS/src/templates/opac/myopac/hold_history.tt2 b/Open-ILS/src/templates/opac/myopac/hold_history.tt2
index c06ab40b7a..cf8af4c798 100644
--- a/Open-ILS/src/templates/opac/myopac/hold_history.tt2
+++ b/Open-ILS/src/templates/opac/myopac/hold_history.tt2
@@ -28,6 +28,11 @@
[% l("E-Items Ready for Checkout") %]
[% END %]
+ [% IF ctx.my_hold_subscriptions.size > 0 %]
+
+ [% END %]
diff --git a/Open-ILS/src/templates/opac/myopac/hold_subscriptions.tt2 b/Open-ILS/src/templates/opac/myopac/hold_subscriptions.tt2
new file mode 100644
index 0000000000..51fa081634
--- /dev/null
+++ b/Open-ILS/src/templates/opac/myopac/hold_subscriptions.tt2
@@ -0,0 +1,76 @@
+[% PROCESS "opac/parts/header.tt2";
+ PROCESS "opac/parts/misc_util.tt2";
+ PROCESS "opac/parts/hold_status.tt2";
+ WRAPPER "opac/parts/myopac/base.tt2";
+ myopac_page = "holds";
+ limit = ctx.hold_history_limit;
+ offset = ctx.hold_history_offset;
+ count = ctx.hold_history_ids.size;
+%]
+
+[% l('Holds History') %]
+
+
+
+
+ [% IF ebook_api.enabled == 'true' %]
+
+
+ [% END %]
+ [% IF ctx.my_hold_subscriptions.size > 0 %]
+
+ [% END %]
+
+
+
+
+
+
+
+ [% IF ctx.my_hold_subscriptions.size AND ctx.my_hold_subscriptions.size < 1 %]
+
+ [% l('No subscriptions found.') %]
+
+ [% ELSE %]
+
+ [% END %]
+
+
+[% END %]
diff --git a/Open-ILS/src/templates/opac/myopac/holds.tt2 b/Open-ILS/src/templates/opac/myopac/holds.tt2
index a4242fe7d1..ab1ed4d7be 100644
--- a/Open-ILS/src/templates/opac/myopac/holds.tt2
+++ b/Open-ILS/src/templates/opac/myopac/holds.tt2
@@ -29,6 +29,11 @@
[% l("E-Items Ready for Checkout") %]
[% END %]
+ [% IF ctx.my_hold_subscriptions.size > 0 %]
+
+ [% END %]
diff --git a/Open-ILS/src/templates/opac/parts/place_hold.tt2 b/Open-ILS/src/templates/opac/parts/place_hold.tt2
index 1f0989d81f..809def348c 100644
--- a/Open-ILS/src/templates/opac/parts/place_hold.tt2
+++ b/Open-ILS/src/templates/opac/parts/place_hold.tt2
@@ -97,6 +97,7 @@ function maybeToggleNumCopies(obj) {
%]
[% IF ctx.is_staff %]
+
+ [% IF CGI.param('hold_type') == 'T' AND ctx.hold_subscriptions.size > 0 AND NOT CGI.param('from_basket') %]
+
+
+
+
+
+ [% END %]
+
+
[% END %]
+
[% END %]
@@ -244,7 +268,7 @@ function maybeToggleNumCopies(obj) {
[% IF ctx.default_phone_notify %]checked="checked"[% END %]/>
-
diff --git a/Open-ILS/src/templates/staff/cat/bucket/batch_hold/index.tt2 b/Open-ILS/src/templates/staff/cat/bucket/batch_hold/index.tt2
new file mode 100644
index 0000000000..3cee21de21
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/bucket/batch_hold/index.tt2
@@ -0,0 +1,82 @@
+[%
+ WRAPPER "staff/base.tt2";
+ ctx.page_title = l("Hold Group Buckets");
+ ctx.page_app = "egCatBatchHoldBuckets";
+ ctx.page_ctrl = "BatchHoldBucketCtrl";
+%]
+
+[% BLOCK APP_JS %]
+
+
+
+
+
+[% END %]
+
+
+
+
+
+
+
+
+
+
+ [% INCLUDE 'staff/cat/bucket/batch_hold/t_bucket_info.tt2' %]
+
+
+
+
+
+
+ [% l('The selected bucket "{{bucketId}}" is not visible to this login.') %]
+
+
+
+
+
+
+
+[% END %]
diff --git a/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_bucket_info.tt2 b/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_bucket_info.tt2
new file mode 100644
index 0000000000..bfa0f612a8
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_bucket_info.tt2
@@ -0,0 +1,21 @@
+
+
+ [% l('Hold Group #{{bucket().id()}}: {{bucket().name()}}') %]
+ / {{bucket().description()}}
+
+
+
+
+
+ / [% l('Created {{bucket().create_time() | date:egDateAndTimeFormat}}') %]
+ /
+ {{bucket()._owner_name}}
+ @ {{bucket()._owner_ou}}
+
+
+
+
+ [% l('No Hold Group Selected') %]
+
+
diff --git a/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_event.tt2 b/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_event.tt2
new file mode 100644
index 0000000000..58806bdae9
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_event.tt2
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+ {{item['title']}}
+
+
+ {{item['author']}}
+
+
+
+ {{item['mappings'].length}}
+
+
+
+
+
+
+
+
+
diff --git a/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_event_create.tt2 b/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_event_create.tt2
new file mode 100644
index 0000000000..3b965a0671
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_event_create.tt2
@@ -0,0 +1,30 @@
+
+
+
+
diff --git a/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_grid_menu.tt2 b/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_grid_menu.tt2
new file mode 100644
index 0000000000..f23e59f604
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_grid_menu.tt2
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_list.tt2 b/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_list.tt2
new file mode 100644
index 0000000000..ea83402d30
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_list.tt2
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_pending.tt2 b/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_pending.tt2
new file mode 100644
index 0000000000..5e441d62d0
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_pending.tt2
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+ [% l('User Not Found') %]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item['card.barcode']}}
+
+
+
+
diff --git a/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_view.tt2 b/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_view.tt2
new file mode 100644
index 0000000000..45c0c4b2f6
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/bucket/batch_hold/t_view.tt2
@@ -0,0 +1,29 @@
+
+
+ [% INCLUDE 'staff/cat/bucket/batch_hold/t_grid_menu.tt2' %]
+
+
+
+
+
+
+
+
+
+
+
+ {{item['card.barcode']}}
+
+
+
+
diff --git a/Open-ILS/src/templates/staff/cat/bucket/share/t_bucket_create.tt2 b/Open-ILS/src/templates/staff/cat/bucket/share/t_bucket_create.tt2
index 56d29db01f..cb9ab95ffe 100644
--- a/Open-ILS/src/templates/staff/cat/bucket/share/t_bucket_create.tt2
+++ b/Open-ILS/src/templates/staff/cat/bucket/share/t_bucket_create.tt2
@@ -19,12 +19,19 @@
-
+
+
+
+
+
+
+
+
+