use constant COOKIE_SSS_EXPAND => 'eg_sss_expand';
use constant COOKIE_ANON_CACHE => 'anoncache';
-use constant ANON_CACHE_MYLIST => 'mylist';
+use constant COOKIE_CART_CACHE => 'cartcache';
+use constant CART_CACHE_MYLIST => 'mylist';
use constant ANON_CACHE_STAFF_SEARCH => 'staffsearch';
use constant DEBUG_TIMING => 0;
}
(undef, $self->ctx->{mylist}) = $self->fetch_mylist unless
- $path =~ /opac\/my(opac\/lists|list)/;
+ $path =~ /opac\/my(opac\/lists|list)/ ||
+ $path =~ m!opac/api/mylist!;
+
+ return $self->load_api_mylist_retrieve if $path =~ m|opac/api/mylist/retrieve|;
+ return $self->load_api_mylist_add if $path =~ m|opac/api/mylist/add|;
+ return $self->load_api_mylist_delete if $path =~ m|opac/api/mylist/delete|;
+ return $self->load_api_mylist_clear if $path =~ m|opac/api/mylist/clear|;
return $self->load_simple("home") if $path =~ m|opac/home|;
return $self->load_simple("css") if $path =~ m|opac/css|;
return $self->load_mylist_add if $path =~ m|opac/mylist/add|;
return $self->load_mylist_delete if $path =~ m|opac/mylist/delete|;
return $self->load_mylist_move if $path =~ m|opac/mylist/move|;
- return $self->load_mylist if $path =~ m|opac/mylist|;
+ return $self->load_mylist_print if $path =~ m|opac/mylist/doprint|;
+ return $self->load_mylist if $path =~ m|opac/mylist| && $path !~ m|opac/mylist/email| && $path !~ m|opac/mylist/doemail|;
return $self->load_cache_clear if $path =~ m|opac/cache/clear|;
return $self->load_temp_warn_post if $path =~ m|opac/temp_warn/post|;
return $self->load_temp_warn if $path =~ m|opac/temp_warn|;
$self->apache->headers_out->add("cache-control" => "no-store, no-cache, must-revalidate");
$self->apache->headers_out->add("expires" => "-1");
+ if ($path =~ m|opac/mylist/email|) {
+ (undef, $self->ctx->{mylist}) = $self->fetch_mylist;
+ }
+ $self->load_simple("mylist/email") if $path =~ m|opac/mylist/email|;
+ return $self->load_mylist_email if $path =~ m|opac/mylist/doemail|;
return $self->load_email_record if $path =~ m|opac/record/email|;
return $self->load_place_hold if $path =~ m|opac/place_hold|;
$url = $self->ctx->{proto} . '://' . $self->ctx->{hostname} . $self->ctx->{opac_root} . '/myopac/holds';
foreach my $param (('loc', 'qtype', 'query')) {
if ($self->cgi->param($param)) {
- $url .= ";$param=" . uri_escape_utf8($self->cgi->param($param));
+ my @vals = $self->cgi->param($param);
+ $url .= ";$param=" . uri_escape_utf8($_) foreach @vals;
}
}
}
$bses->kill_me;
}
+
+ if ($self->cgi->param('clear_cart')) {
+ $self->clear_anon_cache;
+ }
}
# pull the selected formats and languages for metarecord holds
foreach my $param (('loc', 'qtype', 'query', 'sort', 'offset', 'limit')) {
if ($self->cgi->param($param)) {
- $url .= ";$param=" . uri_escape_utf8($self->cgi->param($param));
+ my @vals = $self->cgi->param($param);
+ $url .= ";$param=" . uri_escape_utf8($_) foreach @vals;
}
}
}
-# actions are create, delete, show, hide, rename, add_rec, delete_item, place_hold
+# actions are create, delete, show, hide, rename, add_rec, delete_item, place_hold, print, email
# CGI is action, list=list_id, add_rec/record=bre_id, del_item=bucket_item_id, name=new_bucket_name
sub load_myopac_bookbag_update {
my ($self, $action, $list_id, @hold_recs) = @_;
my @add_rec = $cgi->param('add_rec') || $cgi->param('record');
my @selected_item = $cgi->param('selected_item');
my $shared = $cgi->param('shared');
+ my $move_cart = $cgi->param('move_cart');
my $name = $cgi->param('name');
my $description = $cgi->param('description');
my $success = 0;
my $list;
+ # bail out if user is attempting an action that requires
+ # that at least one list item be selected
+ if ((scalar(@selected_item) == 0) && (scalar(@hold_recs) == 0) &&
+ ($action eq 'place_hold' || $action eq 'print' ||
+ $action eq 'email' || $action eq 'del_item')) {
+ my $url = $self->ctx->{referer};
+ $url .= ($url =~ /\?/ ? '&' : '?') . 'list_none_selected=1' unless $url =~ /list_none_selected/;
+ return $self->generic_redirect($url);
+ }
+
# This url intentionally leaves off the edit_notes parameter, but
# may need to add some back in for paging.
foreach my $param (('loc', 'qtype', 'query', 'sort')) {
if ($cgi->param($param)) {
- $url .= "$param=" . uri_escape_utf8($cgi->param($param)) . ";";
+ my @vals = $cgi->param($param);
+ $url .= ";$param=" . uri_escape_utf8($_) foreach @vals;
}
}
$list->pub($shared ? 't' : 'f');
$success = $U->simplereq('open-ils.actor',
'open-ils.actor.container.create', $e->authtoken, 'biblio', $list);
- if (ref($success) ne 'HASH' && scalar @add_rec) {
+ if (ref($success) ne 'HASH') {
$list_id = (ref($success)) ? $success->id : $success;
- foreach my $add_rec (@add_rec) {
- my $item = Fieldmapper::container::biblio_record_entry_bucket_item->new;
- $item->bucket($list_id);
- $item->target_biblio_record_entry($add_rec);
- $success = $U->simplereq('open-ils.actor',
- 'open-ils.actor.container.item.create', $e->authtoken, 'biblio', $item);
- last unless $success;
+ if (scalar @add_rec) {
+ foreach my $add_rec (@add_rec) {
+ my $item = Fieldmapper::container::biblio_record_entry_bucket_item->new;
+ $item->bucket($list_id);
+ $item->target_biblio_record_entry($add_rec);
+ $success = $U->simplereq('open-ils.actor',
+ 'open-ils.actor.container.item.create', $e->authtoken, 'biblio', $item);
+ last unless $success;
+ }
+ }
+ if ($move_cart) {
+ my ($cache_key, $list) = $self->fetch_mylist(0, 1);
+ foreach my $add_rec (@$list) {
+ my $item = Fieldmapper::container::biblio_record_entry_bucket_item->new;
+ $item->bucket($list_id);
+ $item->target_biblio_record_entry($add_rec);
+ $success = $U->simplereq('open-ils.actor',
+ 'open-ils.actor.container.item.create', $e->authtoken, 'biblio', $item);
+ last unless $success;
+ }
+ $self->clear_anon_cache;
}
}
$url = $cgi->param('where_from') if ($success && $cgi->param('where_from'));
} elsif($action eq 'place_hold') {
- # @hold_recs comes from anon lists redirect; selected_itesm comes from existing buckets
+ # @hold_recs comes from anon lists redirect; selected_items comes from existing buckets
+ my $from_basket = scalar(@hold_recs);
unless (@hold_recs) {
if (@selected_item) {
my $items = $e->search_container_biblio_record_entry_bucket_item({id => \@selected_item});
my $url = $self->ctx->{opac_root} . '/place_hold?hold_type=T';
$url .= ';hold_target=' . $_ for @hold_recs;
+ $url .= ';from_basket=1' if $from_basket;
foreach my $param (('loc', 'qtype', 'query')) {
if ($cgi->param($param)) {
- $url .= ";$param=" . uri_escape_utf8($cgi->param($param));
+ my @vals = $cgi->param($param);
+ $url .= ";$param=" . uri_escape_utf8($_) foreach @vals;
}
}
return $self->generic_redirect($url);
+ } elsif ($action eq 'print') {
+ my $temp_cache_key = $self->_stash_record_list_in_anon_cache(@selected_item);
+ return $self->load_mylist_print($temp_cache_key);
+ } elsif ($action eq 'email') {
+ my $temp_cache_key = $self->_stash_record_list_in_anon_cache(@selected_item);
+ return $self->load_mylist_email($temp_cache_key);
} else {
$list = $e->retrieve_container_biblio_record_entry_bucket($list_id);
# Retrieve the users cached records AKA 'My List'
# Returns an empty list if there are no cached records
sub fetch_mylist {
- my ($self, $with_marc_xml) = @_;
+ my ($self, $with_marc_xml, $skip_sort) = @_;
my $list = [];
- my $cache_key = $self->cgi->cookie((ref $self)->COOKIE_ANON_CACHE);
+ my $cache_key = $self->cgi->cookie((ref $self)->COOKIE_CART_CACHE);
if($cache_key) {
$list = $U->simplereq(
'open-ils.actor',
'open-ils.actor.anon_cache.get_value',
- $cache_key, (ref $self)->ANON_CACHE_MYLIST);
+ $cache_key, (ref $self)->CART_CACHE_MYLIST);
if(!$list) {
$cache_key = undef;
# Leverage QueryParser to sort the items by values of config.metabib_fields
# from the items' marc records.
- if (@$list) {
+ if (@$list && !$skip_sort) {
my ($sorter, $modifier) = $self->_get_bookbag_sort_params("anonsort");
my $query = $self->_prepare_anonlist_sorting_query($list, $sorter, $modifier);
my $args = {
return ($cache_key, $list, $marc_xml);
}
+sub load_api_mylist_retrieve {
+ my $self = shift;
+
+ # this has the effect of instantiating an empty one if need be
+ my ($cache_key, $list) = $self->fetch_mylist(0, 1);
+
+ $self->ctx->{json_response} = {
+ mylist => [ map { 0 + $_ } @$list ], # force integers
+ };
+ $self->ctx->{json_reponse_cookie} =
+ $self->cgi->cookie(
+ -name => (ref $self)->COOKIE_CART_CACHE,
+ -path => '/',
+ -value => ($cache_key) ? $cache_key : '',
+ -expires => ($cache_key) ? undef : '-1h'
+ );
+
+ return Apache2::Const::OK;
+}
+
+sub load_api_mylist_clear {
+ my $self = shift;
+
+ $self->clear_anon_cache;
+
+ # and return fresh, empty cart
+ return $self->load_api_mylist_retrieve();
+}
# Adds a record (by id) to My List, creating a new anon cache + list if necessary.
sub load_mylist_add {
my $self = shift;
- my $rec_id = $self->cgi->param('record');
- my ($cache_key, $list) = $self->fetch_mylist;
- push(@$list, $rec_id);
-
- $cache_key = $U->simplereq(
- 'open-ils.actor',
- 'open-ils.actor.anon_cache.set_value',
- $cache_key, (ref $self)->ANON_CACHE_MYLIST, $list);
+ my ($cache_key, $list) = $self->_do_mylist_add();
# Check if we need to warn patron about adding to a "temporary"
# list:
return $self->mylist_action_redirect($cache_key);
}
-sub load_mylist_delete {
+sub load_api_mylist_add {
my $self = shift;
- my $rec_id = $self->cgi->param('record');
- my ($cache_key, $list) = $self->fetch_mylist;
- $list = [ grep { $_ ne $rec_id } @$list ];
+ my ($cache_key, $list) = $self->_do_mylist_add();
+
+ $self->ctx->{json_response} = {
+ mylist => [ map { 0 + $_ } @$list ], # force integers
+ };
+ $self->ctx->{json_reponse_cookie} =
+ $self->cgi->cookie(
+ -name => (ref $self)->COOKIE_CART_CACHE,
+ -path => '/',
+ -value => ($cache_key) ? $cache_key : '',
+ -expires => ($cache_key) ? undef : '-1h'
+ );
+
+ return Apache2::Const::OK;
+}
+
+sub _do_mylist_add {
+ my $self = shift;
+ my @rec_ids = $self->cgi->param('record');
+
+ my ($cache_key, $list) = $self->fetch_mylist(0, 1);
+ push(@$list, @rec_ids);
$cache_key = $U->simplereq(
'open-ils.actor',
'open-ils.actor.anon_cache.set_value',
- $cache_key, (ref $self)->ANON_CACHE_MYLIST, $list);
+ $cache_key, (ref $self)->CART_CACHE_MYLIST, $list);
+
+ return ($cache_key, $list);
+}
+
+sub load_mylist_delete {
+ my $self = shift;
+
+ my ($cache_key, $list) = $self->_do_mylist_delete;
return $self->mylist_action_redirect($cache_key);
}
+sub load_api_mylist_delete {
+ my $self = shift;
+
+ my ($cache_key, $list) = $self->_do_mylist_delete();
+
+ $self->ctx->{json_response} = {
+ mylist => [ map { 0 + $_ } @$list ], # force integers
+ };
+ $self->ctx->{json_reponse_cookie} =
+ $self->cgi->cookie(
+ -name => (ref $self)->COOKIE_CART_CACHE,
+ -path => '/',
+ -value => ($cache_key) ? $cache_key : '',
+ -expires => ($cache_key) ? undef : '-1h'
+ );
+
+ return Apache2::Const::OK;
+}
+
+sub _do_mylist_delete {
+ my $self = shift;
+ my @rec_ids = $self->cgi->param('record');
+
+ my ($cache_key, $list) = $self->fetch_mylist(0, 1);
+ foreach my $rec_id (@rec_ids) {
+ $list = [ grep { $_ ne $rec_id } @$list ];
+ }
+
+ $cache_key = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.anon_cache.set_value',
+ $cache_key, (ref $self)->CART_CACHE_MYLIST, $list);
+
+ return ($cache_key, $list);
+}
+
+sub load_mylist_print {
+ my $self = shift;
+
+ my $cache_key = shift // $self->cgi->cookie((ref $self)->COOKIE_CART_CACHE);
+
+ if (!$cache_key) {
+ return $self->generic_redirect;
+ }
+
+ my $url = sprintf(
+ "%s://%s%s/record/print/%s",
+ $self->ctx->{proto},
+ $self->ctx->{hostname},
+ $self->ctx->{opac_root},
+ $cache_key,
+ );
+
+ my $redirect = $self->cgi->param('redirect_to');
+ $url .= '?redirect_to=' . uri_escape_utf8($redirect);
+ my $clear_cart = $self->cgi->param('clear_cart');
+ $url .= '&is_list=1';
+ $url .= '&clear_cart=1' if $clear_cart;
+
+ return $self->generic_redirect($url);
+}
+
+sub load_mylist_email {
+ my $self = shift;
+
+ my $cache_key = shift // $self->cgi->cookie((ref $self)->COOKIE_CART_CACHE);
+
+ if (!$cache_key) {
+ return $self->generic_redirect;
+ }
+
+ my $url = sprintf(
+ "%s://%s%s/record/email/%s",
+ $self->ctx->{proto},
+ $self->ctx->{hostname},
+ $self->ctx->{opac_root},
+ $cache_key,
+ );
+
+ my $redirect = $self->cgi->param('redirect_to');
+ $url .= '?redirect_to=' . uri_escape_utf8($redirect);
+ my $clear_cart = $self->cgi->param('clear_cart');
+ $url .= '&is_list=1';
+ $url .= '&clear_cart=1' if $clear_cart;
+
+ return $self->generic_redirect($url);
+}
+
+sub _stash_record_list_in_anon_cache {
+ my $self = shift;
+ my @rec_ids = @_;
+
+ my $cache_key = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.anon_cache.set_value',
+ undef, (ref $self)->CART_CACHE_MYLIST, [ @rec_ids ]);
+ return $cache_key;
+}
+
sub load_mylist_move {
my $self = shift;
my @rec_ids = $self->cgi->param('record');
my $action = $self->cgi->param('action') || '';
- return $self->load_myopac_bookbag_update('place_hold', undef, @rec_ids)
- if $action eq 'place_hold';
-
my ($cache_key, $list) = $self->fetch_mylist;
+
+ unless ((scalar(@rec_ids) > 0) ||
+ ($self->cgi->param('entire_list') && scalar(@$list) > 0)) {
+ my $url = $self->ctx->{referer};
+ $url .= ($url =~ /\?/ ? '&' : '?') . 'cart_none_selected=1';
+ return $self->generic_redirect($url);
+ }
+
+ if ($action eq 'place_hold') {
+ if ($self->cgi->param('entire_list')) {
+ @rec_ids = @$list;
+ }
+ return $self->load_myopac_bookbag_update('place_hold', undef, @rec_ids);
+ }
+ if ($action eq 'print') {
+ my $temp_cache_key = $self->_stash_record_list_in_anon_cache(@rec_ids);
+ return $self->load_mylist_print($temp_cache_key);
+ }
+ if ($action eq 'email') {
+ my $temp_cache_key = $self->_stash_record_list_in_anon_cache(@rec_ids);
+ return $self->load_mylist_email($temp_cache_key);
+ }
+ if ($action eq 'new_list') {
+ my $url = $self->apache->unparsed_uri;
+ $url =~ s!/mylist/move!/myopac/lists!;
+ return $self->generic_redirect($url);
+ }
+
return $self->mylist_action_redirect unless $cache_key;
my @keep;
$cache_key = $U->simplereq(
'open-ils.actor',
'open-ils.actor.anon_cache.set_value',
- $cache_key, (ref $self)->ANON_CACHE_MYLIST, \@keep
+ $cache_key, (ref $self)->CART_CACHE_MYLIST, \@keep
);
+ if ($action eq 'delete' && scalar(@keep) == 0) {
+ my $url = $self->cgi->param('orig_referrer') // $self->ctx->{referer};
+ return $self->generic_redirect($url);
+ }
+
if ($self->ctx->{user} and $action =~ /^\d+$/) {
# in this case, action becomes list_id
$self->load_myopac_bookbag_update('add_rec', $self->cgi->param('action'));
my $self = shift;
my $field = shift;
- my $cache_key = $self->cgi->cookie((ref $self)->COOKIE_ANON_CACHE) or return;
+ my $cache_key = $self->cgi->cookie((ref $self)->COOKIE_CART_CACHE) or return;
$U->simplereq(
'open-ils.actor',
if( my $anchor = $self->cgi->param('anchor') ) {
# on the results page, we want to redirect
# back to record that was affected
- $url = $self->ctx->{referer};
+ $url = $self->cgi->param('redirect_to') // $self->ctx->{referer};
$url =~ s/#.*|$/#$anchor/;
- }
+ } else {
+ $url = $self->cgi->param('redirect_to') // $self->ctx->{referer};
+ }
return $self->generic_redirect(
$url,
$self->cgi->cookie(
- -name => (ref $self)->COOKIE_ANON_CACHE,
+ -name => (ref $self)->COOKIE_CART_CACHE,
-path => '/',
-value => ($cache_key) ? $cache_key : '',
-expires => ($cache_key) ? undef : '-1h'
return $self->generic_redirect(
$base_url,
$self->cgi->cookie(
- -name => (ref $self)->COOKIE_ANON_CACHE,
+ -name => (ref $self)->COOKIE_CART_CACHE,
-path => '/',
-value => ($cache_key) ? $cache_key : '',
-expires => ($cache_key) ? undef : '-1h'
(undef, $self->ctx->{mylist}, $self->ctx->{mylist_marc_xml}) =
$self->fetch_mylist(1);
+ # get list of bookbags in case user wants to move cart contents to
+ # one
+ if ($self->ctx->{user}) {
+ $self->_load_lists_and_settings;
+ }
+
return Apache2::Const::OK;
}
sub load_print_record {
my $self = shift;
- my $rec_id = $self->ctx->{page_args}->[0]
+ my $rec_or_list_id = $self->ctx->{page_args}->[0]
or return Apache2::Const::HTTP_BAD_REQUEST;
- $self->{ctx}->{bre_id} = $rec_id;
+ my $is_list = $self->cgi->param('is_list');
+ my $list;
+ if ($is_list) {
+
+ $list = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.anon_cache.get_value',
+ $rec_or_list_id, (ref $self)->CART_CACHE_MYLIST);
+
+ if(!$list) {
+ $list = [];
+ }
+
+ { # sanitize
+ no warnings qw/numeric/;
+ $list = [map { int $_ } @$list];
+ $list = [grep { $_ > 0} @$list];
+ };
+ } else {
+ $list = $rec_or_list_id;
+ $self->{ctx}->{bre_id} = $rec_or_list_id;
+ }
+
$self->{ctx}->{printable_record} = $U->simplereq(
'open-ils.search',
- 'open-ils.search.biblio.record.print', $rec_id);
+ 'open-ils.search.biblio.record.print', $list);
+
+ if ($self->cgi->param('clear_cart')) {
+ $self->clear_anon_cache;
+ }
+ $self->ctx->{'redirect_to'} = $self->cgi->param('redirect_to');
return Apache2::Const::OK;
}
sub load_email_record {
my $self = shift;
- my $rec_id = $self->ctx->{page_args}->[0]
+ my $rec_or_list_id = $self->ctx->{page_args}->[0]
or return Apache2::Const::HTTP_BAD_REQUEST;
- $self->{ctx}->{bre_id} = $rec_id;
+ my $is_list = $self->cgi->param('is_list');
+ my $list;
+ if ($is_list) {
+
+ $list = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.anon_cache.get_value',
+ $rec_or_list_id, (ref $self)->CART_CACHE_MYLIST);
+
+ if(!$list) {
+ $list = [];
+ }
+
+ { # sanitize
+ no warnings qw/numeric/;
+ $list = [map { int $_ } @$list];
+ $list = [grep { $_ > 0} @$list];
+ };
+ } else {
+ $list = $rec_or_list_id;
+ $self->{ctx}->{bre_id} = $rec_or_list_id;
+ }
+
$U->simplereq(
'open-ils.search',
'open-ils.search.biblio.record.email',
- $self->ctx->{authtoken}, $rec_id);
+ $self->ctx->{authtoken}, $list);
+
+ if ($self->cgi->param('clear_cart')) {
+ $self->clear_anon_cache;
+ }
+ $self->ctx->{'redirect_to'} = $self->cgi->param('redirect_to');
return Apache2::Const::OK;
}
$stat = Apache2::Const::OK;
}
return $stat unless $stat == Apache2::Const::OK;
+
+ # emit context as JSON if handler requests
+ if ($ctx->{json_response}) {
+ $r->content_type("application/json; charset=utf-8");
+ $r->headers_out->add("cache-control" => "no-store, no-cache, must-revalidate");
+ $r->headers_out->add("expires" => "-1");
+ if ($ctx->{json_reponse_cookie}) {
+ $r->headers_out->add('Set-Cookie' => $ctx->{json_reponse_cookie})
+ }
+ $r->print(OpenSRF::Utils::JSON->perl2JSON($ctx->{json_response}));
+ return Apache2::Const::OK;
+ }
+
return Apache2::Const::DECLINED unless $template;
my $text_handler = set_text_handler($ctx, $r);
Subject: Bibliographic Records
Auto-Submitted: auto-generated
-[% FOR cbreb IN target %][% title = '' %]
+[% FOR cbreb IN target %]
[% FOR item IN cbreb.items;
bre_id = item.target_biblio_record_entry;
bibxml = helpers.unapi_bre(bre_id, {flesh => '{mra}'});
+ title = '';
FOR part IN bibxml.findnodes('//*[@tag="245"]/*[@code="a" or @code="b"]');
title = title _ part.textContent;
END;
<div>
<style> li { padding: 8px; margin 5px; }</style>
<ol>
- [% FOR cbreb IN target %][% title = '' %]
+ [% FOR cbreb IN target %]
[% FOR item IN cbreb.items;
bre_id = item.target_biblio_record_entry;
bibxml = helpers.unapi_bre(bre_id, {flesh => '{mra}'});
+ title = '';
FOR part IN bibxml.findnodes('//*[@tag="245"]/*[@code="a" or @code="b"]');
title = title _ part.textContent;
END;
--- /dev/null
+BEGIN;
+
+UPDATE action_trigger.event_definition
+SET template =
+$$
+[%- USE date -%]
+[%- SET user = target.0.owner -%]
+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: Bibliographic Records
+Auto-Submitted: auto-generated
+
+[% FOR cbreb IN target %]
+[% FOR item IN cbreb.items;
+ bre_id = item.target_biblio_record_entry;
+
+ bibxml = helpers.unapi_bre(bre_id, {flesh => '{mra}'});
+ title = '';
+ FOR part IN bibxml.findnodes('//*[@tag="245"]/*[@code="a" or @code="b"]');
+ title = title _ part.textContent;
+ END;
+
+ author = bibxml.findnodes('//*[@tag="100"]/*[@code="a"]').textContent;
+ item_type = bibxml.findnodes('//*[local-name()="attributes"]/*[local-name()="field"][@name="item_type"]').getAttribute('coded-value');
+ publisher = bibxml.findnodes('//*[@tag="260"]/*[@code="b"]').textContent;
+ pubdate = bibxml.findnodes('//*[@tag="260"]/*[@code="c"]').textContent;
+ isbn = bibxml.findnodes('//*[@tag="020"]/*[@code="a"]').textContent;
+ issn = bibxml.findnodes('//*[@tag="022"]/*[@code="a"]').textContent;
+ upc = bibxml.findnodes('//*[@tag="024"]/*[@code="a"]').textContent;
+%]
+
+[% loop.count %]/[% loop.size %]. Bib ID# [% bre_id %]
+[% IF isbn %]ISBN: [% isbn _ "\n" %][% END -%]
+[% IF issn %]ISSN: [% issn _ "\n" %][% END -%]
+[% IF upc %]UPC: [% upc _ "\n" %] [% END -%]
+Title: [% title %]
+Author: [% author %]
+Publication Info: [% publisher %] [% pubdate %]
+Item Type: [% item_type %]
+
+[% END %]
+[% END %]
+$$
+WHERE hook = 'biblio.format.record_entry.email'
+-- from previous stock definition
+AND MD5(template) = 'ee4e6c1b3049086c570c7a77413d46c1';
+
+UPDATE action_trigger.event_definition
+SET template =
+$$
+<div>
+ <style> li { padding: 8px; margin 5px; }</style>
+ <ol>
+ [% FOR cbreb IN target %]
+ [% FOR item IN cbreb.items;
+ bre_id = item.target_biblio_record_entry;
+
+ bibxml = helpers.unapi_bre(bre_id, {flesh => '{mra}'});
+ title = '';
+ FOR part IN bibxml.findnodes('//*[@tag="245"]/*[@code="a" or @code="b"]');
+ title = title _ part.textContent;
+ END;
+
+ author = bibxml.findnodes('//*[@tag="100"]/*[@code="a"]').textContent;
+ item_type = bibxml.findnodes('//*[local-name()="attributes"]/*[local-name()="field"][@name="item_type"]').getAttribute('coded-value');
+ publisher = bibxml.findnodes('//*[@tag="260"]/*[@code="b"]').textContent;
+ pubdate = bibxml.findnodes('//*[@tag="260"]/*[@code="c"]').textContent;
+ isbn = bibxml.findnodes('//*[@tag="020"]/*[@code="a"]').textContent;
+ %]
+
+ <li>
+ Bib ID# [% bre_id %] ISBN: [% isbn %]<br />
+ Title: [% title %]<br />
+ Author: [% author %]<br />
+ Publication Info: [% publisher %] [% pubdate %]<br/>
+ Item Type: [% item_type %]
+ </li>
+ [% END %]
+ [% END %]
+ </ol>
+</div>
+$$
+WHERE hook = 'biblio.format.record_entry.print'
+-- from previous stock definition
+AND MD5(template) = '9ada7ea8417cb23f89d0dc8f15ec68d0';
<span class="browse_the_catalog_lbl"><a href="[% mkurl(ctx.opac_root _ '/browse') %]">[%
l('Browse the Catalog')%]</a></span>
+ [% INCLUDE 'opac/parts/cart.tt2' %]
</div>
<div id="adv_search_parent">
<div id="adv_search_tabs">
id="home_adv_search_link">[%l('Advanced Search')%]</a></span>
<span class="browse_the_catalog_lbl mobile_hide">[% l('Browse the Catalog') %]</span>
+ [% INCLUDE 'opac/parts/cart.tt2' %]
</div>
</div>
<div id="content-wrapper">
width: 78px;
}
+/* styles for selecting records in the results set */
+.result_table_row_selected {
+ background-color: [% css_colors.item_selected %];
+}
+#selected_records_summary, #clear_basket {
+ margin-left: 5em;
+}
+
+/* styles for the basket */
+#record_basket {
+ [% IF rtl == 't' -%]
+ float: left;
+ margin-left: 5em;
+ [% ELSE; %]
+ float: right;
+ margin-right: 5em;
+ [% END; %]
+}
+#record_basket_icon {
+ [% IF rtl == 't' -%]
+ float: left;
+ margin-left: 2em;
+ [% ELSE; %]
+ float: right;
+ margin-right: 2em;
+ [% END; %]
+ position: relative;
+}
+#record_basket_count_floater {
+ background-color: [% css_colors.accent_lighter %];
+ position: absolute;
+ top: -3px;
+ right: -3px; /* relative to icon, so don't want to adjust for RTL */
+ z-index: 2;
+ border-radius: 50%;
+}
+#record_basket_count_floater a {
+ text-decoration: none;
+}
+#basket_actions {
+ [% IF rtl == 't' -%]
+ float: left;
+ [% ELSE; %]
+ float: right;
+ [% END; %]
+}
+#basket_actions select {
+ border-color: rgb(169, 169, 169);
+}
+
.result_number {
[% IF rtl == 't' -%]
padding-right: 1em;
border-color: [% css_colors.border_dark %];
border-width: 1px;
border-style: solid;
+ z-index: 1;
}
.popmenu li:hover li {
float: none;
}
.popmenu li:hover li a {
- background-color: [% css_colors.primary %];
- color: [% css_colors.accent_ultralight %];
+ background-color: [% css_colors.primary %] !important;
+ color: [% css_colors.accent_ultralight %] !important;
}
.popmenu li li a:hover {
- background-color: [% css_colors.accent_ultralight %];
- color: [% css_colors.primary %];
+ background-color: [% css_colors.accent_ultralight %] !important;
+ color: [% css_colors.primary %] !important;
}
-/* Styles for the temporary list entry. */
+/* Styles for the basket entry. */
.popmenu li:hover li[class~="temporary"] a {
background-color: [% css_colors.primary %];
color: [% css_colors.accent_ultralight %];
INCLUDE "opac/parts/topnav.tt2";
ctx.metalinks.push('<meta name="robots" content="noindex,follow">');
ctx.page_title = l("Record Detail") %]
- <h2 class="sr-only">[% l('Temporary List') %]</h2>
+ <h2 class="sr-only">[% l('Basket') %]</h2>
<div class="mobile_hide">
[% INCLUDE "opac/parts/searchbar.tt2" %]
</div>
[% IF ctx.mylist.size;
INCLUDE "opac/parts/anon_list.tt2";
ELSE %]
- <div class="opac-auto-171 opac-auto-097">[% l("You have not created a list yet."); %]</div>
+ <div class="warning_box">[% l("The basket is empty."); %]</div>
+ <button type="button" class="opac-button" onclick="window.location='[% ctx.referer | html %]'">[% l('Return') %]</button>
[% END %]
<div class="common-full-pad"></div>
</div>
--- /dev/null
+[%- PROCESS "opac/parts/header.tt2";
+ PROCESS "opac/parts/misc_util.tt2";
+ WRAPPER "opac/parts/base.tt2";
+ INCLUDE "opac/parts/topnav.tt2";
+ ctx.page_title = l("Confirm Clearing of Basket") %]
+ <h2 class="sr-only">[% l('Confirm Clearing of Basket') %]</h2>
+ [% INCLUDE "opac/parts/searchbar.tt2" %]
+ <div id="content-wrapper">
+ <div id="main-content">
+ <p class="big-strong">[% l('Please confirm that you want to remove all [_1] titles from the basket.', ctx.mylist.size) %]
+ <form method="post" action="[% mkurl(ctx.opac_root _ '/cache/clear', {}, 1) %]">
+ <input type="hidden" name="redirect_to" value="[% ctx.referer %]" />
+ <input id="print_cart_submit" type="submit" name="submit"
+ value="[% l('Confirm') %]" title="[% l('Confirm') %]"
+ alt="[% l('Confirm') %]" class="opac-button" />
+ <input type="reset" name="cancel" onclick="window.location='[% ctx.referer | html %]'" value="[% l('Cancel') %]" id="clear_basket_cancel" class="opac-button" />
+ </form>
+ <div class="common-full-pad"></div>
+ </div>
+ </div>
+[%- END %]
--- /dev/null
+[%- PROCESS "opac/parts/header.tt2";
+ PROCESS "opac/parts/misc_util.tt2";
+ WRAPPER "opac/parts/base.tt2";
+ INCLUDE "opac/parts/topnav.tt2";
+ ctx.page_title = l("Confirm Basket Email") %]
+ <h2 class="sr-only">[% l('Confirm Basket Email') %]</h2>
+ [% INCLUDE "opac/parts/searchbar.tt2" %]
+ <div id="content-wrapper">
+ <div id="main-content">
+ [% IF ctx.mylist.size %]
+ <p class="big-strong">[% l('Please confirm that you want to email the [_1] titles in the basket.', ctx.mylist.size) %]
+ <form method="post" action="[% mkurl(ctx.opac_root _ '/mylist/doemail', {}, 1) %]">
+ <input type="hidden" name="redirect_to" value="[% ctx.referer %]" />
+ <input type="checkbox" name="clear_basket" value="on" />
+ <label for="clear_basket">[% l('Clear basket after emailing it.') %]</label>
+ <br />
+ <input id="print_cart_submit" type="submit" name="submit"
+ value="[% l('Confirm') %]" title="[% l('Confirm') %]"
+ alt="[% l('Confirm') %]" class="opac-button" />
+ <input type="reset" name="cancel" onclick="window.location='[% ctx.referer | html %]'" value="[% l('Cancel') %]" id="clear_basket_cancel" class="opac-button" />
+ </form>
+ [% ELSE %]
+ <div class="warning_box">[% l("The basket is empty."); %]</div>
+ <button type="button" class="opac-button" onclick="window.location='[% ctx.referer | html %]'">[% l('Return') %]</button>
+ [% END %]
+ <div class="common-full-pad"></div>
+ </div>
+ </div>
+[%- END %]
--- /dev/null
+[%- PROCESS "opac/parts/header.tt2";
+ PROCESS "opac/parts/misc_util.tt2";
+ WRAPPER "opac/parts/base.tt2";
+ INCLUDE "opac/parts/topnav.tt2";
+ ctx.page_title = l("Confirm Basket Printing") %]
+ <h2 class="sr-only">[% l('Confirm Basket Printing') %]</h2>
+ [% INCLUDE "opac/parts/searchbar.tt2" %]
+ <div id="content-wrapper">
+ <div id="main-content">
+ [% IF ctx.mylist.size %]
+ <p class="big-strong">[% l('Please confirm that you want to print the [_1] titles in the basket.', ctx.mylist.size) %]
+ <form method="post" action="[% mkurl(ctx.opac_root _ '/mylist/doprint', {}, 1) %]">
+ <input type="hidden" name="redirect_to" value="[% ctx.referer %]" />
+ <input type="checkbox" name="clear_cart" value="on" />
+ <label for="clear_basket">[% l('Clear basket after printing it.') %]</label>
+ <br />
+ <input id="print_cart_submit" type="submit" name="submit"
+ value="[% l('Confirm') %]" title="[% l('Confirm') %]"
+ alt="[% l('Confirm') %]" class="opac-button" />
+ <input type="reset" name="cancel" onclick="window.location='[% ctx.referer | html %]'" value="[% l('Cancel') %]" id="clear_basket_cancel" class="opac-button" />
+ </form>
+ [% ELSE %]
+ <div class="warning_box">[% l("The basket is empty."); %]</div>
+ <button type="button" class="opac-button" onclick="window.location='[% ctx.referer | html %]'">[% l('Return') %]</button>
+ [% END %]
+ <div class="common-full-pad"></div>
+ </div>
+ </div>
+[%- END %]
</a>
</td>
</tr>
+ [% IF ctx.mylist.size %]
+ <tr>
+ <td class="list_create_table_label">
+ <label for="list_move_cart">[% l('Move contents of basket to this list?') %]</label>
+ </td>
+ <td>
+ <select name="move_cart" id="list_move_cart">
+ <option value="0">[% l('No') %]
+ <option value="1" [% IF CGI.param('move_cart_by_default') %]selected="selected"[% END%]>[% l('Yes') %]
+ </select>
+ </td>
+ </tr>
+ [% END %]
<tr>
<td> </td>
<td class="list-create-table-buttons">
</table>
</form>
- <h1>[% l("My Existing Lists") %]</h1>
+ [% IF CGI.param('from_basket'); %]
+ <h1>[% l("... from basket") %]</h1>
+ [% INCLUDE "opac/parts/anon_list.tt2" %]
+ [% ELSE %]
+ <h1>[% l("My Existing Basket and Lists") %]</h1>
[% INCLUDE "opac/parts/anon_list.tt2" %]
[% IF ctx.bookbags.size %]
<div class="header_middle">
<form action="[% mkurl(ctx.opac_root _ '/myopac/list/update') %]" method="post">
<input type="hidden" name="list" value="[% bbag.id %]" />
<input type="hidden" name="sort" value="[% CGI.param('sort') | uri %]" />
+ <input type="hidden" name="redirect_to" value="[% mkurl('', {}, ['list_none_selected', 'cart_none_selected']) %]" />
<div class="bbag-content">
[% IF bbag.items.size %]
<div class="bbag-action">
<select name="action" class="bbag-action-field">
<option disabled="disabled" selected="selected">[% l('-- Actions for these items --') %]</option>
<option value="place_hold">[% l('Place hold') %]</option>
+ <option value="print">[% l('Print title details') %]</option>
+ <option value="email">[% l('Email title details') %]</option>
<option value="del_item">[% l('Remove from list') %]</option>
</select>
[%- INCLUDE "opac/parts/preserve_params.tt2"; %]
<input class="opac-button" type="submit" value="[% l('Go') %]" />
+ [% IF CGI.param('list_none_selected') %]
+ <span class="error">[% l('No items were selected') %]</span>
+ [% END %]
</div>
[% END %]
<table class="bookbag-specific table_no_cell_pad table_no_border_space table_no_border">
[% END %]
</div>
[% END %]
+ [% END %]
</div>
[% END %]
[% IF ctx.mylist.size %]
<div class="bookbag-specific">
- <p class="big-strong">[% l('Temporary List') %]</p>
+ <p class="big-strong">[% l('Basket') %]</p>
<div class="sort">
<form method="get">
- <label for="anonsort">[% l("Sort list items by: ") %]</label>
+ <label for="anonsort">[% l("Sort cart items by: ") %]</label>
[% INCLUDE "opac/parts/filtersort.tt2" mode='bookbag'
id="anonsort" name="anonsort" value=CGI.param("anonsort") %]
<input type="hidden" name="id"
<input class="opac-button" type="submit" value="[% l('Sort') %]" />
</form>
</div>
- <form action="[% mkurl(ctx.opac_root _ '/mylist/move') %]" method="get">
+ <form action="[% mkurl(ctx.opac_root _ '/mylist/move') %]" method="post">
+ <input type="hidden" name="orig_referrer" value="[% CGI.referer | html %]" />
+ <input type="hidden" name="redirect_to" value="[% mkurl('', {}, ['list_none_selected', 'cart_none_selected']) %]" />
<div class="bbag-action" style="clear:both;">
<select name="action">
<option>[% l('-- Actions for these items --') %]</option>
<option value="place_hold">[% l('Place hold') %]</option>
- <option value="delete">[% l('Remove from list') %]</option>
+ <option value="print">[% l('Print title details') %]</option>
+ <option value="email">[% l('Email title details') %]</option>
+ <option value="delete">[% l('Remove from basket') %]</option>
+ <option value="new_list">[% l('Add to new list') %]</option>
[% IF ctx.user AND ctx.bookbags.size %]
<optgroup label="[% l('Move selected items to list:') %]">
[% FOR bbag IN ctx.bookbags %]]
</select>
[%- INCLUDE "opac/parts/preserve_params.tt2"; %]
<input class="opac-button" type="submit" value="[% l('Go') %]" />
+ <input type="checkbox" name="clear_cart">[% l('Clear entire basket when action complete') %]</input>
+ [% IF CGI.param('cart_none_selected') %]
+ <span class="error">[% l('No items were selected') %]</span>
+ [% END %]
</div>
<div class="bbag-content">
<table class="bookbag-specific table_no_cell_pad table_no_border_space table_no_border">
<thead id="acct_list_header_anon">
<tr>
<td class='list_checkbox'>
- <input type="checkbox" onclick="
+ <input type="checkbox" checked="checked" onclick="
var inputs=document.getElementsByTagName('input');
for (i = 0; i < inputs.length; i++) {
if (inputs[i].name == 'record' && !inputs[i].disabled) inputs[i].checked = this.checked;}"/>
PROCESS get_marc_attrs args=attrs %]
<tr>
<td class="list_checkbox">
- <input type="checkbox" name="record" value="[% item %]" />
+ <input type="checkbox" checked="checked" name="record" value="[% item %]" />
</td>
<td class="list_entry" data-label="[% l('Title') %]"><a href="[% mkurl(ctx.opac_root _ '/record/' _ item, {}, ['edit_notes', 'id']) %]">[% attrs.title | html %]</a></td>
<td class="list_entry" data-label="[% l('Author(s)') %]"><a href="[%-
[% l("Add to my list") %]
</a>
<ul>
- <li class="[% tclass %]">
- <a href="[% href %]">[% l('Temporary List') %]</a>
- </li>
[% IF default_list;
label = (ctx.default_bookbag) ? ctx.default_bookbag : l('Default List');
class = (ctx.bookbags.size) ? "default divider" : "default";
--- /dev/null
+<div id="record_basket">
+ <div id="basket_actions">
+ <select id="select_basket_action">
+ <option value="">[% l('-- Basket Actions --') %]</option>
+ <option value="[% mkurl(ctx.opac_root _ '/mylist', {}) %]">[% l('View Basket') %]</option>
+ <option value="[% mkurl(ctx.opac_root _ '/mylist/move', { action => 'place_hold', entire_list => 1 }) %]">[% l('Place Holds') %]</option>
+ <option value="[% mkurl(ctx.opac_root _ '/mylist/print', {}) %]">[% l('Print Title Details') %]</option>
+ <option value="[% mkurl(ctx.opac_root _ '/mylist/email', {}) %]">[% l('Email Title Details') %]</option>
+ [% IF !ctx.is_browser_staff %]
+ <option value="[% mkurl(ctx.opac_root _ '/myopac/lists', { move_cart_by_default => 1, from_basket => 1 }) %]">[% l('Add Basket to Saved List') %]</option>
+ [% END %]
+ [% IF ctx.is_browser_staff %]
+ <option value="add_cart_to_bucket">[% l('Add Basket to Bucket') %]</option>
+ [% END %]
+ <option value="[% mkurl(ctx.opac_root _ '/mylist/clear', {}) %]">[% l('Clear Basket') %]</option>
+ </select>
+ <input class="opac-button" type="button" id="do_basket_action" value="[% l('Go') %]" />
+ </div>
+ <div id="record_basket_icon">
+ <a href="[% mkurl(ctx.opac_root _ '/mylist') %]" class="no-dec" rel="nofollow" vocab="">
+ <img src="[% ctx.media_prefix %]/images/cart-sm.png[% ctx.cache_key %]" alt="[% l('View Basket') %]">
+ </a>
+ <div id="record_basket_count_floater">
+ <a href="[% mkurl(ctx.opac_root _ '/mylist') %]" class="no-dec" rel="nofollow" vocab="">
+ <span id="record_basket_count">[% ctx.mylist.size %]</span>
+ <span class="sr-only">[% l('records in basket') %]</span>
+ </a>
+ </div>
+ </div>
+</div>
##############################################################################
ctx.hide_badge_scores = 'false';
+##############################################################################
+# Maximum number of items allowed to be stored in a basket
+##############################################################################
+ctx.max_cart_size = 500;
+
%]
button_text_shadow = "#555555", # medium grey
table_heading = "#d8d8d8", # grey-blue
mobile_header_text = "#fff", # white
+ item_selected = "#ddd", # grey (lighter)
};
%]
# Don't wrap in l() here; do that where this format string is actually used.
SET HUMAN_NAME_FORMAT = '[_1] [_2] [_3] [_4] [_5]';
- is_advanced = CGI.param("_adv").size;
+ is_advanced = CGI.param("_adv").size || CGI.param("query").size;
is_special = CGI.param("_special").size;
# Check if we want to show the detail record view. Doing this
cgi.delete_all();
END;
+ # some standing, hardcoded parameters to always clear
+ # because they're used for specific, transitory purposes
+ cgi.delete('move_cart_by_default');
+ cgi.delete('cart_none_selected');
+ cgi.delete('list_none_selected');
+
# x and y are artifacts of using <input type="image" /> tags
# instead of true submit buttons, and their values are never used.
cgi.delete('x', 'y');
<script src='[% ctx.media_prefix %]/js/ui/default/opac/ac_google_books.js[% ctx.cache_key %]' async defer></script>
[%- END %]
+<script>
+ window.egStrings = [];
+ window.egStrings['CONFIRM_BASKET_EMPTY'] = "[% l('Remove all records from basket?') %]";
+</script>
+<script src='[% ctx.media_prefix %]/js/ui/default/opac/record_selectors.js[% ctx.cache_key %]' async defer></script>
+
<!-- Require some inputs and selections for browsers that don't support required form field element -->
[% IF ctx.page == 'place_hold' %]
<script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/opac/holds-validation.js[% ctx.cache_key %]">
<script type="text/javascript">if ($('client_tz_id')) { $('client_tz_id').value = OpenSRF.tz }</script>
[%- END; # want_dojo -%]
+
+[%- IF ctx.max_cart_size; %]
+<script type="text/javascript">var max_cart_size = [% ctx.max_cart_size %];</script>
+[%- END; %]
SET some_holds_allowed = 0 IF some_holds_allowed == -1;
ELSE; some_holds_allowed = 1; END;
END %]
-
+
+ [% IF loop.first %]
<form method="post" name="PlaceHold" onsubmit="return validateHoldForm()" >
<input type="hidden" name="hold_type" value="[% CGI.param('hold_type') | html %]" />
[%
</span>
</p>
[% END %]
+ [% END %]
<table id='hold-items-list'>
<tr>
[% END %]
[% END %]
[% INCLUDE "opac/parts/multi_hold_select.tt2" IF NOT (this_hold_disallowed AND hdata.part_required); %]
- [% IF NOT metarecords.disabled %]
+ [% IF NOT metarecords.disabled AND ctx.hold_data.size == 1 %]
[% IF CGI.param('hold_type') == 'T' AND hdata.record.metarecord AND !hdata.part_required %]
<!-- Grab the bre_id so that we can restore it if user accidentally clicks advanced options -->
[% bre_id = hdata.target.id %]
<em>[% l('Enter date in MM/DD/YYYY format') %]</em>
</blockquote>
</p>
+ [% IF CGI.param('from_basket') %]
+ <blockquote><input type="checkbox" name="clear_cart">[% l('Clear basket?') %]</input></blockquote>
+ [% END %]
<input id="place_hold_submit" type="submit" name="submit"
value="[% l('Submit') %]" title="[% l('Submit') %]"
alt="[% l('Submit') %]" class="opac-button" />
[%- END -%]
<div class="rdetail_aux_utils toggle_list">
- [% IF !ctx.is_staff %]
- [% IF ctx.user;
- INCLUDE "opac/parts/bookbag_actions.tt2";
- %]
- [% ELSE;
- operation = ctx.mylist.grep(ctx.bre_id).size ? "delete" : "add";
- label = (operation == "add") ? l("Add to my list") : l("Remove from my list");
+ [% operation = ctx.mylist.grep('^' _ ctx.bre_id _ '$').size ? "delete" : "add";
+ addhref = mkurl(ctx.opac_root _ '/mylist/add', {record => ctx.bre_id}, stop_parms);
+ delhref = mkurl(ctx.opac_root _ '/mylist/delete', {record => ctx.bre_id}, stop_parms);
+ label = (operation == "add") ? l("Add to Basket") : l("Remove from Basket");
%]
- <a href="[% mkurl(ctx.opac_root _ '/mylist/' _ operation, {record => ctx.bre_id}, stop_parms) %]" class="no-dec" rel="nofollow" vocab="">
- <img src="[% ctx.media_prefix %]/images/clipboard.png[% ctx.cache_key %]" alt="" />
- [% label %]
+ <a href="[% addhref %]" id="mylist_add_[% ctx.bre_id %]"
+ rel="nofollow" vocab=""
+ data-recid="[% ctx.bre_id %]" data-action="add"
+ class="no-dec mylist_action [% IF ctx.mylist.grep('^' _ ctx.bre_id _ '$').size %]hidden[% END %]"
+ title="[% l("Add [_1] to basket", attrs.title) %]" rel="nofollow" vocab="">
+ <img src="[% ctx.media_prefix %]/images/add-to-cart.png[% ctx.cache_key %]" alt="" />
+ [% l("Add to basket") %]
+ </a>
+ <a href="[% delhref %]" id="mylist_delete_[% ctx.bre_id %]"
+ rel="nofollow" vocab=""
+ data-recid="[% ctx.bre_id %]" data-action="delete"
+ class="mylist_action [% IF !ctx.mylist.grep('^' _ ctx.bre_id _ '$').size %]hidden[% END %]"
+ title="[% l("Remove [_1] from basket", attrs.title) %]" rel="nofollow" vocab="">
+ <img src="[% ctx.media_prefix %]/images/add-to-cart.png[% ctx.cache_key %]" alt="" />
+ [% l("Remove from basket") %]
</a>
- [% END %]
- [% END %]
</div>
<div class="rdetail_aux_utils toggle_list">
[% IF ctx.mylist.size %]
[%- IF ctx.user; %]
- <a href="[% mkurl(ctx.opac_root _ '/myopac/lists') %]" class="no-dec" rel="nofollow" vocab=""><img src="[% ctx.media_prefix %]/images/clipboard.png[% ctx.cache_key %]" alt="[% l('View My Lists') %]" />[% l(' View My Lists') %]</a>
+ <a href="[% mkurl(ctx.opac_root _ '/myopac/lists') %]" class="no-dec" rel="nofollow" vocab=""><img src="[% ctx.media_prefix %]/images/clipboard.png[% ctx.cache_key %]" alt="[% l('View Basket') %]" />[% l(' View Basket') %]</a>
[%- ELSE %]
- <a href="[% mkurl(ctx.opac_root _ '/mylist') %]" class="no-dec" rel="nofollow" vocab=""><img src="[% ctx.media_prefix %]/images/clipboard.png[% ctx.cache_key %]" alt="[% l('View My Temporary List') %]" />[% l(' View My Temporary List') %]</a>
+ <a href="[% mkurl(ctx.opac_root _ '/mylist') %]" class="no-dec" rel="nofollow" vocab=""><img src="[% ctx.media_prefix %]/images/add-to-cart.png[% ctx.cache_key %]" alt="[% l('View My Basket') %]" />[% l(' View My Basket') %]</a>
[%- END %]
[% END %]
</div>
+ <div class="rdetail_aux_utils toggle_list">
+ [% IF !ctx.is_staff %]
+ [% IF ctx.user;
+ INCLUDE "opac/parts/bookbag_actions.tt2";
+ END;
+ %]
+ [% END %]
+ </div>
<div class="rdetail_aux_utils">
<img src="[% ctx.media_prefix %]/images/clipboard.png[% ctx.cache_key %]" alt="[% l('Print / Email Actions Image') %]" />
<a href="[% mkurl(ctx.opac_root _ '/record/print/' _ ctx.bre_id) %]" class="no-dec" rel="nofollow" vocab="">[% l('Print') %]</a> /
<h3 class="sr-only">[% l('Search Results List') %]</h3>
</div>
<div id="result_block" class="result_block_visible">
+ [% IF !ctx.is_meta %]
+ <div id="record_selector_block" class="hidden">
+ <input type="checkbox" id="select_all_records"></input>
+ <label for="select_all_records">[% l('Select [_1] - [_2]', ctx.result_start, ctx.result_stop) %]</label>
+ <span id="selected_records_summary">
+ <a href="[% mkurl(ctx.opac_root _ '/mylist') %]" class="no-dec" rel="nofollow" vocab="">
+ <span id="selected_records_count">[% ctx.mylist.size %]</span>
+ [% IF ctx.mylist.size == 1; %]
+ [% l('selected title') %]
+ [% ELSE; %]
+ [% l('selected titles') %]
+ [% END; %]
+ </a>
+ <span id="hit_selected_record_limit" class="hidden">Reached limit!</span>
+ <span>
+ <a id="clear_basket" href="#">[% l('Clear cart') %]</a>
+ </div>
+ [% END %]
<table id="result_table_table" title="[% l('Search Results') %]"
class="table_no_border_space table_no_cell_pad">
<thead class="sr-only">
add_parms.import(
{query => ctx.naive_query_scrub(ctx.user_query)} );
END;
+ is_selected = ctx.mylist.grep('^' _ rec.id _ '$').size;
%]
- <tr class="result_table_row">
- <td class="results_row_count" name="results_row_count">[%
- result_count; result_count = result_count + 1
- %].</td>
+ <tr class="result_table_row [% IF is_selected %]result_table_row_selected[% END %]">
+ <td class="results_row_count" name="results_row_count">
+ [% IF !ctx.is_meta; %]
+ <input type="checkbox" id="select-[% rec.bre_id %]" name="selected_record"
+ [% IF is_selected %] checked="checked" [% END %]
+ title="[% l('Add to Basket') %]"
+ class="result_record_selector hidden" value="[% rec.bre_id %]"></input>
+ [% END %]
+ [% result_count; result_count = result_count + 1 %].
+ </td>
<td class='result_table_pic_header'>
<a href="[% mkurl(record_url_path, add_parms, del_parms); %]">
<img alt="[% l('Book cover') %]"
[% IF !ctx.is_meta %]
<div class="results_aux_utils result_util">
[% IF !ctx.is_staff %]
+ [%
+ addhref = mkurl(ctx.opac_root _ '/mylist/add',
+ {record => rec.id, anchor => 'record_' _ rec.id}, 1);
+ delhref = mkurl(ctx.opac_root _ '/mylist/delete',
+ {record => rec.id, anchor => 'record_' _ rec.id}, 1);
+ %]
+ <a href="[% addhref %]" id="mylist_add_[% rec.id %]"
+ data-recid="[% rec.id %]" data-action="add"
+ class="mylist_action [% IF ctx.mylist.grep('^' _ rec.id _ '$').size %]hidden[% END %]"
+ title="[% l("Add [_1] to basket", attrs.title) %]" rel="nofollow" vocab="">
+ <img src="[% ctx.media_prefix %]/images/add-to-cart.png[% ctx.cache_key %]" alt="" />
+ [% l("Add to basket") %]
+ </a>
+ <a href="[% delhref %]" id="mylist_delete_[% rec.id %]"
+ data-recid="[% rec.id %]" data-action="delete"
+ class="mylist_action [% IF !ctx.mylist.grep('^' _ rec.id _ '$').size %]hidden[% END %]"
+ title="[% l("Remove [_1] from basket", attrs.title) %]" rel="nofollow" vocab="">
+ <img src="[% ctx.media_prefix %]/images/add-to-cart.png[% ctx.cache_key %]" alt="" />
+ [% l("Remove from basket") %]
+ </a>
[% IF ctx.user;
INCLUDE "opac/parts/bookbag_actions.tt2";
+ END;
%]
- [% ELSE;
- operation = ctx.mylist.grep(rec.id).size ? "delete" : "add";
- label = (operation == "add") ? l("Add to my list") : l("Remove from my list");
- title_label = (operation == "add") ?
- l("Add [_1] to my list", attrs.title) :
- l("Remove [_1] from my list", attrs.title);
- href = mkurl(ctx.opac_root _ '/mylist/' _ operation,
- {record => rec.id, anchor => 'record_' _ rec.id}, 1);
- %]
- <a href="[% href %]" class="no-dec"
- [% html_text_attr('title', title_label) %] rel="nofollow" vocab="">
- <img src="[% ctx.media_prefix %]/images/clipboard.png[% ctx.cache_key %]" alt="" />
- [% label %]
- </a>
- [% END %]
[% END %]
</div>
[% END %]
<span class="adv_search_catalog_lbl"><a href="[% mkurl(ctx.opac_root _ '/advanced', {}, expert_search_parms.merge(browse_search_parms, facet_search_parms)) %]"
id="home_adv_search_link">[% l('Advanced Search') %]</a></span>
<span class="browse_the_catalog_lbl"><a href="[% mkurl(ctx.opac_root _ '/browse', {}, expert_search_parms.merge(general_search_parms, facet_search_parms, ['fi:has_browse_entry'])) %]">[% l('Browse the Catalog') %]</a></span>
+ [% INCLUDE 'opac/parts/cart.tt2' %]
</div>
<div class="searchbar">
<span class='search_box_wrapper'>
</div>
<a href="[% mkurl(ctx.opac_root _ '/myopac/main', {}, ['single', 'message_id', 'sort','sort_type', 'hid']) %]"
class="opac-button">[% l('My Account') %]</a>
- <a href="[% mkurl(ctx.opac_root _ '/myopac/lists', {}, ['single', 'message_id', 'hid']) %]"
+ <a href="[% mkurl(ctx.opac_root _ '/myopac/lists', {}, ['single', 'message_id', 'hid', 'from_basket']) %]"
class="opac-button">[% l('My Lists') %]</a>
<a href="[% mkurl(ctx.opac_root _ '/logout', {}, 1) %]"
class="opac-button" id="logout_link">[% l('Logout') %]</a>
</h2>
[% END %]
<br/>
+ [% IF ctx.redirect_to %]
+ <p>[ <a href="[% ctx.redirect_to | html %]">[% l("Return") %]</a> ] </p>
+ [% ELSE %]
<p>[ <a href="[% mkurl(ctx.opac_root _ '/record/' _ ctx.bre_id) %]">[% l("Back to Record") %]</a> ]</p>
+ [% END %]
<div class="common-full-pad"></div>
</div>
<br class="clear-both" />
[% END %]
<div class='noprint'>
<hr />
+ [% IF ctx.redirect_to %]
+ <p>[ <a href="[% ctx.redirect_to | html %]">[% l("Return") %]</a> ] </p>
+ [% ELSE %]
<p>[ <a href="[% mkurl(ctx.opac_root _ '/record/' _ ctx.bre_id) %]">[% l("Back to Record") %]</a> ]</p>
+ [% END %]
</div>
</body>
</html>
[% IF ctx.mylist.size %]
<div class="results_header_btns">
[%- IF ctx.user; %]
- <a href="[% mkurl(ctx.opac_root _ '/myopac/lists') %]">[% l('View My List') %]</a>
+ <a href="[% mkurl(ctx.opac_root _ '/myopac/lists') %]">[% l('View My Basket') %]</a>
[%- ELSE %]
- <a href="[% mkurl(ctx.opac_root _ '/mylist') %]">[% l('View My List') %]</a>
+ <a href="[% mkurl(ctx.opac_root _ '/mylist') %]">[% l('View My Basket') %]</a>
[%- END %]
</div>
[% END %]
PROCESS "opac/parts/misc_util.tt2";
WRAPPER "opac/parts/base.tt2";
INCLUDE "opac/parts/topnav.tt2";
- ctx.page_title = l("Temporary List Warning") %]
- <h2 class="sr-only">[% l('Temporary List Warning') %]</h2>
+ ctx.page_title = l("Basket Warning") %]
+ <h2 class="sr-only">[% l('Basket Warning') %]</h2>
[% INCLUDE "opac/parts/searchbar.tt2" %]
<div id="content-wrapper">
<div id="main-content">
- <p class="big-strong">[% l('You are adding to a temporary list.') %]
+ <p class="big-strong">[% l('You are adding to a basket.') %]
[% IF ctx.user ;
l('This information will disappear when you logout, unless you save it to a permanent list.');
ELSE;
--- /dev/null
+;(function () {
+
+ var rec_selector_block = document.getElementById("record_selector_block");
+ var rec_selectors = document.getElementsByClassName("result_record_selector");
+ var mylist_action_links = document.getElementsByClassName("mylist_action");
+ var record_basket_count_el = document.getElementById('record_basket_count');
+ var selected_records_count_el = document.getElementById('selected_records_count');
+ var select_all_records_el = document.getElementById('select_all_records');
+ var clear_basket_el = document.getElementById('clear_basket');
+ var select_action_el = document.getElementById('select_basket_action');
+ var do_basket_action_el = document.getElementById('do_basket_action');
+ var mylist = [];
+
+ function initialize() {
+ var req = new window.XMLHttpRequest();
+ req.open('GET', '/eg/opac/api/mylist/retrieve');
+ if (('responseType' in req) && (req.responseType = 'json')) {
+ req.onload = function (evt) {
+ var result = req.response;
+ handleUpdate(result);
+ syncPageState();
+ }
+ } else {
+ // IE 10/11
+ req.onload = function (evt) {
+ var result = JSON.parse(req.responseText);
+ handleUpdate(result);
+ syncPageState();
+ }
+ }
+ req.send();
+ }
+ initialize();
+
+ function syncPageState() {
+ var all_checked = true;
+ var legacy_adjusted = false;
+ [].forEach.call(rec_selectors, function(el) {
+ el.checked = mylist.includes(parseInt(el.value));
+ if (el.checked) {
+ adjustLegacyControlsVis('checked', el.value);
+ } else {
+ all_checked = false;
+ adjustLegacyControlsVis('unchecked', el.value);
+ }
+ toggleRowHighlighting(el);
+ legacy_adjusted = true;
+ });
+ if (!legacy_adjusted) {
+ [].forEach.call(mylist_action_links, function(el) {
+ if ('dataset' in el) {
+ if (el.dataset.action == 'delete') return;
+ // only need to do this once
+ var op = mylist.includes(parseInt(el.dataset.recid)) ? 'checked' : 'unchecked';
+ adjustLegacyControlsVis(op, el.dataset.recid);
+ }
+ });
+ }
+ if (select_all_records_el && rec_selectors.length) {
+ select_all_records_el.checked = all_checked;
+ }
+ checkMaxCartSize();
+ }
+
+ function handleUpdate(result) {
+ if (result) {
+ mylist = result.mylist;
+ if (selected_records_count_el) {
+ selected_records_count_el.innerHTML = mylist.length;
+ }
+ if (clear_basket_el) {
+ if (mylist.length > 0) {
+ clear_basket_el.classList.remove('hidden');
+ } else {
+ clear_basket_el.classList.add('hidden');
+ }
+ }
+ if (select_action_el) {
+ if (mylist.length > 0) {
+ select_action_el.removeAttribute('disabled');
+ } else {
+ select_action_el.setAttribute('disabled', 'disabled');
+ }
+ }
+ if (do_basket_action_el) {
+ if (mylist.length > 0) {
+ do_basket_action_el.removeAttribute('disabled');
+ } else {
+ do_basket_action_el.setAttribute('disabled', 'disabled');
+ }
+ }
+ if (record_basket_count_el) {
+ record_basket_count_el.innerHTML = mylist.length;
+ }
+ checkMaxCartSize();
+ }
+ }
+
+ function mungeList(op, rec, resync) {
+ console.debug('calling mungeList to ' + op + ' record ' + rec);
+ var req = new window.XMLHttpRequest();
+ if (Array.isArray(rec)) {
+ var qrec = rec.map(function(rec) {
+ return 'record=' + encodeURIComponent(rec);
+ }).join('&');
+ } else {
+ var qrec = 'record=' + encodeURIComponent(rec);
+ }
+ req.open('GET', '/eg/opac/api/mylist/' + op + '?' + qrec);
+ if (('responseType' in req) && (req.responseType = 'json')) {
+ req.onload = function (evt) {
+ var result = req.response;
+ handleUpdate(result);
+ if (resync) syncPageState();
+ }
+ } else {
+ // IE 10/11
+ req.onload = function (evt) {
+ var result = JSON.parse(req.responseText);
+ handleUpdate(result);
+ if (resync) syncPageState();
+ }
+ }
+ req.send();
+ }
+
+ function adjustLegacyControlsVis(op, rec) {
+ if (op == 'add' || op == 'checked') {
+ var t;
+ if (t = document.getElementById('mylist_add_' + rec)) t.classList.add('hidden');
+ if (t = document.getElementById('mylist_delete_' + rec)) t.classList.remove('hidden');
+ } else if (op == 'delete' || op == 'unchecked') {
+ if (t = document.getElementById('mylist_add_' + rec)) t.classList.remove('hidden');
+ if (t = document.getElementById('mylist_delete_' + rec)) t.classList.add('hidden');
+ }
+ }
+
+ function findAncestorWithClass(el, cls) {
+ while ((el = el.parentElement) && !el.classList.contains(cls));
+ return el;
+ }
+ function toggleRowHighlighting(el) {
+ var row = findAncestorWithClass(el, "result_table_row");
+ if (!row) return;
+ if (el.checked) {
+ row.classList.add('result_table_row_selected');
+ } else {
+ row.classList.remove('result_table_row_selected');
+ }
+ }
+
+ function checkMaxCartSize() {
+ if ((typeof max_cart_size === 'undefined') || !max_cart_size) return;
+ var alertel = document.getElementById('hit_selected_record_limit');
+ [].forEach.call(rec_selectors, function(el) {
+ if (!el.checked) el.disabled = (mylist.length >= max_cart_size);
+ });
+ [].forEach.call(mylist_action_links, function(el) {
+ if ('dataset' in el && el.dataset.action == 'add') {
+ if (mylist.length >= max_cart_size) {
+ // hide the add link
+ el.classList.add('hidden');
+ } else {
+ // show the add link unless the record is
+ // already in the cart
+ if (!mylist.includes(parseInt(el.dataset.recid))) el.classList.remove('hidden');
+ }
+ }
+ });
+ if (mylist.length >= max_cart_size) {
+ if (alertel) alertel.classList.remove('hidden');
+ if (select_all_records_el && !select_all_records_el.checked) {
+ select_all_records_el.disabled = true;
+ }
+ } else {
+ if (alertel) alertel.classList.add('hidden');
+ if (select_all_records_el) select_all_records_el.disabled = false;
+ }
+ }
+
+ var all_checked = true;
+ [].forEach.call(rec_selectors, function(el) {
+ el.addEventListener("click", function() {
+ if (this.checked) {
+ mungeList('add', this.value);
+ adjustLegacyControlsVis('add', this.value);
+ } else {
+ mungeList('delete', this.value);
+ adjustLegacyControlsVis('delete', this.value);
+ }
+ toggleRowHighlighting(el);
+ }, false);
+ el.classList.remove("hidden");
+ if (!el.checked) all_checked = false;
+ });
+ if (select_all_records_el && rec_selectors.length) {
+ select_all_records_el.checked = all_checked;
+ }
+ if (rec_selector_block) rec_selector_block.classList.remove("hidden");
+
+ function deselectSelectedOnPage() {
+ [].forEach.call(rec_selectors, function(el) {
+ if (el.checked) {
+ el.checked = false;
+ adjustLegacyControlsVis('delete', el.value);
+ toggleRowHighlighting(el);
+ }
+ });
+ }
+
+ if (select_all_records_el) {
+ select_all_records_el.addEventListener('click', function() {
+ if (this.checked) {
+ // adding
+ var to_add = [];
+ [].forEach.call(rec_selectors, function(el) {
+ if (!el.checked) {
+ el.checked = true;
+ adjustLegacyControlsVis('add', el.value);
+ toggleRowHighlighting(el);
+ to_add.push(el.value);
+ }
+ });
+ if (to_add.length > 0) {
+ mungeList('add', to_add);
+ }
+ } else {
+ // deleting
+ deselectSelectedOnPage();
+ }
+ });
+ }
+
+ function clearCart() {
+ var req = new window.XMLHttpRequest();
+ req.open('GET', '/eg/opac/api/mylist/clear');
+ if (('responseType' in req) && (req.responseType = 'json')) {
+ req.onload = function (evt) {
+ var result = req.response;
+ handleUpdate(result);
+ syncPageState();
+ }
+ } else {
+ // IE 10/11
+ req.onload = function (evt) {
+ var result = JSON.parse(req.responseText);
+ handleUpdate(result);
+ syncPageState();
+ }
+ }
+ req.send();
+ }
+
+ if (clear_basket_el) {
+ clear_basket_el.addEventListener('click', function() {
+ if (confirm(window.egStrings['CONFIRM_BASKET_EMPTY'])) {
+ clearCart();
+ }
+ });
+ }
+
+ [].forEach.call(mylist_action_links, function(el) {
+ el.addEventListener("click", function(evt) {
+ var recid;
+ var action;
+ if ('dataset' in el) {
+ recid = el.dataset.recid;
+ action = el.dataset.action;
+ mungeList(action, recid, true);
+ evt.preventDefault();
+ }
+ });
+ });
+
+ if (do_basket_action_el) {
+ do_basket_action_el.addEventListener('click', function(evt) {
+ if (select_action_el.options[select_action_el.selectedIndex].value) {
+ window.location.href = select_action_el.options[select_action_el.selectedIndex].value;
+ }
+ evt.preventDefault();
+ });
+ }
+
+})();
}
}
- $scope.add_to_record_bucket = function() {
- var recId = $scope.record_id;
+ $scope.add_cart_to_record_bucket = function() {
+ var cartkey = $cookies.get('cartcache');
+ if (!cartkey) return;
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.anon_cache.get_value',
+ cartkey,
+ 'mylist'
+ ).then(function(list) {
+ list = list.map(function(x) {
+ return parseInt(x);
+ });
+ $scope.add_to_record_bucket(list);
+ });
+ }
+
+ $scope.add_to_record_bucket = function(recs) {
+ if (!angular.isArray(recs)) {
+ recs = [ $scope.record_id ];
+ }
return $uibModal.open({
templateUrl: './cat/catalog/t_add_to_bucket',
backdrop: 'static',
).then(function(buckets) { $scope.allBuckets = buckets; });
$scope.add_to_bucket = function() {
- var item = new egCore.idl.cbrebi();
- item.bucket($scope.bucket_id);
- item.target_biblio_record_entry(recId);
- egCore.net.request(
- 'open-ils.actor',
- 'open-ils.actor.container.item.create',
- egCore.auth.token(), 'biblio', item
- ).then(function(resp) {
+ var promises = [];
+ angular.forEach(recs, function(recId) {
+ var item = new egCore.idl.cbrebi();
+ item.bucket($scope.bucket_id);
+ item.target_biblio_record_entry(recId);
+ promises.push(egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.container.item.create',
+ egCore.auth.token(), 'biblio', item
+ ));
+ });
+ $q.all(promises).then(function(resp) {
$uibModalInstance.close();
});
}
$(doc).find('#hold_usr_input').val(barc);
$(doc).find('#hold_usr_input').change();
});
- })
+ });
+ $(doc).find('#select_basket_action').on('change', function() {
+ if (this.options[this.selectedIndex].value && this.options[this.selectedIndex].value == "add_cart_to_bucket") {
+ $scope.add_cart_to_record_bucket();
+ }
+ });
}
}
--- /dev/null
+Batch Actions In the Public Catalog
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The public catalog now displays checkboxes on the bibliographic and
+metarecord constituents results pages. Selecting one or more titles
+by using the checkboxes will dynamically add those title to the
+temporary list, which is now renamed the cart.
+
+Above the results lists there is now a bar with a select-all checkbox,
+a link to the cart management page that also indicates the number of
+of titles in the cart, and a link to remove from the cart titles that
+are selected on the currently displayed results page.
+
+The search bar now includes an icon of a cart and displays the number
+of titles currently in the cart. Next to that icon is a menu of cart
+actions.
+
+The cart actions available are Place Hold, Print Title Details,
+Email Title Details, Add Cart to Saved List, and Clear Cart. In the
+web staff client, the cart actions also include Add Cart to Bucket.
+When an action is selected from this menu, the user is given an
+opportunity to confirm the action and to optionally empty the cart
+when the action is complete. The action is applied to all titles
+in the cart.
+
+Clicking on the cart icon brings the user to a page listing the
+titles in the cart. From there, the user can select specific records
+to request, print, email, add to a list, or remove from the cart.
+
+The list of actions on the record details page now provides separate
+links for adding the title to a cart or to a permanent list.
+
+The permanent list management page in the public catalog now also
+includes batch print and email actions.
+
+Additional information
+++++++++++++++++++++++
+* The checkboxes do not display on the metarecord results page, as
+ metarecords currently cannot be put into carts or lists.
+* The checkboxes are displayed only if Javascript is enabled. However,
+ users can still add items to the cart and perform batch actions on
+ the cart and on lists.
+* A template `config.tt2` setting, `ctx.max_cart_size`, can be used to
+ set a soft limit on the number of titles that can be added to the
+ cart. If this limit is reached, checkboxes to add more records to the
+ cart are disabled unless existing titles in the cart are removed
+ first. The default value for this setting is 500.
+
+Developer notes
++++++++++++++++
+
+This patch adds the the public catalog two routes that return JSON
+rather than HTML:
+
+* `GET /eg/opac/api/mylist/add?record=45`
+* `GET /eg/opac/api/mylist/delete?record=45`
+
+The JSON response is a hash containing a mylist key pointing to the list
+of bib IDs of contents of the cart.
+
+The record parameter can be repeated to allow adding or removing
+records as an atomic operation. Note that this change also now available
+to `/eg/opac/mylist/{add,delete}`
+
+More generally, this adds a way for EGWeb context loaders to specify that
+a response should be emitted as JSON rather than rendering an HTML
+page using `Template::Toolkit`.
+
+Specifically, if the context as munged by the context loader contains
+a `json_response` key, the contents of that key will to provide a
+JSON reponse. The `json_response_cookie` key, if present, can be used
+to set a cookie as part of the response.
+
+Template Toolkit processing is bypassed entirely when emitting a JSON
+response, so the context loader would be entirely reponsible for
+localization of strings in the response meant for direct human
+consumption.