[open-ils-commits] [GIT] Evergreen ILS branch master updated. 4e0528b11a244734b0d2835dd8ce6c96fb28a5fd

Evergreen Git git at git.evergreen-ils.org
Wed Aug 15 14:23:44 EDT 2018


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  4e0528b11a244734b0d2835dd8ce6c96fb28a5fd (commit)
       via  ba5f74d896b52b8e64093324220ceba4f5100862 (commit)
       via  deab49ceef1b036c7918c3103282e0be13710980 (commit)
      from  fdd829da38d9b62e301538af968b727a7f19c330 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 4e0528b11a244734b0d2835dd8ce6c96fb28a5fd
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Aug 15 14:08:41 2018 -0400

    LP#1721575 Stamping DB upgrade: batch catalog actions
    
    Also replaced a remaining reference to "cart" with "basket"
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 08763f6..728d937 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -92,7 +92,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1117', :eg_version); -- berick/kmlussier
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1118', :eg_version); -- gmcharlt/berick
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.bre_format_title_fix.sql b/Open-ILS/src/sql/Pg/upgrade/1118.data.bre_format_title_fix.sql
similarity index 97%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.data.bre_format_title_fix.sql
rename to Open-ILS/src/sql/Pg/upgrade/1118.data.bre_format_title_fix.sql
index 68e47cb..73470ac 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.bre_format_title_fix.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1118.data.bre_format_title_fix.sql
@@ -1,5 +1,7 @@
 BEGIN;
 
+SELECT evergreen.upgrade_deps_block_check('1118', :eg_version);
+
 UPDATE action_trigger.event_definition
 SET template =
 $$
@@ -84,3 +86,5 @@ $$
 WHERE hook = 'biblio.format.record_entry.print'
 -- from previous stock definition
 AND MD5(template) = '9ada7ea8417cb23f89d0dc8f15ec68d0';
+
+COMMIT;
diff --git a/Open-ILS/src/templates/opac/parts/anon_list.tt2 b/Open-ILS/src/templates/opac/parts/anon_list.tt2
index a834dab..56fbd6c 100644
--- a/Open-ILS/src/templates/opac/parts/anon_list.tt2
+++ b/Open-ILS/src/templates/opac/parts/anon_list.tt2
@@ -3,7 +3,7 @@
         <p class="big-strong">[% l('Basket') %]</p>
         <div class="sort">
             <form method="get">
-                <label for="anonsort">[% l("Sort cart items by: ") %]</label>
+                <label for="anonsort">[% l("Sort basket items by: ") %]</label>
                 [% INCLUDE "opac/parts/filtersort.tt2" mode='bookbag'
                     id="anonsort" name="anonsort" value=CGI.param("anonsort") %]
                 <input type="hidden" name="id"

commit ba5f74d896b52b8e64093324220ceba4f5100862
Author: Kathy Lussier <klussier at masslnc.org>
Date:   Tue Aug 14 16:28:35 2018 -0400

    LP#1721575: Changes to some strings
    
    We missed one instance of cart when changing everything over to baskets. Also,
    I updated language for clearing a basket after placing a hold to make it a
    little clearer.
    
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/templates/opac/parts/place_hold.tt2 b/Open-ILS/src/templates/opac/parts/place_hold.tt2
index 23eadde..c4e7e25 100644
--- a/Open-ILS/src/templates/opac/parts/place_hold.tt2
+++ b/Open-ILS/src/templates/opac/parts/place_hold.tt2
@@ -279,7 +279,7 @@ function maybeToggleNumCopies(obj) {
             </blockquote>
         </p>
         [% IF CGI.param('from_basket') %]
-          <blockquote><input type="checkbox" name="clear_cart">[% l('Clear basket?') %]</input></blockquote>
+          <blockquote><input type="checkbox" name="clear_cart">[% l('Clear basket after holds are requested?') %]</input></blockquote>
         [% END %]
         <input id="place_hold_submit" type="submit" name="submit" 
             value="[% l('Submit') %]" title="[% l('Submit') %]"
diff --git a/Open-ILS/src/templates/opac/parts/result/table.tt2 b/Open-ILS/src/templates/opac/parts/result/table.tt2
index 8241e68..77a27f6 100644
--- a/Open-ILS/src/templates/opac/parts/result/table.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/table.tt2
@@ -45,7 +45,7 @@
                         </a>
                         <span id="hit_selected_record_limit" class="hidden">Reached limit!</span>
                     <span>
-                    <a id="clear_basket" href="#">[% l('Clear cart') %]</a>
+                    <a id="clear_basket" href="#">[% l('Clear basket') %]</a>
                 </div>
                 [% END %]
                 <table id="result_table_table" title="[% l('Search Results') %]"

commit deab49ceef1b036c7918c3103282e0be13710980
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Mon May 14 15:24:59 2018 -0400

    LP#1721575: 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 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 at equinoxinitiative.org>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
index d3aecaa..eeeba4a 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
@@ -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|;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
index 027948b..d78b59b 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
@@ -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);
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Container.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Container.pm
index df7b79f..6b07672 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Container.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Container.pm
@@ -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;
 }
 
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm
index 2e48ce3..c071c24 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm
@@ -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;
 }
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGWeb.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGWeb.pm
index 8dc09cb..0d2fe26 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGWeb.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGWeb.pm
@@ -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);
diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index c2eb7f6..46e9bf6 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -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;
@@ -11820,11 +11821,12 @@ $$
 <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
index 0000000..68e47cb
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.bre_format_title_fix.sql
@@ -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';
diff --git a/Open-ILS/src/templates/opac/advanced.tt2 b/Open-ILS/src/templates/opac/advanced.tt2
index de86006..d2549e9 100644
--- a/Open-ILS/src/templates/opac/advanced.tt2
+++ b/Open-ILS/src/templates/opac/advanced.tt2
@@ -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">
diff --git a/Open-ILS/src/templates/opac/browse.tt2 b/Open-ILS/src/templates/opac/browse.tt2
index ea27b7d..dbaaa35 100644
--- a/Open-ILS/src/templates/opac/browse.tt2
+++ b/Open-ILS/src/templates/opac/browse.tt2
@@ -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">
diff --git a/Open-ILS/src/templates/opac/css/style.css.tt2 b/Open-ILS/src/templates/opac/css/style.css.tt2
index e176ed7..31bd817 100644
--- a/Open-ILS/src/templates/opac/css/style.css.tt2
+++ b/Open-ILS/src/templates/opac/css/style.css.tt2
@@ -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 %];
diff --git a/Open-ILS/src/templates/opac/mylist.tt2 b/Open-ILS/src/templates/opac/mylist.tt2
index 5057800..25cb8b1 100644
--- a/Open-ILS/src/templates/opac/mylist.tt2
+++ b/Open-ILS/src/templates/opac/mylist.tt2
@@ -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
index 0000000..7795c48
--- /dev/null
+++ b/Open-ILS/src/templates/opac/mylist/clear.tt2
@@ -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
index 0000000..04a7a3b
--- /dev/null
+++ b/Open-ILS/src/templates/opac/mylist/email.tt2
@@ -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
index 0000000..ac53a21
--- /dev/null
+++ b/Open-ILS/src/templates/opac/mylist/print.tt2
@@ -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 %]
diff --git a/Open-ILS/src/templates/opac/myopac/lists.tt2 b/Open-ILS/src/templates/opac/myopac/lists.tt2
index 12f8003..3f76517 100644
--- a/Open-ILS/src/templates/opac/myopac/lists.tt2
+++ b/Open-ILS/src/templates/opac/myopac/lists.tt2
@@ -59,6 +59,19 @@
                     </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">
@@ -76,7 +89,11 @@
         </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">
@@ -287,16 +304,22 @@
         <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">
@@ -466,5 +489,6 @@
         [% END %]
     </div>
     [% END %]
+    [% END %]
 </div>
 [% END %]
diff --git a/Open-ILS/src/templates/opac/parts/anon_list.tt2 b/Open-ILS/src/templates/opac/parts/anon_list.tt2
index 9ec6d58..a834dab 100644
--- a/Open-ILS/src/templates/opac/parts/anon_list.tt2
+++ b/Open-ILS/src/templates/opac/parts/anon_list.tt2
@@ -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"
@@ -12,12 +12,17 @@
                 <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 %]]
@@ -28,13 +33,17 @@
             </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="[%-
diff --git a/Open-ILS/src/templates/opac/parts/bookbag_actions.tt2 b/Open-ILS/src/templates/opac/parts/bookbag_actions.tt2
index 0b0ee67..b18a4ae 100644
--- a/Open-ILS/src/templates/opac/parts/bookbag_actions.tt2
+++ b/Open-ILS/src/templates/opac/parts/bookbag_actions.tt2
@@ -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
index 0000000..ae587bc
--- /dev/null
+++ b/Open-ILS/src/templates/opac/parts/cart.tt2
@@ -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>
diff --git a/Open-ILS/src/templates/opac/parts/config.tt2 b/Open-ILS/src/templates/opac/parts/config.tt2
index b0ad747..ac85bdf 100644
--- a/Open-ILS/src/templates/opac/parts/config.tt2
+++ b/Open-ILS/src/templates/opac/parts/config.tt2
@@ -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;
+
 %]
diff --git a/Open-ILS/src/templates/opac/parts/css/colors.tt2 b/Open-ILS/src/templates/opac/parts/css/colors.tt2
index 85e00bd..b8c5ca8 100644
--- a/Open-ILS/src/templates/opac/parts/css/colors.tt2
+++ b/Open-ILS/src/templates/opac/parts/css/colors.tt2
@@ -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)
     };
     
 %]
diff --git a/Open-ILS/src/templates/opac/parts/header.tt2 b/Open-ILS/src/templates/opac/parts/header.tt2
index de933e9..76b2314 100644
--- a/Open-ILS/src/templates/opac/parts/header.tt2
+++ b/Open-ILS/src/templates/opac/parts/header.tt2
@@ -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
@@ -61,6 +61,12 @@
             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'); 
diff --git a/Open-ILS/src/templates/opac/parts/js.tt2 b/Open-ILS/src/templates/opac/parts/js.tt2
index 01fb9f9..19ad6ff 100644
--- a/Open-ILS/src/templates/opac/parts/js.tt2
+++ b/Open-ILS/src/templates/opac/parts/js.tt2
@@ -68,6 +68,12 @@
 <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; %]
diff --git a/Open-ILS/src/templates/opac/parts/place_hold.tt2 b/Open-ILS/src/templates/opac/parts/place_hold.tt2
index 099208c..23eadde 100644
--- a/Open-ILS/src/templates/opac/parts/place_hold.tt2
+++ b/Open-ILS/src/templates/opac/parts/place_hold.tt2
@@ -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" />
diff --git a/Open-ILS/src/templates/opac/parts/record/summary.tt2 b/Open-ILS/src/templates/opac/parts/record/summary.tt2
index 223b0f3..3bc0ef5 100644
--- a/Open-ILS/src/templates/opac/parts/record/summary.tt2
+++ b/Open-ILS/src/templates/opac/parts/record/summary.tt2
@@ -98,30 +98,45 @@
             [%- 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> /
diff --git a/Open-ILS/src/templates/opac/parts/result/table.tt2 b/Open-ILS/src/templates/opac/parts/result/table.tt2
index 20da7de..8241e68 100644
--- a/Open-ILS/src/templates/opac/parts/result/table.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/table.tt2
@@ -30,6 +30,24 @@
     <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">
@@ -82,11 +100,18 @@
 				    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 %]
diff --git a/Open-ILS/src/templates/opac/parts/searchbar.tt2 b/Open-ILS/src/templates/opac/parts/searchbar.tt2
index 0a5f62f..4cf34a3 100644
--- a/Open-ILS/src/templates/opac/parts/searchbar.tt2
+++ b/Open-ILS/src/templates/opac/parts/searchbar.tt2
@@ -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'>
diff --git a/Open-ILS/src/templates/opac/parts/topnav.tt2 b/Open-ILS/src/templates/opac/parts/topnav.tt2
index 68fd3bb..e9b58ff 100644
--- a/Open-ILS/src/templates/opac/parts/topnav.tt2
+++ b/Open-ILS/src/templates/opac/parts/topnav.tt2
@@ -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>
diff --git a/Open-ILS/src/templates/opac/record/email.tt2 b/Open-ILS/src/templates/opac/record/email.tt2
index 63520fd..88cf88c 100644
--- a/Open-ILS/src/templates/opac/record/email.tt2
+++ b/Open-ILS/src/templates/opac/record/email.tt2
@@ -19,7 +19,11 @@
             </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" />
diff --git a/Open-ILS/src/templates/opac/record/print.tt2 b/Open-ILS/src/templates/opac/record/print.tt2
index 24cb94e..26c3543 100644
--- a/Open-ILS/src/templates/opac/record/print.tt2
+++ b/Open-ILS/src/templates/opac/record/print.tt2
@@ -22,7 +22,11 @@
         [% 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>
diff --git a/Open-ILS/src/templates/opac/results.tt2 b/Open-ILS/src/templates/opac/results.tt2
index 4c3d6f6..53b8dfd 100644
--- a/Open-ILS/src/templates/opac/results.tt2
+++ b/Open-ILS/src/templates/opac/results.tt2
@@ -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 %]
diff --git a/Open-ILS/src/templates/opac/temp_warn.tt2 b/Open-ILS/src/templates/opac/temp_warn.tt2
index 8aa978d..8fdea97 100644
--- a/Open-ILS/src/templates/opac/temp_warn.tt2
+++ b/Open-ILS/src/templates/opac/temp_warn.tt2
@@ -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
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
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
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
index 0000000..6116b39
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/opac/record_selectors.js
@@ -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();
+        });
+    }
+
+})();
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index 63a177e..b2435e9 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -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
index 0000000..cba31b6
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/OPAC/Batch_Actions.adoc
@@ -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.

-----------------------------------------------------------------------

Summary of changes:
 .../src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm    |   19 ++-
 .../lib/OpenILS/WWW/EGCatLoader/Account.pm         |   69 ++++-
 .../lib/OpenILS/WWW/EGCatLoader/Container.pm       |  237 +++++++++++++++--
 .../perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm |   66 ++++-
 Open-ILS/src/perlmods/lib/OpenILS/WWW/EGWeb.pm     |   13 +
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    2 +-
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |    6 +-
 .../Pg/upgrade/1118.data.bre_format_title_fix.sql  |   90 ++++++
 Open-ILS/src/templates/opac/advanced.tt2           |    1 +
 Open-ILS/src/templates/opac/browse.tt2             |    1 +
 Open-ILS/src/templates/opac/css/style.css.tt2      |   61 ++++-
 Open-ILS/src/templates/opac/mylist.tt2             |    5 +-
 Open-ILS/src/templates/opac/mylist/clear.tt2       |   21 ++
 Open-ILS/src/templates/opac/mylist/email.tt2       |   29 ++
 Open-ILS/src/templates/opac/mylist/print.tt2       |   29 ++
 Open-ILS/src/templates/opac/myopac/lists.tt2       |   26 ++-
 Open-ILS/src/templates/opac/parts/anon_list.tt2    |   21 +-
 .../src/templates/opac/parts/bookbag_actions.tt2   |    3 -
 Open-ILS/src/templates/opac/parts/cart.tt2         |   30 ++
 Open-ILS/src/templates/opac/parts/config.tt2       |    5 +
 Open-ILS/src/templates/opac/parts/css/colors.tt2   |    1 +
 Open-ILS/src/templates/opac/parts/header.tt2       |    8 +-
 Open-ILS/src/templates/opac/parts/js.tt2           |   10 +
 Open-ILS/src/templates/opac/parts/place_hold.tt2   |    9 +-
 .../src/templates/opac/parts/record/summary.tt2    |   43 ++-
 Open-ILS/src/templates/opac/parts/result/table.tt2 |   69 ++++--
 Open-ILS/src/templates/opac/parts/searchbar.tt2    |    1 +
 Open-ILS/src/templates/opac/parts/topnav.tt2       |    2 +-
 Open-ILS/src/templates/opac/record/email.tt2       |    4 +
 Open-ILS/src/templates/opac/record/print.tt2       |    4 +
 Open-ILS/src/templates/opac/results.tt2            |    4 +-
 Open-ILS/src/templates/opac/temp_warn.tt2          |    6 +-
 Open-ILS/web/images/add-to-cart.png                |  Bin 0 -> 617 bytes
 Open-ILS/web/images/cart-md.png                    |  Bin 0 -> 7541 bytes
 Open-ILS/web/images/cart-sm.png                    |  Bin 0 -> 762 bytes
 .../web/js/ui/default/opac/record_selectors.js     |  284 ++++++++++++++++++++
 .../web/js/ui/default/staff/cat/catalog/app.js     |   49 +++-
 docs/RELEASE_NOTES_NEXT/OPAC/Batch_Actions.adoc    |   76 ++++++
 38 files changed, 1182 insertions(+), 122 deletions(-)
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1118.data.bre_format_title_fix.sql
 create mode 100644 Open-ILS/src/templates/opac/mylist/clear.tt2
 create mode 100644 Open-ILS/src/templates/opac/mylist/email.tt2
 create mode 100644 Open-ILS/src/templates/opac/mylist/print.tt2
 create mode 100644 Open-ILS/src/templates/opac/parts/cart.tt2
 create mode 100644 Open-ILS/web/images/add-to-cart.png
 create mode 100644 Open-ILS/web/images/cart-md.png
 create mode 100644 Open-ILS/web/images/cart-sm.png
 create mode 100644 Open-ILS/web/js/ui/default/opac/record_selectors.js
 create mode 100644 docs/RELEASE_NOTES_NEXT/OPAC/Batch_Actions.adoc


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list