LP#1721575: Batch Actions In the Public Catalog
authorGalen Charlton <gmc@equinoxinitiative.org>
Mon, 14 May 2018 19:24:59 +0000 (15:24 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 15 Aug 2018 18:08:42 +0000 (14:08 -0400)
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 basket.

Above the results lists there is now a bar with a select-all checkbox,
a link to the basket management page that also indicates the number of
of titles in the basket, and a link to remove from the basket titles that
are selected on the currently displayed results page.

The search bar now includes an icon of a basket and displays the number
of titles currently in the basket. Next to that icon is a menu of basket
actions.

The basket 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 basket 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 basket
when the action is complete. The action is applied to all titles
in the basket.

Clicking on the basket icon brings the user to a page listing the
titles in the basket. From there, the user can select specific records
to request, print, email, add to a list, or remove from the basket.

The list of actions on the record details page now provides separate
links for adding the title to a basket 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 baskets or lists.
* The checkboxes are displayed only if Javascript is enabled. However,
  users can still add items to the basket and perform batch actions on
  the basket and on lists.
* A template `config.tt2` setting, `ctx.max_basket_size`, can be used to
  set a soft limit on the number of titles that can be added to the
  basket. If this limit is reached, checkboxes to add more records to the
  basket are disabled unless existing titles in the basket 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 basket.

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.

Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
Signed-off-by: Kathy Lussier <klussier@masslnc.org>
Signed-off-by: Bill Erickson <berickxx@gmail.com>
37 files changed:
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Container.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGWeb.pm
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.bre_format_title_fix.sql [new file with mode: 0644]
Open-ILS/src/templates/opac/advanced.tt2
Open-ILS/src/templates/opac/browse.tt2
Open-ILS/src/templates/opac/css/style.css.tt2
Open-ILS/src/templates/opac/mylist.tt2
Open-ILS/src/templates/opac/mylist/clear.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/mylist/email.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/mylist/print.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/myopac/lists.tt2
Open-ILS/src/templates/opac/parts/anon_list.tt2
Open-ILS/src/templates/opac/parts/bookbag_actions.tt2
Open-ILS/src/templates/opac/parts/cart.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/parts/config.tt2
Open-ILS/src/templates/opac/parts/css/colors.tt2
Open-ILS/src/templates/opac/parts/header.tt2
Open-ILS/src/templates/opac/parts/js.tt2
Open-ILS/src/templates/opac/parts/place_hold.tt2
Open-ILS/src/templates/opac/parts/record/summary.tt2
Open-ILS/src/templates/opac/parts/result/table.tt2
Open-ILS/src/templates/opac/parts/searchbar.tt2
Open-ILS/src/templates/opac/parts/topnav.tt2
Open-ILS/src/templates/opac/record/email.tt2
Open-ILS/src/templates/opac/record/print.tt2
Open-ILS/src/templates/opac/results.tt2
Open-ILS/src/templates/opac/temp_warn.tt2
Open-ILS/web/images/add-to-cart.png [new file with mode: 0644]
Open-ILS/web/images/cart-md.png [new file with mode: 0644]
Open-ILS/web/images/cart-sm.png [new file with mode: 0644]
Open-ILS/web/js/ui/default/opac/record_selectors.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
docs/RELEASE_NOTES_NEXT/OPAC/Batch_Actions.adoc [new file with mode: 0644]

index d3aecaa..eeeba4a 100644 (file)
@@ -36,7 +36,8 @@ use constant COOKIE_PHYSICAL_LOC => 'eg_physical_loc';
 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;
@@ -129,7 +130,13 @@ sub load {
     }
 
     (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|;
@@ -146,7 +153,8 @@ sub load {
     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|;
@@ -190,6 +198,11 @@ sub load {
     $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|;
index 027948b..d78b59b 100644 (file)
@@ -934,7 +934,8 @@ sub handle_hold_update {
         $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;
             }
         }
     }
@@ -1421,6 +1422,10 @@ sub attempt_hold_placement {
 
         $bses->kill_me;
     }
+
+    if ($self->cgi->param('clear_cart')) {
+        $self->clear_anon_cache;
+    }
 }
 
 # pull the selected formats and languages for metarecord holds
@@ -2400,7 +2405,8 @@ sub load_myopac_bookbags {
 
                     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;
                         }
                     }
 
@@ -2473,7 +2479,7 @@ sub load_myopac_bookbags {
 }
 
 
-# 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) = @_;
@@ -2490,11 +2496,22 @@ sub load_myopac_bookbag_update {
     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.
 
@@ -2503,7 +2520,8 @@ sub load_myopac_bookbag_update {
 
     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;
         }
     }
 
@@ -2518,15 +2536,29 @@ sub load_myopac_bookbag_update {
             $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'));
@@ -2537,7 +2569,8 @@ sub load_myopac_bookbag_update {
 
     } 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});
@@ -2550,13 +2583,21 @@ sub load_myopac_bookbag_update {
 
         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);
index df7b79f..6b07672 100644 (file)
@@ -10,17 +10,17 @@ my $U = 'OpenILS::Application::AppUtils';
 # 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;
@@ -59,7 +59,7 @@ sub fetch_mylist {
 
     # 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 = {
@@ -73,19 +73,40 @@ sub fetch_mylist {
     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:
@@ -96,30 +117,181 @@ sub load_mylist_add {
     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;
@@ -128,9 +300,14 @@ sub load_mylist_move {
     $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'));
@@ -151,7 +328,7 @@ sub clear_anon_cache {
     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',
@@ -170,14 +347,16 @@ sub mylist_action_redirect {
     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'
@@ -209,7 +388,7 @@ sub mylist_warning_redirect {
     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'
@@ -222,6 +401,12 @@ sub load_mylist {
     (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;
 }
 
index 2e48ce3..c071c24 100644 (file)
@@ -509,13 +509,40 @@ sub get_hold_copy_summary {
 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;
 }
@@ -523,14 +550,41 @@ sub load_print_record {
 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;
 }
index 8dc09cb..0d2fe26 100644 (file)
@@ -81,6 +81,19 @@ sub handler_guts {
         $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);
index c2eb7f6..46e9bf6 100644 (file)
@@ -11772,11 +11772,12 @@ Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %]
 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;
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.bre_format_title_fix.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.bre_format_title_fix.sql
new file mode 100644 (file)
index 0000000..68e47cb
--- /dev/null
@@ -0,0 +1,86 @@
+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';
index de86006..d2549e9 100644 (file)
@@ -16,6 +16,7 @@
 
             <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">
index ea27b7d..dbaaa35 100644 (file)
@@ -37,6 +37,7 @@
                     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">
index e176ed7..31bd817 100644 (file)
@@ -1030,6 +1030,56 @@ tr.result_table_row > td.result_table_pic_header {
     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;
@@ -2399,19 +2449,20 @@ a.preflib_change {
     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 %];
index 5057800..25cb8b1 100644 (file)
@@ -4,7 +4,7 @@
     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>
@@ -13,7 +13,8 @@
             [%  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>
diff --git a/Open-ILS/src/templates/opac/mylist/clear.tt2 b/Open-ILS/src/templates/opac/mylist/clear.tt2
new file mode 100644 (file)
index 0000000..7795c48
--- /dev/null
@@ -0,0 +1,21 @@
+[%- 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 %]
diff --git a/Open-ILS/src/templates/opac/mylist/email.tt2 b/Open-ILS/src/templates/opac/mylist/email.tt2
new file mode 100644 (file)
index 0000000..04a7a3b
--- /dev/null
@@ -0,0 +1,29 @@
+[%- 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 %]
diff --git a/Open-ILS/src/templates/opac/mylist/print.tt2 b/Open-ILS/src/templates/opac/mylist/print.tt2
new file mode 100644 (file)
index 0000000..ac53a21
--- /dev/null
@@ -0,0 +1,29 @@
+[%- 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 %]
index 12f8003..3f76517 100644 (file)
                     </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>&nbsp;</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 %]
index 9ec6d58..a834dab 100644 (file)
@@ -1,9 +1,9 @@
         [% 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;}"/>
@@ -50,7 +59,7 @@
                         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="[%-
index 0b0ee67..b18a4ae 100644 (file)
@@ -44,9 +44,6 @@
           [% 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";
diff --git a/Open-ILS/src/templates/opac/parts/cart.tt2 b/Open-ILS/src/templates/opac/parts/cart.tt2
new file mode 100644 (file)
index 0000000..ae587bc
--- /dev/null
@@ -0,0 +1,30 @@
+<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>
index b0ad747..ac85bdf 100644 (file)
@@ -265,4 +265,9 @@ ctx.exclude_electronic_checkbox = 0;
 ##############################################################################
 ctx.hide_badge_scores = 'false';
 
+##############################################################################
+# Maximum number of items allowed to be stored in a basket
+##############################################################################
+ctx.max_cart_size = 500;
+
 %]
index 85e00bd..b8c5ca8 100644 (file)
@@ -32,6 +32,7 @@
         button_text_shadow = "#555555", # medium grey
         table_heading = "#d8d8d8", # grey-blue
         mobile_header_text = "#fff", # white
+        item_selected = "#ddd", # grey (lighter)
     };
     
 %]
index de933e9..76b2314 100644 (file)
@@ -9,7 +9,7 @@
     # 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'); 
index 01fb9f9..19ad6ff 100644 (file)
 <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 %]">
@@ -143,3 +149,7 @@ var aou_hash = {
 
 <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; %]
index 099208c..23eadde 100644 (file)
@@ -66,7 +66,8 @@ function maybeToggleNumCopies(obj) {
                 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 %]" />
         [%  
@@ -129,6 +130,7 @@ function maybeToggleNumCopies(obj) {
             </span>
         </p>
         [% END %]
+      [% END %]
 
         <table id='hold-items-list'>
             <tr>
@@ -180,7 +182,7 @@ function maybeToggleNumCopies(obj) {
                         [% 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 %]
@@ -276,6 +278,9 @@ function maybeToggleNumCopies(obj) {
                 <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" />
index 223b0f3..3bc0ef5 100644 (file)
             [%- 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> /
index 20da7de..8241e68 100644 (file)
     <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') %]"
@@ -451,24 +476,30 @@ END;
                                                     [% 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 %]
index 0a5f62f..4cf34a3 100644 (file)
@@ -44,6 +44,7 @@ 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'>
index 68fd3bb..e9b58ff 100644 (file)
@@ -36,7 +36,7 @@
                 </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>
index 63520fd..88cf88c 100644 (file)
             </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" />
index 24cb94e..26c3543 100644 (file)
         [% 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>
index 4c3d6f6..53b8dfd 100644 (file)
@@ -57,9 +57,9 @@
                 [% 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 %]
index 8aa978d..8fdea97 100644 (file)
@@ -2,12 +2,12 @@
     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;
diff --git a/Open-ILS/web/images/add-to-cart.png b/Open-ILS/web/images/add-to-cart.png
new file mode 100644 (file)
index 0000000..d1fcf6d
Binary files /dev/null and b/Open-ILS/web/images/add-to-cart.png differ
diff --git a/Open-ILS/web/images/cart-md.png b/Open-ILS/web/images/cart-md.png
new file mode 100644 (file)
index 0000000..a18c7af
Binary files /dev/null and b/Open-ILS/web/images/cart-md.png differ
diff --git a/Open-ILS/web/images/cart-sm.png b/Open-ILS/web/images/cart-sm.png
new file mode 100644 (file)
index 0000000..b027326
Binary files /dev/null and b/Open-ILS/web/images/cart-sm.png differ
diff --git a/Open-ILS/web/js/ui/default/opac/record_selectors.js b/Open-ILS/web/js/ui/default/opac/record_selectors.js
new file mode 100644 (file)
index 0000000..6116b39
--- /dev/null
@@ -0,0 +1,284 @@
+;(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();
+        });
+    }
+
+})();
index 63a177e..b2435e9 100644 (file)
@@ -338,8 +338,26 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         }
     }
 
-    $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',
@@ -360,14 +378,18 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
                 ).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();
                     });
                 }
@@ -605,7 +627,12 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
                     $(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();
+                }
+            });
         }
 
     }
diff --git a/docs/RELEASE_NOTES_NEXT/OPAC/Batch_Actions.adoc b/docs/RELEASE_NOTES_NEXT/OPAC/Batch_Actions.adoc
new file mode 100644 (file)
index 0000000..cba31b6
--- /dev/null
@@ -0,0 +1,76 @@
+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.