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

Evergreen Git git at git.evergreen-ils.org
Fri Sep 1 16:07:40 EDT 2017


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  a3f854db8c407ccec37ac5728779544c4cd8feca (commit)
       via  0cbe5b5a064d99c726d4e6137b28a321bbc72381 (commit)
       via  e6b541bea9302ec004e28c88cda005efa0380527 (commit)
       via  c8a0199542897b0cbc46bf7499d3c7975014556c (commit)
       via  9710a3bd15bb9d61927b310546d76a590d2fc3e0 (commit)
       via  1004dda5626abf095b966fb7bc2ac317bfe71505 (commit)
       via  210317af1ee75577ae5b355d568288d637ce456d (commit)
       via  07d942af5cff7a861b6b1f1809cae5a8374e430c (commit)
       via  b4b649509cf28581b28a13071707f6555301b066 (commit)
       via  904875cd9cfceeb8681c4d2befa878b61ed65a06 (commit)
       via  8a9ba80dd1533138773762a5d3ee4c030b2c92bc (commit)
       via  2efd93f58bee719e8d8f85a25dd45b2b6e552a6a (commit)
       via  4f9df515878d90c031c75c9d4fb5ebf486a87d4c (commit)
       via  34b67a5e983b92c9deeb1da5cfabb14f5bf7557c (commit)
       via  1f8471c342ee4e3f2c800257bb12dd208ea99480 (commit)
       via  2bafbbd2aee289e5bb4fc810aeb369cc9ffe00a6 (commit)
       via  e80fd44bf73f770758bb8706b474ddb401e043c1 (commit)
       via  2cc4477c4c657ed9a29eb57c58b5b995a35ed59a (commit)
       via  da17a15eb931f82a9b38f2d579c0fed1215a0962 (commit)
       via  61f4a26d0396fa6ff3fa0293c5f439cdb8d403d7 (commit)
       via  bddaee3a63eb5a3e3d1345899dbc5f9c30d52241 (commit)
       via  346994bd5daaa3c6060431dd41b335f022372b39 (commit)
      from  b5e13c8060288681fb6760da97d65928253fa957 (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 a3f854db8c407ccec37ac5728779544c4cd8feca
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Fri Sep 1 10:36:26 2017 -0700

    LP#1673870: release notes for ebook transactions
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/docs/opac/ebook_transactions.adoc b/docs/opac/ebook_transactions.adoc
new file mode 100644
index 0000000..020e60a
--- /dev/null
+++ b/docs/opac/ebook_transactions.adoc
@@ -0,0 +1,36 @@
+Ebook transactions
+------------------
+
+This release adds support for checking out and placing holds on
+OverDrive and OneClickdigital ebook titles from within the public
+catalog.  (This extends the partial integration with third-party APIs
+that was included in Evergreen 2.12.)
+
+This is an experimental feature.  It is not recommended for production
+use without careful testing.
+
+When ebook integration is enabled, a "Check Out E-Item" link will be
+displayed when viewing an ebook title from a supported vendor in the
+catalog.  Clicking on the link allows the user to check out and download
+that title from the vendor directly within the catalog.
+
+If no copies are available for the title, a "Place Hold on E-Item" link
+is displayed instead, and the user may place a hold on the title.  (Note
+that some vendors require the user's account to have an email address
+before permitting a hold.)
+
+My Account includes the ability to view current ebook checkouts and
+holds, download already-checked-out titles, and cancel holds.
+
+For API integration to work, you need to request API access from the
+vendor, and the vendor must have a way of authenticating your patrons.
+Your Evergreen system also needs to be configured for ebook API
+integration, following the instructions in the
+https://evergreen-ils.org/documentation/release/RELEASE_NOTES_2_12.html#_public_catalog[Evergreen
+2.12 release notes].
+
+This feature assumes that you are importing MARC records supplied by the
+vendor into your Evergreen system, using Vandelay or some other MARC
+import method.  This feature does not search the vendor's online
+collections or automatically import vendor records into your system; it
+merely augments records that are already in Evergreen.

commit 0cbe5b5a064d99c726d4e6137b28a321bbc72381
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Thu Aug 31 12:59:40 2017 -0400

    LP#1673870: avoid e-session cookie weirdness
    
    This patch is a bit of hackery to work around an intermittent
    (but frequent) situation where the vendor session cookie got
    cleared right before invoking the cancel hold method, leading
    to its failure.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
index 970066a..abc46e5 100644
--- a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
@@ -170,20 +170,24 @@ function updateHoldView() {
                 hold_status = h.queue_position + ' / ' + h.queue_size;
             }
             h.doCancelHold = function() {
-                var ebook = new Ebook(this.vendor, this.title_id);
-                ebook.cancelHold(authtoken, patron_id, function(resp) {
-                    if (resp.error_msg) {
-                        console.log('Cancel hold failed: ' + resp.error_msg);
-                        dojo.removeClass('ebook_cancel_hold_failed', "hidden");
-                    } else {
-                        console.log('Cancel hold succeeded!');
-                        dojo.destroy("hold-" + ebook.id);
-                        dojo.removeClass('ebook_cancel_hold_succeeded', "hidden");
-                        // Updating the transaction cache to remove the canceled hold
-                        // is inconvenient, so we skip cleanupAfterAction() and merely
-                        // clear transaction cache to force a refresh on next page load.
-                        dojo.cookie('ebook_xact_cache', '', {path: '/', expires: '-1h'});
-                    }
+                var vendor = this.vendor;
+                var title_id = this.title_id;
+                checkSession(vendor, function() {
+                    var ebook = new Ebook(vendor, title_id);
+                    ebook.cancelHold(authtoken, patron_id, function(resp) {
+                        if (resp.error_msg) {
+                            console.log('Cancel hold failed: ' + resp.error_msg);
+                            dojo.removeClass('ebook_cancel_hold_failed', "hidden");
+                        } else {
+                            console.log('Cancel hold succeeded!');
+                            dojo.destroy("hold-" + ebook.id);
+                            dojo.removeClass('ebook_cancel_hold_succeeded', "hidden");
+                            // Updating the transaction cache to remove the canceled hold
+                            // is inconvenient, so we skip cleanupAfterAction() and merely
+                            // clear transaction cache to force a refresh on next page load.
+                            dojo.cookie('ebook_xact_cache', '', {path: '/', expires: '-1h'});
+                        }
+                    });
                 });
             };
             var tr = dojo.create("tr", { id: "hold-" + h.title_id }, dojo.byId('ebook_holds_main_table_body'));

commit e6b541bea9302ec004e28c88cda005efa0380527
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Thu Aug 31 12:17:12 2017 -0400

    LP#1673870: some tidying up of JavaScript
    
    - test for ebook_spinner div before attempting to set a class
    - eject a couple Perlisms
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
index 335cecf..970066a 100644
--- a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
@@ -67,7 +67,7 @@ function addTransactionsToPage() {
     if (active_ebook && typeof active_ebook.vendor !== 'undefined') {
         active_ebook.ses = active_ebook.ses || dojo.cookie(active_ebook.vendor);
     }
-    dojo.addClass('ebook_spinner', "hidden");
+    if (dojo.byId('ebook_spinner')) dojo.addClass('ebook_spinner', "hidden");
     if (myopac_page) {
         console.log('updating page with cached transaction details, if applicable');
         if (myopac_page === 'ebook_circs')
@@ -173,7 +173,7 @@ function updateHoldView() {
                 var ebook = new Ebook(this.vendor, this.title_id);
                 ebook.cancelHold(authtoken, patron_id, function(resp) {
                     if (resp.error_msg) {
-                        console.log('Cancel hold failed: ' . resp.error_msg);
+                        console.log('Cancel hold failed: ' + resp.error_msg);
                         dojo.removeClass('ebook_cancel_hold_failed', "hidden");
                     } else {
                         console.log('Cancel hold succeeded!');
@@ -339,7 +339,7 @@ function doCheckout() {
 function doPlaceHold() {
     active_ebook.placeHold(authtoken, patron_id, function(resp) {
         if (resp.error_msg) {
-            console.log('Place hold failed: ' . resp.error_msg);
+            console.log('Place hold failed: ' + resp.error_msg);
             dojo.removeClass('ebook_place_hold_failed', "hidden");
         } else {
             console.log('Place hold succeeded!');

commit c8a0199542897b0cbc46bf7499d3c7975014556c
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Thu Aug 31 11:49:08 2017 -0400

    LP#1673870: add support for cache-busting
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/templates/opac/myopac/ebook_checkout.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_checkout.tt2
index b48835d..789ad13 100644
--- a/Open-ILS/src/templates/opac/myopac/ebook_checkout.tt2
+++ b/Open-ILS/src/templates/opac/myopac/ebook_checkout.tt2
@@ -13,7 +13,7 @@
     <div class="clear-both"></div>
     <div id="ebook_checkout_failed" class="warning_box hidden">[% l('E-item could not be checked out.') %]</div>
     <div id="ebook_checkout_succeeded" class="success hidden">[% l('E-item successfully checked out.') %]</div>
-    <div id="ebook_spinner"><img src="/opac/images/progressbar_green.gif" alt="[% l("Loading...") %]"/></div>
+    <div id="ebook_spinner"><img src="[% ctx.media_prefix %]/opac/images/progressbar_green.gif[% ctx.cache_key %]" alt="[% l("Loading...") %]"/></div>
     <div id='ebook_circs_main' class="hidden">
         <table id="ebook_circs_main_table"
             title="[% l('Check Out E-Item') %]"
diff --git a/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2
index 0e97f8e..be1912c 100644
--- a/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2
+++ b/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2
@@ -24,7 +24,7 @@
     </div>
     <div class="clear-both"></div>
     <div id="no_ebook_circs" class="warning_box hidden">[% l('You have no e-items checked out.') %]</div>
-    <div id="ebook_spinner"><img src="/opac/images/progressbar_green.gif" alt="[% l("Loading...") %]"/></div>
+    <div id="ebook_spinner"><img src="[% ctx.media_prefix %]/opac/images/progressbar_green.gif[% ctx.cache_key %]" alt="[% l("Loading...") %]"/></div>
     <div id='ebook_circs_main' class="hidden">
         <table id="ebook_circs_main_table"
             title="[% l('E-Items Currently Checked Out') %]"
diff --git a/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2
index 7914639..f862ed6 100644
--- a/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2
+++ b/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2
@@ -33,7 +33,7 @@
     <div id="no_ebook_holds" class="warning_box hidden">[% l('You have no e-item holds.') %]</div>
     <div id="ebook_cancel_hold_failed" class="warning_box hidden">[% l('Hold could not be canceled.') %]</div>
     <div id="ebook_cancel_hold_succeeded" class="success hidden">[% l('Your hold has been canceled.') %]</div>
-    <div id="ebook_spinner"><img src="/opac/images/progressbar_green.gif" alt="[% l("Loading...") %]"/></div>
+    <div id="ebook_spinner"><img src="[% ctx.media_prefix %]/opac/images/progressbar_green.gif[% ctx.cache_key %]" alt="[% l("Loading...") %]"/></div>
     <div id='ebook_holds_main' class="hidden">
         <table id="ebook_holds_main_table"
             title="[% l('E-Items on Hold') %]"
diff --git a/Open-ILS/src/templates/opac/myopac/ebook_holds_ready.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_holds_ready.tt2
index 807357c..55806d5 100644
--- a/Open-ILS/src/templates/opac/myopac/ebook_holds_ready.tt2
+++ b/Open-ILS/src/templates/opac/myopac/ebook_holds_ready.tt2
@@ -31,7 +31,7 @@
     </div>
     <div class="clear-both"></div>
     <div id="no_ebook_holds" class="warning_box hidden">[% l('You have no e-item holds ready to be checked out.') %]</div>
-    <div id="ebook_spinner"><img src="/opac/images/progressbar_green.gif" alt="[% l("Loading...") %]"/></div>
+    <div id="ebook_spinner"><img src="[% ctx.media_prefix %]/opac/images/progressbar_green.gif[% ctx.cache_key %]" alt="[% l("Loading...") %]"/></div>
     <div id='ebook_holds_main' class="hidden">
         <table id="ebook_holds_main_table"
             title="[% l('E-Items Ready for Checkout') %]"
diff --git a/Open-ILS/src/templates/opac/myopac/ebook_place_hold.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_place_hold.tt2
index 7e7d5d9..cefea21 100644
--- a/Open-ILS/src/templates/opac/myopac/ebook_place_hold.tt2
+++ b/Open-ILS/src/templates/opac/myopac/ebook_place_hold.tt2
@@ -14,7 +14,7 @@
     <div class="clear-both"></div>
     <div id="ebook_place_hold_failed" class="warning_box hidden">[% l('Hold could not be placed.') %]</div>
     <div id="ebook_place_hold_succeeded" class="success hidden">[% l('E-item is now on hold.') %]</div>
-    <div id="ebook_spinner"><img src="/opac/images/progressbar_green.gif" alt="[% l("Loading...") %]"/></div>
+    <div id="ebook_spinner"><img src="[% ctx.media_prefix %]/opac/images/progressbar_green.gif[% ctx.cache_key %]" alt="[% l("Loading...") %]"/></div>
     <div id='ebook_holds_main' class="hidden">
         <table id="ebook_holds_main_table"
             title="[% l('Place Hold on E-Item') %]"
diff --git a/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2 b/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2
index bcff091..d5ba0f4 100644
--- a/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2
+++ b/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2
@@ -90,6 +90,6 @@ dojo.addOnLoad(function() {
 [%- END %]
 
 [%- IF (ctx.page == 'rresult' OR ctx.page == 'record') %]
-<script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/opac/ebook_api/avail.js"></script>
+<script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/opac/ebook_api/avail.js[% ctx.cache_key %]"></script>
 [%- END %]
 
diff --git a/Open-ILS/src/templates/opac/parts/record/summary.tt2 b/Open-ILS/src/templates/opac/parts/record/summary.tt2
index f893cf6..d26810d 100644
--- a/Open-ILS/src/templates/opac/parts/record/summary.tt2
+++ b/Open-ILS/src/templates/opac/parts/record/summary.tt2
@@ -84,14 +84,14 @@
             <div id="[%- ctx.bre_id -%]_ebook_checkout" class="rdetail_aux_utils ebook_action hidden">
                 <a href="[% mkurl(ctx.opac_root _ '/myopac/ebook_checkout',
                     {title => args.ebook.ebook_id, vendor => args.ebook.vendor, action => 'checkout'}, stop_parms) %]"
-                class="no-dec" rel="nofollow" vocab=""><img src="[% ctx.media_prefix %]/images/green_check.png"
+                class="no-dec" rel="nofollow" vocab=""><img src="[% ctx.media_prefix %]/images/green_check.png[% ctx.cache_key %]"
                     [% img_alt(l('Check Out [_1]', attrs.title)) %]/>
                 <span class="place_hold">[% l('Check Out E-Item') %]</span></a>
             </div>
             <div id="[%- ctx.bre_id -%]_ebook_place_hold" class="rdetail_aux_utils ebook_action hidden">
                 <a href="[% mkurl(ctx.opac_root _ '/myopac/ebook_place_hold',
                     {title => args.ebook.ebook_id, vendor => args.ebook.vendor, action => 'place_hold'}, stop_parms) %]"
-                class="no-dec" rel="nofollow" vocab=""><img src="[% ctx.media_prefix %]/images/green_check.png"
+                class="no-dec" rel="nofollow" vocab=""><img src="[% ctx.media_prefix %]/images/green_check.png[% ctx.cache_key %]"
                     [% img_alt(l('Place Hold on [_1]', attrs.title)) %]/>
                 <span class="place_hold">[% l('Place Hold on E-Item') %]</span></a>
             </div>
diff --git a/Open-ILS/src/templates/opac/parts/result/table.tt2 b/Open-ILS/src/templates/opac/parts/result/table.tt2
index 35dc615..ae77275 100644
--- a/Open-ILS/src/templates/opac/parts/result/table.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/table.tt2
@@ -404,7 +404,7 @@ END;
                                                                 ['query','tag','subfield','term','_special','sort','page']) %]"
                                                                 [% html_text_attr('title', l('Check Out [_1]', attrs.title)) %]
                                                                     class="no-dec" rel="nofollow" vocab=""><img
-                                                                src="[% ctx.media_prefix %]/images/green_check.png"
+                                                                src="[% ctx.media_prefix %]/images/green_check.png[% ctx.cache_key %]"
                                                                 alt=""/><span class="result_place_hold">[% l('Check Out E-Item') %]</span></a>
                                                         </div>
                                                         <div id="[%- rec.id -%]_ebook_place_hold" class="results_aux_utils result_util ebook_action hidden">
@@ -413,7 +413,7 @@ END;
                                                                 ['query','tag','subfield','term','_special','sort','page']) %]"
                                                                 [% html_text_attr('title', l('Place Hold on [_1]', attrs.title)) %]
                                                                     class="no-dec" rel="nofollow" vocab=""><img
-                                                                src="[% ctx.media_prefix %]/images/green_check.png"
+                                                                src="[% ctx.media_prefix %]/images/green_check.png[% ctx.cache_key %]"
                                                                 alt=""/><span class="result_place_hold">[% l('Place Hold on E-Item') %]</span></a>
                                                         </div>
                                                         [%- END -%]

commit 9710a3bd15bb9d61927b310546d76a590d2fc3e0
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Wed Aug 16 11:30:54 2017 -0700

    LP#1673870: fix OneClickdigital handler to reflect how their API actually works, documentation notwithstanding
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OneClickdigital.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OneClickdigital.pm
index be42463..602a7dc 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OneClickdigital.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OneClickdigital.pm
@@ -128,7 +128,7 @@ sub do_patron_auth {
 }
 
 # get basic metadata for an item (title, author, cover image if any)
-# GET http://api.oneclickdigital.us/v1/libraries/{libraryId}/titles/{isbn}
+# GET http://api.oneclickdigital.us/v1/libraries/{libraryId}/media/{isbn}
 sub get_title_info {
     my ($self, $isbn) = @_;
     my $base_uri = $self->{base_uri};
@@ -136,13 +136,13 @@ sub get_title_info {
     my $session_id = $self->{session_id};
     my $req = {
         method => 'GET',
-        uri    => "$base_uri/libraries/$library_id/titles/$isbn"
+        uri    => "$base_uri/libraries/$library_id/media/$isbn"
     };
     my $res = $self->request($req, $session_id);
     if (defined ($res)) {
         return {
             title  => $res->{content}->{title},
-            author => $res->{content}->{authors}[0]{text}
+            author => $res->{content}->{authors}
         };
     } else {
         $logger->error("EbookAPI: could not retrieve OneClickdigital title details for ISBN $isbn");
@@ -189,7 +189,8 @@ sub checkout {
     my $session_id = $self->{session_id};
     my $req = {
         method => 'POST',
-        uri    => "$base_uri/libraries/$library_id/patrons/$patron_id/checkouts/$isbn"
+        uri    => "$base_uri/libraries/$library_id/patrons/$patron_id/checkouts/$isbn",
+        headers => { "Content-Length" => "0" }
     };
     my $res = $self->request($req, $session_id);
 
@@ -221,7 +222,8 @@ sub renew {
     my $session_id = $self->{session_id};
     my $req = {
         method => 'PUT',
-        uri    => "$base_uri/libraries/$library_id/patrons/$patron_id/checkouts/$isbn"
+        uri    => "$base_uri/libraries/$library_id/patrons/$patron_id/checkouts/$isbn",
+        headers => { "Content-Length" => "0" }
     };
     my $res = $self->request($req, $session_id);
 
@@ -273,10 +275,10 @@ sub get_patron_checkouts {
         $logger->info("EbookAPI: received response for OneClickdigital checkouts: " . Dumper $res);
         foreach my $checkout (@{$res->{content}}) {
             push @$checkouts, {
-                xact_id => $checkout->{transactionID},
+                xact_id => $checkout->{transactionId},
                 title_id => $checkout->{isbn},
                 due_date => $checkout->{expiration},
-                download_url => $checkout->{downloadURL},
+                download_url => $checkout->{downloadUrl},
                 title => $checkout->{title},
                 author => $checkout->{authors}
             };
@@ -307,7 +309,7 @@ sub get_patron_holds {
         $logger->info("EbookAPI: received response for OneClickdigital holds: " . Dumper $res);
         foreach my $hold (@{$res->{content}}) {
             push @$holds, {
-                xact_id => $hold->{transactionID},
+                xact_id => $hold->{transactionId},
                 title_id => $hold->{isbn},
                 expire_date => $hold->{expiration},
                 title => $hold->{title},

commit 1004dda5626abf095b966fb7bc2ac317bfe71505
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Mon Aug 14 14:03:59 2017 -0700

    LP#1673870: display ebook availability when detailed holdings info is unavailable
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/avail.js b/Open-ILS/web/js/ui/default/opac/ebook_api/avail.js
index b65bf87..b822602 100644
--- a/Open-ILS/web/js/ui/default/opac/ebook_api/avail.js
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/avail.js
@@ -19,8 +19,10 @@ dojo.addOnLoad(function() {
                             var avail = holdings.available;
                             if (avail == 1) {
                                 node.innerHTML = 'This title is available online.';
+                                dojo.removeClass(ebook.rec_id + '_ebook_checkout', "hidden");
                             } else if (avail == 0) {
                                 node.innerHTML = 'This title is not currently available.';
+                                dojo.removeClass(ebook.rec_id + '_ebook_place_hold', "hidden");
                             } else {
                                 console.log(ebook.id + ' has bad availability: ' + avail);
                             }

commit 210317af1ee75577ae5b355d568288d637ce456d
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Wed Jul 12 12:35:43 2017 -0700

    LP#1673870: indicate suspended holds for OverDrive
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm
index b6997d1..6eca31e 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm
@@ -710,6 +710,7 @@ sub get_patron_holds {
                 queue_size => $hold->{numberOfHolds},
                 # TODO: special handling for ready-to-checkout holds
                 is_ready => ( $hold->{actions}->{checkout} ) ? 1 : 0,
+                is_frozen => ( $hold->{holdSuspension} ) ? 1 : 0,
                 create_date => $hold->{holdPlacedDate},
                 expire_date => ( $hold->{holdExpires} ) ? $hold->{holdExpires} : '-',
                 title => $title_info->{title},

commit 07d942af5cff7a861b6b1f1809cae5a8374e430c
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Tue Jul 11 15:45:48 2017 -0700

    LP#1673870: move ebook availability JS to standard location
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2 b/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2
index 30ee0f5..bcff091 100644
--- a/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2
+++ b/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2
@@ -89,3 +89,7 @@ dojo.addOnLoad(function() {
 <script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/opac/ebook_api/loggedin.js[% ctx.cache_key %]"></script>
 [%- END %]
 
+[%- IF (ctx.page == 'rresult' OR ctx.page == 'record') %]
+<script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/opac/ebook_api/avail.js"></script>
+[%- END %]
+
diff --git a/Open-ILS/src/templates/opac/parts/js.tt2 b/Open-ILS/src/templates/opac/parts/js.tt2
index 79db0ed..01fb9f9 100644
--- a/Open-ILS/src/templates/opac/parts/js.tt2
+++ b/Open-ILS/src/templates/opac/parts/js.tt2
@@ -123,7 +123,6 @@
 
 [% IF ebook_api.enabled == 'true' %]
     [% INCLUDE "opac/parts/ebook_api/base_js.tt2" %]
-    [% INCLUDE "opac/parts/ebook_api/avail_js.tt2" IF (ctx.page == 'rresult' OR ctx.page == 'record') %]
     [% INCLUDE "opac/parts/ebook_api/login_js.tt2" IF (ctx.page == 'login') %]
 [% END %]
 
diff --git a/Open-ILS/src/templates/opac/parts/ebook_api/avail_js.tt2 b/Open-ILS/web/js/ui/default/opac/ebook_api/avail.js
similarity index 98%
rename from Open-ILS/src/templates/opac/parts/ebook_api/avail_js.tt2
rename to Open-ILS/web/js/ui/default/opac/ebook_api/avail.js
index 216d2da..b65bf87 100644
--- a/Open-ILS/src/templates/opac/parts/ebook_api/avail_js.tt2
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/avail.js
@@ -1,4 +1,3 @@
-<script type="text/javascript">
 dojo.addOnLoad(function() {
 
     // detect ebooks on current page for each vendor
@@ -51,4 +50,3 @@ dojo.addOnLoad(function() {
     });
 
 });
-</script>

commit b4b649509cf28581b28a13071707f6555301b066
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Fri Aug 18 13:35:09 2017 -0700

    LP#1673870: show spinner in My Account while loading from ebook API
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/templates/opac/myopac/ebook_checkout.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_checkout.tt2
index 2b38da3..b48835d 100644
--- a/Open-ILS/src/templates/opac/myopac/ebook_checkout.tt2
+++ b/Open-ILS/src/templates/opac/myopac/ebook_checkout.tt2
@@ -13,6 +13,7 @@
     <div class="clear-both"></div>
     <div id="ebook_checkout_failed" class="warning_box hidden">[% l('E-item could not be checked out.') %]</div>
     <div id="ebook_checkout_succeeded" class="success hidden">[% l('E-item successfully checked out.') %]</div>
+    <div id="ebook_spinner"><img src="/opac/images/progressbar_green.gif" alt="[% l("Loading...") %]"/></div>
     <div id='ebook_circs_main' class="hidden">
         <table id="ebook_circs_main_table"
             title="[% l('Check Out E-Item') %]"
diff --git a/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2
index 1bee6e7..0e97f8e 100644
--- a/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2
+++ b/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2
@@ -24,6 +24,7 @@
     </div>
     <div class="clear-both"></div>
     <div id="no_ebook_circs" class="warning_box hidden">[% l('You have no e-items checked out.') %]</div>
+    <div id="ebook_spinner"><img src="/opac/images/progressbar_green.gif" alt="[% l("Loading...") %]"/></div>
     <div id='ebook_circs_main' class="hidden">
         <table id="ebook_circs_main_table"
             title="[% l('E-Items Currently Checked Out') %]"
diff --git a/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2
index 1a77a71..7914639 100644
--- a/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2
+++ b/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2
@@ -33,6 +33,7 @@
     <div id="no_ebook_holds" class="warning_box hidden">[% l('You have no e-item holds.') %]</div>
     <div id="ebook_cancel_hold_failed" class="warning_box hidden">[% l('Hold could not be canceled.') %]</div>
     <div id="ebook_cancel_hold_succeeded" class="success hidden">[% l('Your hold has been canceled.') %]</div>
+    <div id="ebook_spinner"><img src="/opac/images/progressbar_green.gif" alt="[% l("Loading...") %]"/></div>
     <div id='ebook_holds_main' class="hidden">
         <table id="ebook_holds_main_table"
             title="[% l('E-Items on Hold') %]"
diff --git a/Open-ILS/src/templates/opac/myopac/ebook_holds_ready.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_holds_ready.tt2
index 006c986..807357c 100644
--- a/Open-ILS/src/templates/opac/myopac/ebook_holds_ready.tt2
+++ b/Open-ILS/src/templates/opac/myopac/ebook_holds_ready.tt2
@@ -31,6 +31,7 @@
     </div>
     <div class="clear-both"></div>
     <div id="no_ebook_holds" class="warning_box hidden">[% l('You have no e-item holds ready to be checked out.') %]</div>
+    <div id="ebook_spinner"><img src="/opac/images/progressbar_green.gif" alt="[% l("Loading...") %]"/></div>
     <div id='ebook_holds_main' class="hidden">
         <table id="ebook_holds_main_table"
             title="[% l('E-Items Ready for Checkout') %]"
diff --git a/Open-ILS/src/templates/opac/myopac/ebook_place_hold.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_place_hold.tt2
index 6cdbc54..7e7d5d9 100644
--- a/Open-ILS/src/templates/opac/myopac/ebook_place_hold.tt2
+++ b/Open-ILS/src/templates/opac/myopac/ebook_place_hold.tt2
@@ -14,6 +14,7 @@
     <div class="clear-both"></div>
     <div id="ebook_place_hold_failed" class="warning_box hidden">[% l('Hold could not be placed.') %]</div>
     <div id="ebook_place_hold_succeeded" class="success hidden">[% l('E-item is now on hold.') %]</div>
+    <div id="ebook_spinner"><img src="/opac/images/progressbar_green.gif" alt="[% l("Loading...") %]"/></div>
     <div id='ebook_holds_main' class="hidden">
         <table id="ebook_holds_main_table"
             title="[% l('Place Hold on E-Item') %]"
diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
index 56846bc..335cecf 100644
--- a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
@@ -67,6 +67,7 @@ function addTransactionsToPage() {
     if (active_ebook && typeof active_ebook.vendor !== 'undefined') {
         active_ebook.ses = active_ebook.ses || dojo.cookie(active_ebook.vendor);
     }
+    dojo.addClass('ebook_spinner', "hidden");
     if (myopac_page) {
         console.log('updating page with cached transaction details, if applicable');
         if (myopac_page === 'ebook_circs')

commit 904875cd9cfceeb8681c4d2befa878b61ed65a06
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Tue Jul 4 16:20:11 2017 -0700

    LP#1673870: Handle OverDrive ebook checkout and download
    
    The workflow for checking out and downloading a title via the OverDrive
    API is relatively complex:
    
    1. Check out a title.
    
    2. Lock in a specific format for the checked-out title.  Once you lock
    in a format, you can only download the title in that format -- except
    that the browser-based OverDrive Read and OverDrive Listen formats are
    always available (if supported for that title), even if you've locked in
    another format.
    
    3. Request a link for downloading the title in the specified format.
    Download links are dynamically generated and only work for 60 seconds
    from the time of your request.
    
    To simplify the process, we require the user to lock in a format during
    checkout.  Then, when the user clicks the Download button, we request a
    download link; OverDrive responds with a URL, and we immediately
    redirect the current browser tab/window to that URL.
    
    A new API call, open-ils.ebook_api.title.get_download_link, has been
    added for requesting the download link.  Since API calls are not
    vendor-specific, we also add support for the new method in the test
    module, complete with unit test.
    
    Supplementary fixes:
    
    - show spinner in My Account while loading from ebook API
    - ensure session ID is available to ebook object during transactions
    - fix display of ebook formats
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm
index d6961a8..bc968b5 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm
@@ -481,7 +481,7 @@ __PACKAGE__->register_method(
 # - barcode: patron barcode
 #
 sub do_xact {
-    my ($self, $conn, $auth, $session_id, $title_id, $barcode, $email) = @_;
+    my ($self, $conn, $auth, $session_id, $title_id, $barcode, $param) = @_;
 
     my $action;
     if ($self->api_name =~ /checkout/) {
@@ -509,9 +509,12 @@ sub do_xact {
 
     # handler method constructs and submits request (and handles any external authentication)
     my $res;
-    # place_hold has email as optional additional param
-    if ($action eq 'place_hold') {
-        $res = $handler->place_hold($title_id, $user_token, $email);
+    if ($action eq 'checkout') {
+        # checkout has format as optional additional param
+        $res = $handler->checkout($title_id, $user_token, $param);
+    } elsif ($action eq 'place_hold') {
+        # place_hold has email as optional additional param
+        $res = $handler->place_hold($title_id, $user_token, $param);
     } else {
         $res = $handler->$action($title_id, $user_token);
     }
@@ -845,4 +848,40 @@ __PACKAGE__->register_method(
     }
 );
 
+sub get_download_link {
+    my ($self, $conn, $auth, $session_id, $request_link) = @_;
+    my $handler = new_handler($session_id);
+    return $handler->do_get_download_link($request_link);
+}
+__PACKAGE__->register_method(
+    method => 'get_download_link',
+    api_name => 'open-ils.ebook_api.title.get_download_link',
+    api_level => 1,
+    argc => 3,
+    signature => {
+        desc => "Get download link for an OverDrive title that has been checked out",
+        params => [
+            {
+                name => 'authtoken',
+                desc => 'Authentication token',
+                type => 'string'
+            },
+            {
+                name => 'session_id',
+                desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
+                type => 'string'
+            },
+            {
+                name => 'request_link',
+                desc => 'The URL used to request a download link',
+                type => 'string'
+            }
+        ],
+        return => {
+            desc => 'Success: { url => "http://example.com/download-link" } / Failure: { error_msg => "Download link request failed." }',
+            type => 'hashref'
+        }
+    }
+);
+
 1;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm
index 9549fa4..b6997d1 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm
@@ -376,10 +376,13 @@ sub get_title_info {
     };
     if (my $res = $self->handle_http_request($req, $self->{session_id})) {
         if ($res->{content}->{title}) {
-            return {
+            my $info = {
                 title  => $res->{content}->{title},
                 author => $res->{content}->{creators}[0]{name}
             };
+            # Append format information (useful for checkouts).
+            $info->{formats} = $self->get_formats($title_id);
+            return $info;
         } else {
             $logger->error("EbookAPI: OverDrive metadata lookup failed for $title_id");
         }
@@ -446,6 +449,20 @@ sub do_holdings_lookup {
     }
 
     # request available formats
+    $holdings->{formats} = $self->get_formats($title_id);
+
+    return $holdings;
+}
+
+# Returns a list of available formats for a given title.
+sub get_formats {
+    my ($self, $title_id) = @_;
+    $self->do_client_auth() if (!$self->{bearer_token});
+    $self->get_library_info() if (!$self->{collection_token});
+    my $collection_token = $self->{collection_token};
+
+    my $formats = [];
+
     my $format_req = {
         method  => 'GET',
         uri     => $self->{discovery_base_uri} . "/collections/$collection_token/products/$title_id/metadata"
@@ -453,7 +470,7 @@ sub do_holdings_lookup {
     if (my $format_res = $self->handle_http_request($format_req, $self->{session_id})) {
         if ($format_res->{content}->{formats}) {
             foreach my $f (@{$format_res->{content}->{formats}}) {
-                push @{$holdings->{formats}}, $f->{name};
+                push @$formats, { id => $f->{id}, name => $f->{name} };
             }
         } else {
             $logger->info("EbookAPI: OverDrive holdings format request for title $title_id contained no format information");
@@ -462,7 +479,7 @@ sub do_holdings_lookup {
         $logger->error("EbookAPI: failed to retrieve OverDrive holdings formats for title $title_id");
     }
 
-    return $holdings;
+    return $formats;
 }
 
 # POST https://patron.api.overdrive.com/v1/patrons/me/checkouts
@@ -500,8 +517,17 @@ sub do_holdings_lookup {
 #     ],
 #     ...
 # }
+#
+# Our return value looks like this:
+# {
+#     due_date => "10/14/2013 10:56:00 AM",
+#     formats => [
+#         "ebook-overdrive" => "https://patron.api.overdrive.com/v1/patrons/me/checkouts/76C1B7D0-17F4-4C05-8397-C66C17411584/formats/ebook-overdrive/downloadlink?errorpageurl={errorpageurl}&odreadauthurl={odreadauthurl}",
+#         ...
+#     ]
+# }
 sub checkout {
-    my ($self, $title_id, $patron_token) = @_;
+    my ($self, $title_id, $patron_token, $format) = @_;
     my $request_content = {
         fields => [
             {
@@ -510,6 +536,9 @@ sub checkout {
             }
         ]
     };
+    if ($format) {
+        push @{$request_content->{fields}}, { name => 'formatType', value => $format };
+    }
     my $req = {
         method  => 'POST',
         uri     => $self->{circulation_base_uri} . "/patrons/me/checkouts",
@@ -517,7 +546,16 @@ sub checkout {
     };
     if (my $res = $self->handle_http_request($req, $self->{session_id})) {
         if ($res->{content}->{expires}) {
-            return { due_date => $res->{content}->{expires} };
+            my $checkout = { due_date => $res->{content}->{expires} };
+            if (defined $res->{content}->{formats}) {
+                my $formats = {};
+                foreach my $f (@{$res->{content}->{formats}}) {
+                    my $ftype = $f->{formatType};
+                    $formats->{$ftype} = $f->{linkTemplates}->{downloadLink}->{href};
+                }
+                $checkout->{formats} = $formats;
+            }
+            return $checkout;
         }
         $logger->error("EbookAPI: checkout failed for OverDrive title $title_id");
         return { error_msg => ( (defined $res->{content}) ? $res->{content} : 'Unknown checkout error' ) };
@@ -637,12 +675,17 @@ sub get_patron_checkouts {
         foreach my $checkout (@{$res->{content}->{checkouts}}) {
             my $title_id = $checkout->{reserveId};
             my $title_info = $self->get_title_info($title_id);
-            # TODO get download URL - need to "lock in" a format first, see OD Checkouts API docs
+            my $formats = {};
+            foreach my $f (@{$checkout->{formats}}) {
+                my $ftype = $f->{formatType};
+                $formats->{$ftype} = $f->{linkTemplates}->{downloadLink}->{href};
+            };
             push @$checkouts, {
                 title_id => $title_id,
                 due_date => $checkout->{expires},
                 title => $title_info->{title},
-                author => $title_info->{author}
+                author => $title_info->{author},
+                formats => $formats
             }
         };
         $self->{checkouts} = $checkouts;
@@ -703,4 +746,33 @@ sub do_get_patron_xacts {
     return $self->handle_http_request($req, $self->{session_id});
 }
 
+# get download URL for checked-out title
+sub do_get_download_link {
+    my ($self, $request_link) = @_;
+    # Request links use the same domain as the circulation base URI, but they
+    # are apparently always plain HTTP.  The request link still works if you
+    # use HTTPS instead.  So, if our circulation base URI uses HTTPS, let's
+    # force the request link to HTTPS too, for two reasons:
+    # 1. A preference for HTTPS is implied by the library's circulation base
+    #    URI setting.
+    # 2. The base URI of the request link has to match the circulation base URI
+    #    (including the same protocol) in order for the handle_http_request()
+    #    method above to automatically re-authenticate the patron, if required.
+    if ($self->{circulation_base_uri} =~ /^https:/) {
+        $request_link =~ s/^http:/https:/;
+    }
+    my $req = {
+        method  => 'GET',
+        uri     => $request_link
+    };
+    if (my $res = $self->handle_http_request($req, $self->{session_id})) {
+        if ($res->{content}->{links}->{contentlink}->{href}) {
+            return { url => $res->{content}->{links}->{contentlink}->{href} };
+        }
+        return { error_msg => ( (defined $res->{content}) ? $res->{content} : 'Could not get content link' ) };
+    }
+    $logger->error("EbookAPI: no response received from OverDrive server");
+    return;
+}
+
 1;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm
index adab52c..31d7bf9 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm
@@ -272,6 +272,9 @@ sub checkout {
     # Patron ID or patron auth token, as returned by do_patron_auth().
     my $user_token = shift;
 
+    # Ebook format to be checked out (optional, not used here).
+    my $format = shift;
+
     # If checkout succeeds, the response is a hashref with the following fields:
     # - due_date
     # - xact_id (optional)
@@ -503,3 +506,23 @@ sub get_patron_holds {
     return $self->{holds};
 }
 
+sub do_get_download_link {
+    my $self = shift;
+    my $request_link = shift;
+
+    # For some vendors (e.g. OverDrive), the workflow is as follows:
+    #
+    # 1. Perform a checkout.
+    # 2. Checkout response contains a URL which we use to request a
+    #    format-specific download link for the checked-out title.
+    # 3. Submit a request to the request link.
+    # 4. Response contains a (temporary/dynamic) URL which the user
+    #    clicks on to download the ebook in the desired format.
+    #    
+    # For other vendors, the download link for a title is static and not
+    # format-dependent.  In that case, we just return the original request link
+    # (but ideally the UI will skip the download link request altogether, since
+    # it's superfluous in that case).
+
+    return $request_link;
+}
diff --git a/Open-ILS/src/perlmods/live_t/20-lp1541559-ebook-api.t b/Open-ILS/src/perlmods/live_t/20-lp1541559-ebook-api.t
index 6c0aedb..0b81a4c 100644
--- a/Open-ILS/src/perlmods/live_t/20-lp1541559-ebook-api.t
+++ b/Open-ILS/src/perlmods/live_t/20-lp1541559-ebook-api.t
@@ -1,6 +1,6 @@
 #!perl
 use strict; use warnings;
-use Test::More tests => 23; # XXX
+use Test::More tests => 24; # XXX
 use OpenILS::Utils::TestUtils;
 
 diag("Tests Ebook API");
@@ -154,6 +154,14 @@ my $checkout_req = $ebook_api->request(
 my $checkout = $checkout_req->recv->content;
 ok(exists $checkout->{due_date}, 'Ebook checked out');
 
+# open-ils.ebook_api.title.get_download_link
+my $request_link = 'http://example.com/ebookapi/t/003';
+my $download_link_req = $ebook_api->request(
+    'open-ils.ebook_api.title.get_download_link', $authtoken, $session_id, $request_link);
+my $download_link = $download_link_req->recv->content;
+# Test module just returns the original request_link as the response.
+ok($download_link eq $request_link, 'Received download link for ebook');
+
 # open-ils.ebook_api.renew
 my $renew_req = $ebook_api->request(
     'open-ils.ebook_api.renew', $authtoken, $session_id, '001', EBOOK_API_PATRON_USERNAME);
diff --git a/Open-ILS/src/templates/opac/parts/ebook_api/avail_js.tt2 b/Open-ILS/src/templates/opac/parts/ebook_api/avail_js.tt2
index 5a03dc5..216d2da 100644
--- a/Open-ILS/src/templates/opac/parts/ebook_api/avail_js.tt2
+++ b/Open-ILS/src/templates/opac/parts/ebook_api/avail_js.tt2
@@ -29,7 +29,7 @@ dojo.addOnLoad(function() {
                             if (holdings.formats.length > 0) {
                                 var formats_ul = dojo.create("ul", null, ebook.rec_id + '_formats');
                                 dojo.forEach(holdings.formats, function(f) {
-                                    dojo.create("li", { innerHTML: f }, formats_ul);
+                                    dojo.create("li", { innerHTML: f.name }, formats_ul);
                                 });
                                 var status_node = dojo.byId(ebook.rec_id + '_status');
                                 var status_str = holdings.copies_available + ' of ' + holdings.copies_owned + ' available';
diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js b/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js
index 5119507..03814c4 100644
--- a/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js
@@ -13,11 +13,10 @@ function Ebook(vendor, id) {
     this.avail;   // availability info for this title
     this.holdings = {}; // holdings info
     this.conns = {}; // references to Dojo event connection for performing actions with this ebook
-
 }
 
 Ebook.prototype.getDetails = function(callback) {
-    var ses = dojo.cookie(this.vendor);
+    var ses = this.ses || dojo.cookie(this.vendor);
     var ebook = this;
     new OpenSRF.ClientSession('open-ils.ebook_api').request({
         method: 'open-ils.ebook_api.title.details',
@@ -29,6 +28,8 @@ Ebook.prototype.getDetails = function(callback) {
                 console.log('title details response: ' + resp.content());
                 ebook.title = resp.content().title;
                 ebook.author = resp.content().author;
+                if (typeof resp.content().formats !== 'undefined')
+                    ebook.formats = resp.content().formats;
                 return callback(ebook);
             }
         }
@@ -36,7 +37,7 @@ Ebook.prototype.getDetails = function(callback) {
 }
 
 Ebook.prototype.getAvailability = function(callback) {
-    var ses = dojo.cookie(this.vendor);
+    var ses = this.ses || dojo.cookie(this.vendor);
     new OpenSRF.ClientSession('open-ils.ebook_api').request({
         method: 'open-ils.ebook_api.title.availability',
         params: [ ses, this.id ],
@@ -53,7 +54,7 @@ Ebook.prototype.getAvailability = function(callback) {
 }
 
 Ebook.prototype.getHoldings = function(callback) {
-    var ses = dojo.cookie(this.vendor);
+    var ses = this.ses || dojo.cookie(this.vendor);
     new OpenSRF.ClientSession('open-ils.ebook_api').request({
         method: 'open-ils.ebook_api.title.holdings',
         params: [ ses, this.id ],
@@ -70,11 +71,18 @@ Ebook.prototype.getHoldings = function(callback) {
 }
 
 Ebook.prototype.checkout = function(authtoken, patron_id, callback) {
-    var ses = dojo.cookie(this.vendor);
+    var ses = this.ses || dojo.cookie(this.vendor);
     var ebook = this;
+    // get selected checkout format (optional, used by OverDrive)
+    var checkout_format;
+    var format_selector = dojo.byId('checkout-format');
+    if (format_selector) {
+        checkout_format = format_selector.value;
+    }
+    // perform checkout
     new OpenSRF.ClientSession('open-ils.ebook_api').request({
         method: 'open-ils.ebook_api.checkout',
-        params: [ authtoken, ses, ebook.id, patron_id ],
+        params: [ authtoken, ses, ebook.id, patron_id, checkout_format ],
         async: true,
         oncomplete: function(r) {
             var resp = r.recv();
@@ -87,7 +95,7 @@ Ebook.prototype.checkout = function(authtoken, patron_id, callback) {
 }
 
 Ebook.prototype.placeHold = function(authtoken, patron_id, callback) {
-    var ses = dojo.cookie(this.vendor);
+    var ses = this.ses || dojo.cookie(this.vendor);
     var ebook = this;
     new OpenSRF.ClientSession('open-ils.ebook_api').request({
         method: 'open-ils.ebook_api.place_hold',
@@ -104,7 +112,7 @@ Ebook.prototype.placeHold = function(authtoken, patron_id, callback) {
 }
 
 Ebook.prototype.cancelHold = function(authtoken, patron_id, callback) {
-    var ses = dojo.cookie(this.vendor);
+    var ses = this.ses || dojo.cookie(this.vendor);
     var ebook = this;
     new OpenSRF.ClientSession('open-ils.ebook_api').request({
         method: 'open-ils.ebook_api.cancel_hold',
@@ -120,3 +128,43 @@ Ebook.prototype.cancelHold = function(authtoken, patron_id, callback) {
     }).send();
 }
 
+Ebook.prototype.download = function() {
+    var ses = this.ses || dojo.cookie(this.vendor);
+    var ebook = this;
+    var request_link;
+    var format_selector = dojo.byId('download-format');
+    if (!format_selector) {
+        console.log('could not find a specified format for download');
+        return;
+    } else {
+        request_link = format_selector.value;
+    }
+    // Request links include params like "errorpageurl={errorpageurl}"
+    // for redirecting the user if there's an error doing the download, etc.
+    // In these scenarios we always redirect the user to the current page.
+    // TODO: Add params to the current-page URL so that, if redirected, we
+    // can detect those params on page reload and show a useful message.
+    request_link = request_link.replace('{errorpageurl}', window.location.href);
+    request_link = request_link.replace('{odreadauthurl}', window.location.href);
+    // Now we're ready to request our download link.
+    new OpenSRF.ClientSession('open-ils.ebook_api').request({
+        method: 'open-ils.ebook_api.title.get_download_link',
+        params: [ authtoken, ses, request_link ],
+        async: true,
+        oncomplete: function(r) {
+            var resp = r.recv();
+            if (resp) {
+                if (resp.content().error_msg) {
+                    console.log('download link request failed: ' + resp.content().error_msg);
+                } else if (resp.content().url) {
+                    var url = resp.content().url;
+                    console.log('download link received: ' + url);
+                    window.location = url;
+                } else {
+                    console.log('unknown error requesting download link');
+                }
+            }
+        }
+    }).send();
+}
+
diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
index be520d5..56846bc 100644
--- a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
@@ -20,6 +20,7 @@ var xacts = {
     holds_pending: [],
     holds_ready: []
 };
+var ebooks = [];
 
 // Ebook to perform actions on.
 var active_ebook;
@@ -62,6 +63,10 @@ function addTotalsToPage() {
 
 // Update current page with detailed transaction info, where appropriate.
 function addTransactionsToPage() {
+    // ensure active ebook has access to session ID to avoid scoping issues during transactions
+    if (active_ebook && typeof active_ebook.vendor !== 'undefined') {
+        active_ebook.ses = active_ebook.ses || dojo.cookie(active_ebook.vendor);
+    }
     if (myopac_page) {
         console.log('updating page with cached transaction details, if applicable');
         if (myopac_page === 'ebook_circs')
@@ -113,13 +118,25 @@ function updateCheckoutView() {
     } else {
         dojo.empty('ebook_circs_main_table_body');
         dojo.forEach(xacts.checkouts, function(x) {
-            var dl_link = '<a href="' + x.download_url + '">' + l_strings.download + '</a>';
+            var ebook = new Ebook(x.vendor, x.title_id);
             var tr = dojo.create("tr", null, dojo.byId('ebook_circs_main_table_body'));
             dojo.create("td", { innerHTML: x.title }, tr);
             dojo.create("td", { innerHTML: x.author }, tr);
             dojo.create("td", { innerHTML: x.due_date }, tr);
-            dojo.create("td", { innerHTML: dl_link}, tr);
+            var dl_td = dojo.create("td", null, tr);
+            if (x.download_url) {
+                dl_td.innerHTML = '<a href="' + x.download_url + '">' + l_strings.download + '</a>';
+            }
+            if (x.formats) {
+                var select = dojo.create("select", { id: "download-format" }, dl_td);
+                for (f in x.formats) {
+                    dojo.create("option", { value: x.formats[f], innerHTML: f }, select);
+                }
+                var button = dojo.create("input", { id: "download-button", type: "button", value: l_strings.download }, dl_td);
+                ebook.conns.download = dojo.connect(button, 'onclick', ebook, "download");
+            }
             // TODO: more actions (renew, checkin)
+            ebooks.push(ebook);
         });
         dojo.addClass('no_ebook_circs', "hidden");
         dojo.removeClass('ebook_circs_main', "hidden");
@@ -184,6 +201,8 @@ function updateHoldView() {
 
 // set up page for user to perform a checkout
 function getReadyForCheckout() {
+    if (typeof ebook_action.type === 'undefined')
+        return;
     if (typeof active_ebook === 'undefined') {
         console.log('No active ebook specified, cannot prepare for checkout');
         dojo.removeClass('ebook_checkout_failed', "hidden");
@@ -195,6 +214,12 @@ function getReadyForCheckout() {
             dojo.create("td", { innerHTML: ebook.author }, tr);
             dojo.create("td", null, tr);
             dojo.create("td", { id: "checkout-button-td" }, tr);
+            if (typeof active_ebook.formats !== 'undefined') {
+                var select = dojo.create("select", { id: "checkout-format" }, dojo.byId('checkout-button-td'));
+                dojo.forEach(active_ebook.formats, function(f) {
+                    dojo.create("option", { value: f.id, innerHTML: f.name }, select);
+                });
+            }
             var button = dojo.create("input", { id: "checkout-button", type: "button", value: l_strings.checkout }, dojo.byId('checkout-button-td'));
             ebook.conns.checkout = dojo.connect(button, 'onclick', "doCheckout");
             dojo.removeClass('ebook_circs_main', "hidden");
@@ -204,6 +229,8 @@ function getReadyForCheckout() {
 
 // set up page for user to place a hold
 function getReadyForHold() {
+    if (typeof ebook_action.type === 'undefined')
+        return;
     if (typeof active_ebook === 'undefined') {
         console.log('No active ebook specified, cannot prepare for hold');
         dojo.removeClass('ebook_hold_failed', "hidden");
@@ -238,28 +265,72 @@ function cleanupAfterAction() {
 
 // check out our active ebook
 function doCheckout() {
+    var ses = dojo.cookie(active_ebook.vendor); // required when inspecting checkouts for download_url
     active_ebook.checkout(authtoken, patron_id, function(resp) {
-        if (resp.due_date) {
-            console.log('Checkout succeeded!');
-            dojo.destroy('checkout-button');
-            dojo.removeClass('ebook_checkout_succeeded', "hidden");
-            // add our successful checkout to top of transaction cache
-            var new_xact = {
-                title_id: active_ebook.id,
-                title: active_ebook.title,
-                author: active_ebook.author,
-                due_date: resp.due_date,
-                download_url: '' // TODO - for OverDrive, user must "lock in" a format first!
-            };
-            xacts.checkouts.unshift(new_xact);
-            cleanupAfterAction();
-        } else {
+        if (resp.error_msg) {
             console.log('Checkout failed: ' + resp.error_msg);
             dojo.removeClass('ebook_checkout_failed', "hidden");
+            return;
+        }
+        console.log('Checkout succeeded!');
+        dojo.destroy('checkout-button');
+        dojo.destroy('checkout-format'); // remove optional format selector
+        dojo.removeClass('ebook_checkout_succeeded', "hidden");
+        // add our successful checkout to top of transaction cache
+        var new_xact = {
+            title_id: active_ebook.id,
+            title: active_ebook.title,
+            author: active_ebook.author,
+            due_date: resp.due_date,
+            finish: function() {
+                console.log('new_xact.finish()');
+                xacts.checkouts.unshift(this);
+                cleanupAfterAction();
+                // When we switch to jQuery, we can use .one() instead of .on(),
+                // obviating the need for an explicit disconnect here.
+                dojo.disconnect(active_ebook.conns.checkout);
+            }
+        };
+        if (resp.download_url) {
+            // Use download URL from checkout response, if available.
+            new_xact.download_url = resp.download_url;
+            dojo.create("a", { href: new_xact.download_url, innerHTML: l_strings.download }, dojo.byId('checkout-button-td'));
+            new_xact.finish();
+        } else if (typeof resp.formats !== 'undefined') {
+            // User must select download format from list of options.
+            var select = dojo.create("select", { id: "download-format" }, dojo.byId('checkout-button-td'));
+            for (f in resp.formats) {
+                dojo.create("option", { value: resp.formats[f], innerHTML: f }, select);
+            }
+            var button = dojo.create("input", { id: "download-button", type: "button", value: l_strings.download }, dojo.byId('checkout-button-td'));
+            active_ebook.conns.download = dojo.connect(button, 'onclick', active_ebook, "download");
+            new_xact.finish();
+        } else if (typeof resp.xact_id !== 'undefined') {
+            // No download URL provided by API checkout response.  Grab fresh
+            // list of user checkouts from API, find the just-completed
+            // checkout by transaction ID, and get the download URL from that.
+            // We call the OpenSRF method directly because Relation.getCheckouts()
+            // results in scoping issues when retrieving the vendor session cookie.
+            new_xact.xact_id = resp.xact_id;
+            new OpenSRF.ClientSession('open-ils.ebook_api').request({
+                method: 'open-ils.ebook_api.patron.get_checkouts',
+                params: [ authtoken, ses, patron_id ],
+                async: false,
+                oncomplete: function(r) {
+                    var resp = r.recv();
+                    if (resp) {
+                        dojo.forEach(resp.content(), function(x) {
+                            if (x.xact_id === new_xact.xact_id) {
+                                new_xact.download_url = x.download_url;
+                                dojo.create("a", { href: new_xact.download_url, innerHTML: l_strings.download }, dojo.byId('checkout-button-td'));
+                                return;
+                            }
+                        });
+                        new_xact.finish();
+                    }
+                }
+            }).send();
         }
-        // When we switch to jQuery, we can use .one() instead of .on(),
-        // obviating the need for an explicit disconnect here.
-        dojo.disconnect(active_ebook.conns.checkout);
     });
 }
 

commit 8a9ba80dd1533138773762a5d3ee4c030b2c92bc
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Wed Jun 28 13:49:37 2017 -0700

    LP#1673870: fix bug that prevented more than one ebook transaction from appearing in list in My Account
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
index 6f73789..be520d5 100644
--- a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
@@ -111,8 +111,8 @@ function updateCheckoutView() {
     if (xacts.checkouts.length < 1) {
         dojo.removeClass('no_ebook_circs', "hidden");
     } else {
+        dojo.empty('ebook_circs_main_table_body');
         dojo.forEach(xacts.checkouts, function(x) {
-            dojo.empty('ebook_circs_main_table_body');
             var dl_link = '<a href="' + x.download_url + '">' + l_strings.download + '</a>';
             var tr = dojo.create("tr", null, dojo.byId('ebook_circs_main_table_body'));
             dojo.create("td", { innerHTML: x.title }, tr);
@@ -141,6 +141,7 @@ function updateHoldView() {
     if (holds.length < 1) {
         dojo.removeClass('no_ebook_holds', "hidden");
     } else {
+        dojo.empty('ebook_holds_main_table_body');
         dojo.forEach(holds, function(h) {
             var hold_status;
             if (h.is_ready) {
@@ -167,7 +168,6 @@ function updateHoldView() {
                     }
                 });
             };
-            dojo.empty('ebook_holds_main_table_body');
             var tr = dojo.create("tr", { id: "hold-" + h.title_id }, dojo.byId('ebook_holds_main_table_body'));
             dojo.create("td", { innerHTML: h.title }, tr);
             dojo.create("td", { innerHTML: h.author }, tr);

commit 2efd93f58bee719e8d8f85a25dd45b2b6e552a6a
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Wed Jun 28 13:36:24 2017 -0700

    LP#1673870: Support placing and canceling OverDrive holds
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm
index e09282e..d6961a8 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm
@@ -481,7 +481,7 @@ __PACKAGE__->register_method(
 # - barcode: patron barcode
 #
 sub do_xact {
-    my ($self, $conn, $auth, $session_id, $title_id, $barcode) = @_;
+    my ($self, $conn, $auth, $session_id, $title_id, $barcode, $email) = @_;
 
     my $action;
     if ($self->api_name =~ /checkout/) {
@@ -508,7 +508,13 @@ sub do_xact {
     my $user_token = $handler->do_patron_auth($barcode);
 
     # handler method constructs and submits request (and handles any external authentication)
-    my $res = $handler->$action($title_id, $user_token);
+    my $res;
+    # place_hold has email as optional additional param
+    if ($action eq 'place_hold') {
+        $res = $handler->place_hold($title_id, $user_token, $email);
+    } else {
+        $res = $handler->$action($title_id, $user_token);
+    }
     if (defined ($res)) {
         return $res;
     } else {
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm
index 6134594..9549fa4 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm
@@ -555,6 +555,59 @@ sub checkin {
     return;
 }
 
+sub place_hold {
+    my ($self, $title_id, $patron_token, $email) = @_;
+    my $fields = [
+        {
+            name  => 'reserveId',
+            value => $title_id
+        }
+    ];
+    if ($email) {
+        push @$fields, { name => 'emailAddress', value => $email };
+        # TODO: Use autoCheckout=true when we have a patron email?
+    } else {
+        push @$fields, { name => 'ignoreEmail', value => 'true' };
+    }
+    my $request_content = { fields => $fields };
+    my $req = {
+        method  => 'POST',
+        uri     => $self->{circulation_base_uri} . "/patrons/me/holds",
+        content => OpenSRF::Utils::JSON->perl2JSON($request_content)
+    };
+    if (my $res = $self->handle_http_request($req, $self->{session_id})) {
+        if ($res->{content}->{holdPlacedDate}) {
+            return {
+                queue_position => $res->{content}->{holdListPosition},
+                queue_size => $res->{content}->{numberOfHolds},
+                expire_date => (defined $res->{content}->{holdExpires}) ? $res->{content}->{holdExpires} : undef
+            };
+        }
+        $logger->error("EbookAPI: place hold failed for OverDrive title $title_id");
+        return { error_msg => "Could not place hold." };
+    }
+    $logger->error("EbookAPI: no response received from OverDrive server");
+    return;
+}
+
+sub cancel_hold {
+    my ($self, $title_id, $patron_token) = @_;
+    my $req = {
+        method  => 'DELETE',
+        uri     => $self->{circulation_base_uri} . "/patrons/me/holds/$title_id"
+    };
+    if (my $res = $self->handle_http_request($req, $self->{session_id})) {
+        if ($res->{status} =~ /^204/) {
+            return {};
+        } else {
+            $logger->error("EbookAPI: cancel hold failed for OverDrive title $title_id");
+            return { error_msg => ( (defined $res->{content}) ? $res->{content} : 'Could not cancel hold' ) };
+        }
+    }
+    $logger->error("EbookAPI: no response received from OverDrive server");
+    return;
+}
+
 # List of patron checkouts:
 # GET http://patron.api.overdrive.com/v1/patrons/me/checkouts
 # User-Agent: {Your application}
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm
index a6f3e3f..adab52c 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm
@@ -368,6 +368,9 @@ sub place_hold {
     # Patron ID or patron auth token, as returned by do_patron_auth().
     my $user_token = shift;
 
+    # Email address of patron (optional, not used here).
+    my $email = shift;
+
     # If hold is successfully placed, return a hashref with the following
     # fields:
     # - queue_position: this user's position in hold queue for this title
diff --git a/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2 b/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2
index 9d8b06f..30ee0f5 100644
--- a/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2
+++ b/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2
@@ -58,6 +58,11 @@ ebook_action.vendor = '[% CGI.param("vendor") %]';
 // user- or login-specific vars
 var authtoken = '[% ctx.authtoken %]';
 var patron_id = '[% ctx.active_card %]'; // using barcode of active card as patron ID
+[%- IF ctx.user.email %]
+var patron_email = '[% ctx.user.email | html %]';
+[%- ELSE %]
+var patron_email = null;
+[%- END %]
 
 var myopac_page;
 [% IF myopac_page %]
diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js b/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js
index f074926..5119507 100644
--- a/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js
@@ -91,7 +91,7 @@ Ebook.prototype.placeHold = function(authtoken, patron_id, callback) {
     var ebook = this;
     new OpenSRF.ClientSession('open-ils.ebook_api').request({
         method: 'open-ils.ebook_api.place_hold',
-        params: [ authtoken, ses, ebook.id, patron_id ],
+        params: [ authtoken, ses, ebook.id, patron_id, patron_email ],
         async: true,
         oncomplete: function(r) {
             var resp = r.recv();

commit 4f9df515878d90c031c75c9d4fb5ebf486a87d4c
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Mon Jun 26 16:23:54 2017 -0700

    LP#1673870: Use separate templates for ebook checkouts and holds
    
    Previously we used the same TT2 template for listing checkouts and
    performing a checkout, and for listing holds and placing a hold.  This
    leads to problems; in particular, URL parameters required for checkout
    and hold actions were "sticky," so viewing your holds after placing a
    hold would show the "Place Hold" UI instead of a list of holds.  This
    commit introduces separate templates for ebook actions as a workaround.
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/templates/opac/myopac/ebook_checkout.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_checkout.tt2
new file mode 100644
index 0000000..2b38da3
--- /dev/null
+++ b/Open-ILS/src/templates/opac/myopac/ebook_checkout.tt2
@@ -0,0 +1,33 @@
+[%  PROCESS "opac/parts/header.tt2";
+    PROCESS "opac/parts/misc_util.tt2";
+    PROCESS "opac/parts/myopac/column_sort_support.tt2";
+    WRAPPER "opac/parts/myopac/base.tt2";
+    myopac_page = "ebook_checkout";
+%]
+<h3 class="sr-only">[% l('Check Out E-Item') %]</h3>
+<div id='myopac_checked_div'>
+
+    <div class="header_middle">
+        <span class="float-left">[% l('Check Out E-Item') %]</span>
+    </div>
+    <div class="clear-both"></div>
+    <div id="ebook_checkout_failed" class="warning_box hidden">[% l('E-item could not be checked out.') %]</div>
+    <div id="ebook_checkout_succeeded" class="success hidden">[% l('E-item successfully checked out.') %]</div>
+    <div id='ebook_circs_main' class="hidden">
+        <table id="ebook_circs_main_table"
+            title="[% l('Check Out E-Item') %]"
+            class="table_no_border_space table_no_cell_pad item_list_padding">
+            <thead>
+            <tr>
+                <th>[% l("Title") %]</th>
+                <th>[% l("Author") %]</th>
+                <th>[% l("Due Date") %]</th>
+                <th>[% l("Actions") %]</th>
+            </tr>
+            </thead>
+            <tbody id="ebook_circs_main_table_body"></tbody>
+        </table>
+    </div>
+</div>
+[% END %]
+
diff --git a/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2
index 7af1a8d..1bee6e7 100644
--- a/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2
+++ b/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2
@@ -3,11 +3,6 @@
     PROCESS "opac/parts/myopac/column_sort_support.tt2";
     WRAPPER "opac/parts/myopac/base.tt2";
     myopac_page = "ebook_circs";
-    IF CGI.param("action") == 'checkout';
-        ebook_circs_title = l('Check Out E-Item');
-    ELSE;
-        ebook_circs_title = l('E-Items Currently Checked Out');
-    END;
 %]
 <h3 class="sr-only">[% l('E-Items Currently Checked Out') %]</h3>
 <div id='myopac_checked_div'>
@@ -25,15 +20,13 @@
     </div>
 
     <div class="header_middle">
-        <span class="float-left">[% ebook_circs_title %]</span>
+        <span class="float-left">[% l('E-Items Currently Checked Out') %]</span>
     </div>
     <div class="clear-both"></div>
     <div id="no_ebook_circs" class="warning_box hidden">[% l('You have no e-items checked out.') %]</div>
-    <div id="ebook_checkout_failed" class="warning_box hidden">[% l('E-item could not be checked out.') %]</div>
-    <div id="ebook_checkout_succeeded" class="success hidden">[% l('E-item successfully checked out.') %]</div>
     <div id='ebook_circs_main' class="hidden">
         <table id="ebook_circs_main_table"
-            title="[% ebook_circs_title %]"
+            title="[% l('E-Items Currently Checked Out') %]"
             class="table_no_border_space table_no_cell_pad item_list_padding">
             <thead>
             <tr>
diff --git a/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2
index eeefea3..1a77a71 100644
--- a/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2
+++ b/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2
@@ -4,13 +4,6 @@
     PROCESS "opac/parts/myopac/column_sort_support.tt2";
     WRAPPER "opac/parts/myopac/base.tt2";
     myopac_page = "ebook_holds";
-    IF CGI.param("action") == 'place_hold';
-        ebook_holds_title = l('Place Hold on E-Item');
-    ELSIF CGI.param("action") == 'cancel_hold';
-        ebook_holds_title = l('Cancel Hold on E-Item');
-    ELSE;
-        ebook_holds_title = l('E-Items on Hold');
-    END;
     limit = (ctx.holds_limit.defined) ? ctx.holds_limit : 0;
     offset = (ctx.holds_offset.defined) ? ctx.holds_offset : 0;
     count = (ctx.holds_ids.size.defined) ? ctx.holds_ids.size : 0;
@@ -34,17 +27,15 @@
     </div>
 
     <div class="header_middle">
-        <span class="float-left">[% ebook_holds_title %]</span>
+        <span class="float-left">[% l('E-Items on Hold') %]</span>
     </div>
     <div class="clear-both"></div>
     <div id="no_ebook_holds" class="warning_box hidden">[% l('You have no e-item holds.') %]</div>
-    <div id="ebook_place_hold_failed" class="warning_box hidden">[% l('Hold could not be placed.') %]</div>
-    <div id="ebook_place_hold_succeeded" class="success hidden">[% l('E-item is now on hold.') %]</div>
     <div id="ebook_cancel_hold_failed" class="warning_box hidden">[% l('Hold could not be canceled.') %]</div>
     <div id="ebook_cancel_hold_succeeded" class="success hidden">[% l('Your hold has been canceled.') %]</div>
     <div id='ebook_holds_main' class="hidden">
         <table id="ebook_holds_main_table"
-            title="[% ebook_holds_title %]"
+            title="[% l('E-Items on Hold') %]"
             class="table_no_border_space table_no_cell_pad item_list_padding">
             <thead>
             <tr>
diff --git a/Open-ILS/src/templates/opac/myopac/ebook_place_hold.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_place_hold.tt2
new file mode 100644
index 0000000..6cdbc54
--- /dev/null
+++ b/Open-ILS/src/templates/opac/myopac/ebook_place_hold.tt2
@@ -0,0 +1,35 @@
+[%  PROCESS "opac/parts/header.tt2";
+    PROCESS "opac/parts/misc_util.tt2";
+    PROCESS "opac/parts/hold_status.tt2";
+    PROCESS "opac/parts/myopac/column_sort_support.tt2";
+    WRAPPER "opac/parts/myopac/base.tt2";
+    myopac_page = "ebook_place_hold";
+%]
+<h3 class="sr-only">[% l('Place Hold on E-Item') %]</h3>
+<div id='myopac_holds_div'>
+
+    <div class="header_middle">
+        <span class="float-left">[% l('Place Hold on E-Item') %]</span>
+    </div>
+    <div class="clear-both"></div>
+    <div id="ebook_place_hold_failed" class="warning_box hidden">[% l('Hold could not be placed.') %]</div>
+    <div id="ebook_place_hold_succeeded" class="success hidden">[% l('E-item is now on hold.') %]</div>
+    <div id='ebook_holds_main' class="hidden">
+        <table id="ebook_holds_main_table"
+            title="[% l('Place Hold on E-Item') %]"
+            class="table_no_border_space table_no_cell_pad item_list_padding">
+            <thead>
+            <tr>
+                <th>[% l("Title") %]</th>
+                <th>[% l("Author") %]</th>
+                <th>[% l("Expire Date") %]</th>
+                <th>[% l("Status") %]</th>
+                <th>[% l("Actions") %]</th>
+            </tr>
+            </thead>
+            <tbody id="ebook_holds_main_table_body"></tbody>
+        </table>
+    </div>
+</div>
+[% END %]
+
diff --git a/Open-ILS/src/templates/opac/parts/record/summary.tt2 b/Open-ILS/src/templates/opac/parts/record/summary.tt2
index c9b9919..f893cf6 100644
--- a/Open-ILS/src/templates/opac/parts/record/summary.tt2
+++ b/Open-ILS/src/templates/opac/parts/record/summary.tt2
@@ -82,14 +82,14 @@
 
             [%- IF ebook_api.enabled == 'true' && args.ebook %]
             <div id="[%- ctx.bre_id -%]_ebook_checkout" class="rdetail_aux_utils ebook_action hidden">
-                <a href="[% mkurl(ctx.opac_root _ '/myopac/ebook_circs',
+                <a href="[% mkurl(ctx.opac_root _ '/myopac/ebook_checkout',
                     {title => args.ebook.ebook_id, vendor => args.ebook.vendor, action => 'checkout'}, stop_parms) %]"
                 class="no-dec" rel="nofollow" vocab=""><img src="[% ctx.media_prefix %]/images/green_check.png"
                     [% img_alt(l('Check Out [_1]', attrs.title)) %]/>
                 <span class="place_hold">[% l('Check Out E-Item') %]</span></a>
             </div>
             <div id="[%- ctx.bre_id -%]_ebook_place_hold" class="rdetail_aux_utils ebook_action hidden">
-                <a href="[% mkurl(ctx.opac_root _ '/myopac/ebook_holds',
+                <a href="[% mkurl(ctx.opac_root _ '/myopac/ebook_place_hold',
                     {title => args.ebook.ebook_id, vendor => args.ebook.vendor, action => 'place_hold'}, stop_parms) %]"
                 class="no-dec" rel="nofollow" vocab=""><img src="[% ctx.media_prefix %]/images/green_check.png"
                     [% img_alt(l('Place Hold on [_1]', attrs.title)) %]/>
diff --git a/Open-ILS/src/templates/opac/parts/result/table.tt2 b/Open-ILS/src/templates/opac/parts/result/table.tt2
index df87911..35dc615 100644
--- a/Open-ILS/src/templates/opac/parts/result/table.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/table.tt2
@@ -399,7 +399,7 @@ END;
 
                                                         [%- IF ebook_api.enabled == 'true' && args.ebook %]
                                                         <div id="[%- rec.id -%]_ebook_checkout" class="results_aux_utils result_util ebook_action hidden">
-                                                            <a href="[% mkurl(ctx.opac_root _ '/myopac/ebook_circs',
+                                                            <a href="[% mkurl(ctx.opac_root _ '/myopac/ebook_checkout',
                                                                 {title => args.ebook.ebook_id, vendor => args.ebook.vendor, action => 'checkout'},
                                                                 ['query','tag','subfield','term','_special','sort','page']) %]"
                                                                 [% html_text_attr('title', l('Check Out [_1]', attrs.title)) %]
@@ -408,7 +408,7 @@ END;
                                                                 alt=""/><span class="result_place_hold">[% l('Check Out E-Item') %]</span></a>
                                                         </div>
                                                         <div id="[%- rec.id -%]_ebook_place_hold" class="results_aux_utils result_util ebook_action hidden">
-                                                            <a href="[% mkurl(ctx.opac_root _ '/myopac/ebook_holds',
+                                                            <a href="[% mkurl(ctx.opac_root _ '/myopac/ebook_place_hold',
                                                                 {title => args.ebook.ebook_id, vendor => args.ebook.vendor, action => 'place_hold'},
                                                                 ['query','tag','subfield','term','_special','sort','page']) %]"
                                                                 [% html_text_attr('title', l('Place Hold on [_1]', attrs.title)) %]
diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
index ae2cd38..6f73789 100644
--- a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
@@ -70,6 +70,10 @@ function addTransactionsToPage() {
             updateHoldView();
         if (myopac_page === 'ebook_holds_ready')
             updateHoldView();
+        if (myopac_page === 'ebook_checkout')
+            getReadyForCheckout();
+        if (myopac_page === 'ebook_place_hold')
+            getReadyForHold();
     }
 }
         
@@ -104,11 +108,7 @@ function updateMyAccountSummary() {
 }
 
 function updateCheckoutView() {
-    if (typeof ebook_action.type !== 'undefined') {
-        if (ebook_action.type == 'checkout') {
-            getReadyForCheckout();
-        }
-    } else if (xacts.checkouts.length < 1) {
+    if (xacts.checkouts.length < 1) {
         dojo.removeClass('no_ebook_circs', "hidden");
     } else {
         dojo.forEach(xacts.checkouts, function(x) {
@@ -127,12 +127,6 @@ function updateCheckoutView() {
 }
 
 function updateHoldView() {
-    // handle hold actions
-    if (typeof ebook_action.type !== 'undefined') {
-        getReadyForHold();
-        return;
-    }
-
     if (myopac_page === 'ebook_holds_ready') {
         // only show holds that are ready for checkout
         var holds = xacts.holds_ready;

commit 34b67a5e983b92c9deeb1da5cfabb14f5bf7557c
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Fri Jul 14 11:56:54 2017 -0700

    LP#1673870: remove separate JS function for ready-only holds
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
index feb6c2b..ae2cd38 100644
--- a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
@@ -69,7 +69,7 @@ function addTransactionsToPage() {
         if (myopac_page === 'ebook_holds')
             updateHoldView();
         if (myopac_page === 'ebook_holds_ready')
-            updateHoldReadyView();
+            updateHoldView();
     }
 }
         
@@ -133,12 +133,16 @@ function updateHoldView() {
         return;
     }
 
-    // no hold action, just displaying holds
-    var holds_pending = xacts.holds_pending;
-    var holds_ready = xacts.holds_ready;
+    if (myopac_page === 'ebook_holds_ready') {
+        // only show holds that are ready for checkout
+        var holds = xacts.holds_ready;
+    } else {
+        var holds_pending = xacts.holds_pending;
+        var holds_ready = xacts.holds_ready;
 
-    // combine all holds into a single list, ready-for-checkout holds first
-    var holds = holds_ready.concat(holds_pending);
+        // combine all holds into a single list, ready-for-checkout holds first
+        var holds = holds_ready.concat(holds_pending);
+    }
 
     if (holds.length < 1) {
         dojo.removeClass('no_ebook_holds', "hidden");
@@ -184,41 +188,6 @@ function updateHoldView() {
     }
 }
 
-function updateHoldReadyView() {
-    var holds = xacts.holds_ready;
-    if (holds.length < 1) {
-        dojo.removeClass('no_ebook_holds', "hidden");
-    } else {
-        dojo.forEach(holds, function(h) {
-            dojo.empty('ebook_holds_main_table_body');
-            var tr = dojo.create("tr", null, dojo.byId('ebook_holds_main_table_body'));
-            dojo.create("td", { innerHTML: h.title }, tr);
-            dojo.create("td", { innerHTML: h.author }, tr);
-            dojo.create("td", { innerHTML: h.expire_date }, tr);
-            var actions_td = dojo.create("td", null, tr);
-            var actions_ul = dojo.create("ul", null, actions_td);
-            var cancel_hold_li = dojo.create("li", null, actions_ul);
-            dojo.create("a", { href: ebookActionUrl(h, "cancel_hold"), innerHTML: l_strings.cancel_hold }, cancel_hold_li);
-        });
-        dojo.addClass('no_ebook_holds', "hidden");
-        dojo.removeClass('ebook_holds_main', "hidden");
-    }
-}
-
-// construct a link to perform an ebook action
-// TODO: preserve any existing GET params (aside from vendor, title, action)
-function ebookActionUrl(xact, action) {
-    var base_uri;
-    if (action === 'checkout') {
-        base_uri = 'ebook_circs';
-    } else if (action === 'place_hold' || action === 'cancel_hold') {
-        base_uri = 'ebook_holds';
-    } else {
-        return;
-    }
-    return base_uri + "?vendor=" + xact.vendor + ";title=" + xact.title_id + ";action=" + action;
-}
-
 // set up page for user to perform a checkout
 function getReadyForCheckout() {
     if (typeof active_ebook === 'undefined') {

commit 1f8471c342ee4e3f2c800257bb12dd208ea99480
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Fri Jun 23 15:57:49 2017 -0700

    LP#1673870: Add ebook API cancel hold functionality to OPAC
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js b/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js
index bb90bb0..f074926 100644
--- a/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js
@@ -103,3 +103,20 @@ Ebook.prototype.placeHold = function(authtoken, patron_id, callback) {
     }).send();
 }
 
+Ebook.prototype.cancelHold = function(authtoken, patron_id, callback) {
+    var ses = dojo.cookie(this.vendor);
+    var ebook = this;
+    new OpenSRF.ClientSession('open-ils.ebook_api').request({
+        method: 'open-ils.ebook_api.cancel_hold',
+        params: [ authtoken, ses, ebook.id, patron_id ],
+        async: true,
+        oncomplete: function(r) {
+            var resp = r.recv();
+            if (resp) {
+                console.log('cancel hold response: ' + resp.content());
+                return callback(resp.content());
+            }
+        }
+    }).send();
+}
+
diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
index b03dcb6..feb6c2b 100644
--- a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
@@ -152,13 +152,32 @@ function updateHoldView() {
             } else {
                 hold_status = h.queue_position + ' / ' + h.queue_size;
             }
+            h.doCancelHold = function() {
+                var ebook = new Ebook(this.vendor, this.title_id);
+                ebook.cancelHold(authtoken, patron_id, function(resp) {
+                    if (resp.error_msg) {
+                        console.log('Cancel hold failed: ' . resp.error_msg);
+                        dojo.removeClass('ebook_cancel_hold_failed', "hidden");
+                    } else {
+                        console.log('Cancel hold succeeded!');
+                        dojo.destroy("hold-" + ebook.id);
+                        dojo.removeClass('ebook_cancel_hold_succeeded', "hidden");
+                        // Updating the transaction cache to remove the canceled hold
+                        // is inconvenient, so we skip cleanupAfterAction() and merely
+                        // clear transaction cache to force a refresh on next page load.
+                        dojo.cookie('ebook_xact_cache', '', {path: '/', expires: '-1h'});
+                    }
+                });
+            };
             dojo.empty('ebook_holds_main_table_body');
-            var tr = dojo.create("tr", null, dojo.byId('ebook_holds_main_table_body'));
+            var tr = dojo.create("tr", { id: "hold-" + h.title_id }, dojo.byId('ebook_holds_main_table_body'));
             dojo.create("td", { innerHTML: h.title }, tr);
             dojo.create("td", { innerHTML: h.author }, tr);
             dojo.create("td", { innerHTML: h.expire_date }, tr);
             dojo.create("td", { innerHTML: hold_status }, tr);
-            dojo.create("td", null, tr); // TODO actions
+            var actions_td = dojo.create("td", null, tr);
+            var button = dojo.create("input", { id: "cancel-hold-" + h.title_id, type: "button", value: l_strings.cancel_hold }, actions_td);
+            dojo.connect(button, 'onclick', h, "doCancelHold");
         });
         dojo.addClass('no_ebook_holds', "hidden");
         dojo.removeClass('ebook_holds_main', "hidden");
@@ -176,13 +195,30 @@ function updateHoldReadyView() {
             dojo.create("td", { innerHTML: h.title }, tr);
             dojo.create("td", { innerHTML: h.author }, tr);
             dojo.create("td", { innerHTML: h.expire_date }, tr);
-            dojo.create("td", null, tr); // TODO actions
+            var actions_td = dojo.create("td", null, tr);
+            var actions_ul = dojo.create("ul", null, actions_td);
+            var cancel_hold_li = dojo.create("li", null, actions_ul);
+            dojo.create("a", { href: ebookActionUrl(h, "cancel_hold"), innerHTML: l_strings.cancel_hold }, cancel_hold_li);
         });
         dojo.addClass('no_ebook_holds', "hidden");
         dojo.removeClass('ebook_holds_main', "hidden");
     }
 }
 
+// construct a link to perform an ebook action
+// TODO: preserve any existing GET params (aside from vendor, title, action)
+function ebookActionUrl(xact, action) {
+    var base_uri;
+    if (action === 'checkout') {
+        base_uri = 'ebook_circs';
+    } else if (action === 'place_hold' || action === 'cancel_hold') {
+        base_uri = 'ebook_holds';
+    } else {
+        return;
+    }
+    return base_uri + "?vendor=" + xact.vendor + ";title=" + xact.title_id + ";action=" + action;
+}
+
 // set up page for user to perform a checkout
 function getReadyForCheckout() {
     if (typeof active_ebook === 'undefined') {
@@ -220,9 +256,6 @@ function getReadyForHold() {
             if (ebook_action.type == 'place_hold') {
                 var button = dojo.create("input", { id: "hold-button", type: "button", value: l_strings.place_hold }, dojo.byId('hold-button-td'));
                 ebook.conns.checkout = dojo.connect(button, 'onclick', "doPlaceHold");
-            } else if (ebook_action.type == 'cancel_hold') {
-                var button = dojo.create("input", { id: "hold-button", type: "button", value: l_strings.cancel_hold }, dojo.byId('hold-button-td'));
-                ebook.conns.checkout = dojo.connect(button, 'onclick', "doCancelHold");
             }
             dojo.removeClass('ebook_holds_main', "hidden");
         });

commit 2bafbbd2aee289e5bb4fc810aeb369cc9ffe00a6
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Fri Jun 23 13:43:03 2017 -0700

    LP#1673870: Link ebook transaction objects to vendor in JS
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/relation.js b/Open-ILS/web/js/ui/default/opac/ebook_api/relation.js
index d423f33..9be5fc5 100644
--- a/Open-ILS/web/js/ui/default/opac/ebook_api/relation.js
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/relation.js
@@ -19,6 +19,7 @@ Relation.prototype.getCheckouts = function(callback) {
                 console.log('retrieved checkouts for patron');
                 rel.checkouts = [];
                 dojo.forEach(resp.content(), function(checkout) {
+                    checkout.vendor = rel.vendor;
                     rel.checkouts.push(checkout);
                 });
                 return callback(rel);
@@ -39,6 +40,7 @@ Relation.prototype.getHolds = function(callback) {
             if (resp) {
                 console.log('retrieved holds for patron');
                 dojo.forEach(resp.content(), function(hold) {
+                    hold.vendor = rel.vendor;
                     if (hold.is_ready === 1) {
                         rel.holds_ready.push(hold);
                     } else {
@@ -64,9 +66,11 @@ Relation.prototype.getTransactions = function(callback) {
                 console.log('retrieved holds for patron');
                 var xacts = resp.content();
                 dojo.forEach(xacts.checkouts, function(checkout) {
+                    checkout.vendor = rel.vendor;
                     rel.checkouts.push(checkout);
                 });
                 dojo.forEach(xacts.holds, function(hold) {
+                    hold.vendor = rel.vendor;
                     if (hold.is_ready === 1) {
                         rel.holds_ready.push(hold);
                     } else {

commit e80fd44bf73f770758bb8706b474ddb401e043c1
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Wed Jun 21 12:05:11 2017 -0700

    LP#1673870: Add ebook transaction links to search results and record details
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/templates/opac/parts/ebook_api/avail_js.tt2 b/Open-ILS/src/templates/opac/parts/ebook_api/avail_js.tt2
index f70f115..5a03dc5 100644
--- a/Open-ILS/src/templates/opac/parts/ebook_api/avail_js.tt2
+++ b/Open-ILS/src/templates/opac/parts/ebook_api/avail_js.tt2
@@ -35,6 +35,11 @@ dojo.addOnLoad(function() {
                                 var status_str = holdings.copies_available + ' of ' + holdings.copies_owned + ' available';
                                 status_node.innerHTML = status_str;
                                 dojo.removeClass(ebook.rec_id + '_ebook_holdings', "hidden");
+                                if (holdings.copies_available > 0) {
+                                    dojo.removeClass(ebook.rec_id + '_ebook_checkout', "hidden");
+                                } else {
+                                    dojo.removeClass(ebook.rec_id + '_ebook_place_hold', "hidden");
+                                }
                             }
                         }
                         // unhide holdings/availability info now that it's populated
diff --git a/Open-ILS/src/templates/opac/parts/record/summary.tt2 b/Open-ILS/src/templates/opac/parts/record/summary.tt2
index 42835fd..c9b9919 100644
--- a/Open-ILS/src/templates/opac/parts/record/summary.tt2
+++ b/Open-ILS/src/templates/opac/parts/record/summary.tt2
@@ -79,6 +79,24 @@
                 <span class="place_hold">[% l('Place Hold') %]</span></a>
             </div>
             [%- END -%]
+
+            [%- IF ebook_api.enabled == 'true' && args.ebook %]
+            <div id="[%- ctx.bre_id -%]_ebook_checkout" class="rdetail_aux_utils ebook_action hidden">
+                <a href="[% mkurl(ctx.opac_root _ '/myopac/ebook_circs',
+                    {title => args.ebook.ebook_id, vendor => args.ebook.vendor, action => 'checkout'}, stop_parms) %]"
+                class="no-dec" rel="nofollow" vocab=""><img src="[% ctx.media_prefix %]/images/green_check.png"
+                    [% img_alt(l('Check Out [_1]', attrs.title)) %]/>
+                <span class="place_hold">[% l('Check Out E-Item') %]</span></a>
+            </div>
+            <div id="[%- ctx.bre_id -%]_ebook_place_hold" class="rdetail_aux_utils ebook_action hidden">
+                <a href="[% mkurl(ctx.opac_root _ '/myopac/ebook_holds',
+                    {title => args.ebook.ebook_id, vendor => args.ebook.vendor, action => 'place_hold'}, stop_parms) %]"
+                class="no-dec" rel="nofollow" vocab=""><img src="[% ctx.media_prefix %]/images/green_check.png"
+                    [% img_alt(l('Place Hold on [_1]', attrs.title)) %]/>
+                <span class="place_hold">[% l('Place Hold on E-Item') %]</span></a>
+            </div>
+            [%- END -%]
+
             <div class="rdetail_aux_utils toggle_list">
         [% IF !ctx.is_staff %]
             [%  IF ctx.user;
diff --git a/Open-ILS/src/templates/opac/parts/result/table.tt2 b/Open-ILS/src/templates/opac/parts/result/table.tt2
index d520f5e..df87911 100644
--- a/Open-ILS/src/templates/opac/parts/result/table.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/table.tt2
@@ -396,6 +396,28 @@ END;
                                                                 alt=""/><span class="result_place_hold">[% l('Place Hold') %]</span></a>
                                                         </div>
 [%- END -%]
+
+                                                        [%- IF ebook_api.enabled == 'true' && args.ebook %]
+                                                        <div id="[%- rec.id -%]_ebook_checkout" class="results_aux_utils result_util ebook_action hidden">
+                                                            <a href="[% mkurl(ctx.opac_root _ '/myopac/ebook_circs',
+                                                                {title => args.ebook.ebook_id, vendor => args.ebook.vendor, action => 'checkout'},
+                                                                ['query','tag','subfield','term','_special','sort','page']) %]"
+                                                                [% html_text_attr('title', l('Check Out [_1]', attrs.title)) %]
+                                                                    class="no-dec" rel="nofollow" vocab=""><img
+                                                                src="[% ctx.media_prefix %]/images/green_check.png"
+                                                                alt=""/><span class="result_place_hold">[% l('Check Out E-Item') %]</span></a>
+                                                        </div>
+                                                        <div id="[%- rec.id -%]_ebook_place_hold" class="results_aux_utils result_util ebook_action hidden">
+                                                            <a href="[% mkurl(ctx.opac_root _ '/myopac/ebook_holds',
+                                                                {title => args.ebook.ebook_id, vendor => args.ebook.vendor, action => 'place_hold'},
+                                                                ['query','tag','subfield','term','_special','sort','page']) %]"
+                                                                [% html_text_attr('title', l('Place Hold on [_1]', attrs.title)) %]
+                                                                    class="no-dec" rel="nofollow" vocab=""><img
+                                                                src="[% ctx.media_prefix %]/images/green_check.png"
+                                                                alt=""/><span class="result_place_hold">[% l('Place Hold on E-Item') %]</span></a>
+                                                        </div>
+                                                        [%- END -%]
+
                                                         <div class="results_aux_utils result_util">
                                                         [% IF !ctx.is_staff %]
                                                             [%  IF ctx.user;

commit 2cc4477c4c657ed9a29eb57c58b5b995a35ed59a
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Wed Mar 22 13:00:47 2017 -0700

    LP#1673870: Add ebook API place hold functionality to OPAC
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2
index 76bde68..eeefea3 100644
--- a/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2
+++ b/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2
@@ -4,6 +4,13 @@
     PROCESS "opac/parts/myopac/column_sort_support.tt2";
     WRAPPER "opac/parts/myopac/base.tt2";
     myopac_page = "ebook_holds";
+    IF CGI.param("action") == 'place_hold';
+        ebook_holds_title = l('Place Hold on E-Item');
+    ELSIF CGI.param("action") == 'cancel_hold';
+        ebook_holds_title = l('Cancel Hold on E-Item');
+    ELSE;
+        ebook_holds_title = l('E-Items on Hold');
+    END;
     limit = (ctx.holds_limit.defined) ? ctx.holds_limit : 0;
     offset = (ctx.holds_offset.defined) ? ctx.holds_offset : 0;
     count = (ctx.holds_ids.size.defined) ? ctx.holds_ids.size : 0;
@@ -27,13 +34,17 @@
     </div>
 
     <div class="header_middle">
-        <span class="float-left">[% l('E-Items on Hold') %]</span>
+        <span class="float-left">[% ebook_holds_title %]</span>
     </div>
     <div class="clear-both"></div>
     <div id="no_ebook_holds" class="warning_box hidden">[% l('You have no e-item holds.') %]</div>
+    <div id="ebook_place_hold_failed" class="warning_box hidden">[% l('Hold could not be placed.') %]</div>
+    <div id="ebook_place_hold_succeeded" class="success hidden">[% l('E-item is now on hold.') %]</div>
+    <div id="ebook_cancel_hold_failed" class="warning_box hidden">[% l('Hold could not be canceled.') %]</div>
+    <div id="ebook_cancel_hold_succeeded" class="success hidden">[% l('Your hold has been canceled.') %]</div>
     <div id='ebook_holds_main' class="hidden">
         <table id="ebook_holds_main_table"
-            title="[% l('E-Items on Hold') %]"
+            title="[% ebook_holds_title %]"
             class="table_no_border_space table_no_cell_pad item_list_padding">
             <thead>
             <tr>
diff --git a/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2 b/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2
index 7479509..9d8b06f 100644
--- a/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2
+++ b/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2
@@ -13,6 +13,8 @@ l_strings.download = '[% l('Download') %]';
 l_strings.ready_for_checkout = '[% l('Ready for Checkout') %]';
 l_strings.suspended = '[% l('Suspended') %]';
 l_strings.checkout = '[% l('Checkout') %]';
+l_strings.place_hold = '[% l('Place Hold') %]';
+l_strings.cancel_hold = '[% l('Cancel Hold') %]';
 
 // give us cookies!
 dojo.require("dojo.cookie");
diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js b/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js
index 46b223c..bb90bb0 100644
--- a/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js
@@ -86,3 +86,20 @@ Ebook.prototype.checkout = function(authtoken, patron_id, callback) {
     }).send();
 }
 
+Ebook.prototype.placeHold = function(authtoken, patron_id, callback) {
+    var ses = dojo.cookie(this.vendor);
+    var ebook = this;
+    new OpenSRF.ClientSession('open-ils.ebook_api').request({
+        method: 'open-ils.ebook_api.place_hold',
+        params: [ authtoken, ses, ebook.id, patron_id ],
+        async: true,
+        oncomplete: function(r) {
+            var resp = r.recv();
+            if (resp) {
+                console.log('place hold response: ' + resp.content());
+                return callback(resp.content());
+            }
+        }
+    }).send();
+}
+
diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
index fb79a4c..b03dcb6 100644
--- a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
@@ -127,6 +127,13 @@ function updateCheckoutView() {
 }
 
 function updateHoldView() {
+    // handle hold actions
+    if (typeof ebook_action.type !== 'undefined') {
+        getReadyForHold();
+        return;
+    }
+
+    // no hold action, just displaying holds
     var holds_pending = xacts.holds_pending;
     var holds_ready = xacts.holds_ready;
 
@@ -196,6 +203,43 @@ function getReadyForCheckout() {
     }
 }
 
+// set up page for user to place a hold
+function getReadyForHold() {
+    if (typeof active_ebook === 'undefined') {
+        console.log('No active ebook specified, cannot prepare for hold');
+        dojo.removeClass('ebook_hold_failed', "hidden");
+    } else {
+        active_ebook.getDetails( function(ebook) {
+            dojo.empty('ebook_holds_main_table_body');
+            var tr = dojo.create("tr", null, dojo.byId('ebook_holds_main_table_body'));
+            dojo.create("td", { innerHTML: ebook.title }, tr);
+            dojo.create("td", { innerHTML: ebook.author }, tr);
+            dojo.create("td", null, tr); // Expire Date
+            dojo.create("td", null, tr); // Status
+            dojo.create("td", { id: "hold-button-td" }, tr);
+            if (ebook_action.type == 'place_hold') {
+                var button = dojo.create("input", { id: "hold-button", type: "button", value: l_strings.place_hold }, dojo.byId('hold-button-td'));
+                ebook.conns.checkout = dojo.connect(button, 'onclick', "doPlaceHold");
+            } else if (ebook_action.type == 'cancel_hold') {
+                var button = dojo.create("input", { id: "hold-button", type: "button", value: l_strings.cancel_hold }, dojo.byId('hold-button-td'));
+                ebook.conns.checkout = dojo.connect(button, 'onclick', "doCancelHold");
+            }
+            dojo.removeClass('ebook_holds_main', "hidden");
+        });
+    }
+}
+
+function cleanupAfterAction() {
+    // unset variables related to the transaction we have performed,
+    // to avoid any weirdness on page reload
+    ebook_action = {};
+    // update page to account for successful checkout
+    addTotalsToPage();
+    addTransactionsToPage();
+    // clear transaction cache to force a refresh on next page load
+    dojo.cookie('ebook_xact_cache', '', {path: '/', expires: '-1h'});
+}
+
 // check out our active ebook
 function doCheckout() {
     active_ebook.checkout(authtoken, patron_id, function(resp) {
@@ -212,15 +256,7 @@ function doCheckout() {
                 download_url: '' // TODO - for OverDrive, user must "lock in" a format first!
             };
             xacts.checkouts.unshift(new_xact);
-            // unset variables related to the transaction we have performed,
-            // to avoid any weirdness on page reload
-            ebook_action = {};
-            active_ebook = undefined;
-            // update page to account for successful checkout
-            addTotalsToPage();
-            addTransactionsToPage();
-            // clear transaction cache to force a refresh on next page load
-            dojo.cookie('ebook_xact_cache', '', {path: '/', expires: '-1h'});
+            cleanupAfterAction();
         } else {
             console.log('Checkout failed: ' + resp.error_msg);
             dojo.removeClass('ebook_checkout_failed', "hidden");
@@ -231,6 +267,34 @@ function doCheckout() {
     });
 }
 
+// place hold on our active ebook
+function doPlaceHold() {
+    active_ebook.placeHold(authtoken, patron_id, function(resp) {
+        if (resp.error_msg) {
+            console.log('Place hold failed: ' . resp.error_msg);
+            dojo.removeClass('ebook_place_hold_failed', "hidden");
+        } else {
+            console.log('Place hold succeeded!');
+            dojo.destroy('hold-button');
+            dojo.removeClass('ebook_place_hold_succeeded', "hidden");
+            var new_hold = {
+                title_id: active_ebook.id,
+                title: active_ebook.title,
+                author: active_ebook.author,
+                queue_position: resp.queue_position,
+                queue_size: resp.queue_size,
+                expire_date: resp.expire_date
+            };
+            if ( resp.is_ready || (resp.queue_position === 1 && resp.queue_size === 1) ) {
+                xacts.holds_ready.unshift(new_hold);
+            } else {
+                xacts.holds_pending.unshift(new_hold);
+            }
+            cleanupAfterAction();
+        }
+    });
+}
+
 // deserialize transactions from cache, returning them as a JS object
 function getCachedTransactions() {
     console.log('retrieving cached transaction details');

commit da17a15eb931f82a9b38f2d579c0fed1215a0962
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Tue Mar 21 15:32:48 2017 -0700

    LP#1673870: Checkout/renew/checkin methods for OverDrive API
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm
index 97a301a..6134594 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm
@@ -29,6 +29,7 @@ use OpenSRF::EX qw(:try);
 use OpenSRF::Utils::SettingsClient;
 use OpenSRF::Utils::Logger qw($logger);
 use OpenSRF::Utils::Cache;
+use OpenSRF::Utils::JSON;
 use OpenILS::Application::AppUtils;
 use Data::Dumper;
 
@@ -464,6 +465,96 @@ sub do_holdings_lookup {
     return $holdings;
 }
 
+# POST https://patron.api.overdrive.com/v1/patrons/me/checkouts
+# Authorization: Bearer {OAuth patron access token}
+# Content-Type: application/json; charset=utf-8
+# 
+# Request content looks like this:
+# {
+#     "fields": [
+#         {
+#             "name": "reserveId",
+#             "value": "76C1B7D0-17F4-4C05-8397-C66C17411584"
+#         }
+#     ]
+# }
+#
+# Response looks like this:
+# {
+#     "reserveId": "76C1B7D0-17F4-4C05-8397-C66C17411584",
+#     "expires": "10/14/2013 10:56:00 AM",
+#     "isFormatLockedIn": false,
+#     "formats": [
+#         {
+#             "reserveId": "76C1B7D0-17F4-4C05-8397-C66C17411584",
+#             "formatType": "ebook-overdrive",
+#             "linkTemplates": {
+#                 "downloadLink": {
+#                     "href": "https://patron.api.overdrive.com/v1/patrons/me/checkouts/76C1B7D0-17F4-4C05-8397-C66C17411584/formats/ebook-overdrive/downloadlink?errorpageurl={errorpageurl}&odreadauthurl={odreadauthurl}",
+#                     ...
+#                 },
+#                 ...
+#             },
+#             ...
+#         }
+#     ],
+#     ...
+# }
+sub checkout {
+    my ($self, $title_id, $patron_token) = @_;
+    my $request_content = {
+        fields => [
+            {
+                name  => 'reserveId',
+                value => $title_id
+            }
+        ]
+    };
+    my $req = {
+        method  => 'POST',
+        uri     => $self->{circulation_base_uri} . "/patrons/me/checkouts",
+        content => OpenSRF::Utils::JSON->perl2JSON($request_content)
+    };
+    if (my $res = $self->handle_http_request($req, $self->{session_id})) {
+        if ($res->{content}->{expires}) {
+            return { due_date => $res->{content}->{expires} };
+        }
+        $logger->error("EbookAPI: checkout failed for OverDrive title $title_id");
+        return { error_msg => ( (defined $res->{content}) ? $res->{content} : 'Unknown checkout error' ) };
+    }
+    $logger->error("EbookAPI: no response received from OverDrive server");
+    return;
+}
+
+# renew is not supported by OverDrive API
+sub renew {
+    $logger->error("EbookAPI: OverDrive API does not support renewals");
+    return { error_msg => "Title cannot be renewed." };
+}
+
+# NB: A title cannot be checked in once a format has been locked in.
+# Successful checkin returns an HTTP 204 response with no content.
+# DELETE https://patron.api.overdrive.com/v1/patrons/me/checkouts/08F7D7E6-423F-45A6-9A1E-5AE9122C82E7
+# Authorization: Bearer {OAuth patron access token}
+# Host: patron.api.overdrive.com
+sub checkin {
+    my ($self, $title_id, $patron_token) = @_;
+    my $req = {
+        method  => 'DELETE',
+        uri     => $self->{circulation_base_uri} . "/patrons/me/checkouts/$title_id"
+    };
+    if (my $res = $self->handle_http_request($req, $self->{session_id})) {
+        if ($res->{status} =~ /^204/) {
+            return {};
+        } else {
+            $logger->error("EbookAPI: checkin failed for OverDrive title $title_id");
+            return { error_msg => ( (defined $res->{content}) ? $res->{content} : 'Checkin failed' ) };
+        }
+    }
+    $logger->error("EbookAPI: no response received from OverDrive server");
+    return;
+}
+
 # List of patron checkouts:
 # GET http://patron.api.overdrive.com/v1/patrons/me/checkouts
 # User-Agent: {Your application}

commit 61f4a26d0396fa6ff3fa0293c5f439cdb8d403d7
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Fri Mar 17 15:22:32 2017 -0700

    LP#1673870: Add ebook API checkout functionality to OPAC
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2
index 5a53976..7af1a8d 100644
--- a/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2
+++ b/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2
@@ -2,7 +2,13 @@
     PROCESS "opac/parts/misc_util.tt2";
     PROCESS "opac/parts/myopac/column_sort_support.tt2";
     WRAPPER "opac/parts/myopac/base.tt2";
-    myopac_page = "ebook_circs"  %]
+    myopac_page = "ebook_circs";
+    IF CGI.param("action") == 'checkout';
+        ebook_circs_title = l('Check Out E-Item');
+    ELSE;
+        ebook_circs_title = l('E-Items Currently Checked Out');
+    END;
+%]
 <h3 class="sr-only">[% l('E-Items Currently Checked Out') %]</h3>
 <div id='myopac_checked_div'>
 
@@ -11,7 +17,7 @@
             <a href="[% mkurl('circs',{},1) %]">[% l("Current Items Checked Out") %]</a>
         </div>
         <div class="align selected">
-            <a href="#">[% l("E-Items Currently Checked Out") %]</a>
+            <a href="[% mkurl('',{},1) %]">[% l("E-Items Currently Checked Out") %]</a>
         </div>
         <div class="align">
             <a href="[% mkurl('circ_history',{},1) %]">[% l("Check Out History") %]</a>
@@ -19,13 +25,15 @@
     </div>
 
     <div class="header_middle">
-        <span class="float-left">[% l('E-Items Currently Checked Out') %]</span>
+        <span class="float-left">[% ebook_circs_title %]</span>
     </div>
     <div class="clear-both"></div>
     <div id="no_ebook_circs" class="warning_box hidden">[% l('You have no e-items checked out.') %]</div>
+    <div id="ebook_checkout_failed" class="warning_box hidden">[% l('E-item could not be checked out.') %]</div>
+    <div id="ebook_checkout_succeeded" class="success hidden">[% l('E-item successfully checked out.') %]</div>
     <div id='ebook_circs_main' class="hidden">
         <table id="ebook_circs_main_table"
-            title="[% l('E-Items Currently Checked Out') %]"
+            title="[% ebook_circs_title %]"
             class="table_no_border_space table_no_cell_pad item_list_padding">
             <thead>
             <tr>
diff --git a/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2 b/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2
index 538de71..7479509 100644
--- a/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2
+++ b/Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2
@@ -12,6 +12,7 @@ var l_strings = {};
 l_strings.download = '[% l('Download') %]';
 l_strings.ready_for_checkout = '[% l('Ready for Checkout') %]';
 l_strings.suspended = '[% l('Suspended') %]';
+l_strings.checkout = '[% l('Checkout') %]';
 
 // give us cookies!
 dojo.require("dojo.cookie");
@@ -39,6 +40,18 @@ dojo.forEach(vendor_list, function(v) {
     cookie_registry.push(v);
 });
 
+// essential info for performing a transaction
+var ebook_action = {};
+[%- IF CGI.param("action").defined %]
+ebook_action.type = '[% CGI.param("action") %]';
+[%- END -%]
+[%- IF CGI.param("title").defined %]
+ebook_action.title_id = '[% CGI.param("title") %]';
+[%- END -%]
+[%- IF CGI.param("vendor").defined %]
+ebook_action.vendor = '[% CGI.param("vendor") %]';
+[%- END -%]
+
 [% IF ctx.user %]
 // user- or login-specific vars
 var authtoken = '[% ctx.authtoken %]';
diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js b/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js
index f933b69..46b223c 100644
--- a/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js
@@ -8,8 +8,31 @@ function Ebook(vendor, id) {
     this.vendor = vendor;
     this.id = id; // external ID for this title
     this.rec_id;  // bre.id for this title's MARC record
+    this.title;   // title of ebook
+    this.author;  // author of ebook
     this.avail;   // availability info for this title
     this.holdings = {}; // holdings info
+    this.conns = {}; // references to Dojo event connection for performing actions with this ebook
+
+}
+
+Ebook.prototype.getDetails = function(callback) {
+    var ses = dojo.cookie(this.vendor);
+    var ebook = this;
+    new OpenSRF.ClientSession('open-ils.ebook_api').request({
+        method: 'open-ils.ebook_api.title.details',
+        params: [ ses, ebook.id ],
+        async: true,
+        oncomplete: function(r) {
+            var resp = r.recv();
+            if (resp) {
+                console.log('title details response: ' + resp.content());
+                ebook.title = resp.content().title;
+                ebook.author = resp.content().author;
+                return callback(ebook);
+            }
+        }
+    }).send();
 }
 
 Ebook.prototype.getAvailability = function(callback) {
@@ -46,3 +69,20 @@ Ebook.prototype.getHoldings = function(callback) {
     }).send();
 }
 
+Ebook.prototype.checkout = function(authtoken, patron_id, callback) {
+    var ses = dojo.cookie(this.vendor);
+    var ebook = this;
+    new OpenSRF.ClientSession('open-ils.ebook_api').request({
+        method: 'open-ils.ebook_api.checkout',
+        params: [ authtoken, ses, ebook.id, patron_id ],
+        async: true,
+        oncomplete: function(r) {
+            var resp = r.recv();
+            if (resp) {
+                console.log('checkout response: ' + resp.content());
+                return callback(resp.content());
+            }
+        }
+    }).send();
+}
+
diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
index 0ba1827..fb79a4c 100644
--- a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
+++ b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js
@@ -21,6 +21,12 @@ var xacts = {
     holds_ready: []
 };
 
+// Ebook to perform actions on.
+var active_ebook;
+if (typeof ebook_action.title_id !== 'undefined') {
+    active_ebook = new Ebook(ebook_action.vendor, ebook_action.title_id);
+}
+
 dojo.addOnLoad(function() {
 
     dojo.forEach(vendor_list, function(v) {
@@ -98,7 +104,11 @@ function updateMyAccountSummary() {
 }
 
 function updateCheckoutView() {
-    if (xacts.checkouts.length < 1) {
+    if (typeof ebook_action.type !== 'undefined') {
+        if (ebook_action.type == 'checkout') {
+            getReadyForCheckout();
+        }
+    } else if (xacts.checkouts.length < 1) {
         dojo.removeClass('no_ebook_circs', "hidden");
     } else {
         dojo.forEach(xacts.checkouts, function(x) {
@@ -166,6 +176,61 @@ function updateHoldReadyView() {
     }
 }
 
+// set up page for user to perform a checkout
+function getReadyForCheckout() {
+    if (typeof active_ebook === 'undefined') {
+        console.log('No active ebook specified, cannot prepare for checkout');
+        dojo.removeClass('ebook_checkout_failed', "hidden");
+    } else {
+        active_ebook.getDetails( function(ebook) {
+            dojo.empty('ebook_circs_main_table_body');
+            var tr = dojo.create("tr", null, dojo.byId('ebook_circs_main_table_body'));
+            dojo.create("td", { innerHTML: ebook.title }, tr);
+            dojo.create("td", { innerHTML: ebook.author }, tr);
+            dojo.create("td", null, tr);
+            dojo.create("td", { id: "checkout-button-td" }, tr);
+            var button = dojo.create("input", { id: "checkout-button", type: "button", value: l_strings.checkout }, dojo.byId('checkout-button-td'));
+            ebook.conns.checkout = dojo.connect(button, 'onclick', "doCheckout");
+            dojo.removeClass('ebook_circs_main', "hidden");
+        });
+    }
+}
+
+// check out our active ebook
+function doCheckout() {
+    active_ebook.checkout(authtoken, patron_id, function(resp) {
+        if (resp.due_date) {
+            console.log('Checkout succeeded!');
+            dojo.destroy('checkout-button');
+            dojo.removeClass('ebook_checkout_succeeded', "hidden");
+            // add our successful checkout to top of transaction cache
+            var new_xact = {
+                title_id: active_ebook.id,
+                title: active_ebook.title,
+                author: active_ebook.author,
+                due_date: resp.due_date,
+                download_url: '' // TODO - for OverDrive, user must "lock in" a format first!
+            };
+            xacts.checkouts.unshift(new_xact);
+            // unset variables related to the transaction we have performed,
+            // to avoid any weirdness on page reload
+            ebook_action = {};
+            active_ebook = undefined;
+            // update page to account for successful checkout
+            addTotalsToPage();
+            addTransactionsToPage();
+            // clear transaction cache to force a refresh on next page load
+            dojo.cookie('ebook_xact_cache', '', {path: '/', expires: '-1h'});
+        } else {
+            console.log('Checkout failed: ' + resp.error_msg);
+            dojo.removeClass('ebook_checkout_failed', "hidden");
+        }
+        // When we switch to jQuery, we can use .one() instead of .on(),
+        // obviating the need for an explicit disconnect here.
+        dojo.disconnect(active_ebook.conns.checkout);
+    });
+}
+
 // deserialize transactions from cache, returning them as a JS object
 function getCachedTransactions() {
     console.log('retrieving cached transaction details');

commit bddaee3a63eb5a3e3d1345899dbc5f9c30d52241
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Fri Mar 17 13:04:09 2017 -0700

    LP#1673870: Add basic ebook API title lookup
    
    Adds an API method to obtain the title and author (and, eventually,
    cover image URL) for a given ebook via the open-ils.ebook_api service.
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm
index 1b3c8c6..e09282e 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm
@@ -380,6 +380,37 @@ sub request {
     }
 }
 
+sub get_details {
+    my ($self, $conn, $session_id, $title_id) = @_;
+    my $handler = new_handler($session_id);
+    return $handler->get_title_info($title_id);
+}
+__PACKAGE__->register_method(
+    method => 'get_details',
+    api_name => 'open-ils.ebook_api.title.details',
+    api_level => 1,
+    argc => 2,
+    signature => {
+        desc => "Get basic metadata for an ebook title",
+        params => [
+            {
+                name => 'session_id',
+                desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
+                type => 'string'
+            },
+            {
+                name => 'title_id',
+                desc => 'The title ID (ISBN, unique identifier, etc.)',
+                type => 'string'
+            }
+        ],
+        return => {
+            desc => 'Success: { title => "Title", author => "Author Name" } / Failure: { error => "Title not found" }',
+            type => 'hashref'
+        }
+    }
+);
+
 sub get_availability {
     my ($self, $conn, $session_id, $title_id) = @_;
     my $handler = new_handler($session_id);
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OneClickdigital.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OneClickdigital.pm
index 166b7c9..be42463 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OneClickdigital.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OneClickdigital.pm
@@ -127,6 +127,29 @@ sub do_patron_auth {
     return;
 }
 
+# get basic metadata for an item (title, author, cover image if any)
+# GET http://api.oneclickdigital.us/v1/libraries/{libraryId}/titles/{isbn}
+sub get_title_info {
+    my ($self, $isbn) = @_;
+    my $base_uri = $self->{base_uri};
+    my $library_id = $self->{library_id};
+    my $session_id = $self->{session_id};
+    my $req = {
+        method => 'GET',
+        uri    => "$base_uri/libraries/$library_id/titles/$isbn"
+    };
+    my $res = $self->request($req, $session_id);
+    if (defined ($res)) {
+        return {
+            title  => $res->{content}->{title},
+            author => $res->{content}->{authors}[0]{text}
+        };
+    } else {
+        $logger->error("EbookAPI: could not retrieve OneClickdigital title details for ISBN $isbn");
+        return;
+    }
+}
+
 # does this title have available "copies"? y/n
 # GET http://api.oneclickdigital.us/v1/libraries/{libraryID}/media/{isbn}/availability
 sub do_availability_lookup {
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm
index a20846c..a6f3e3f 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm
@@ -42,6 +42,7 @@
 #   - do_client_auth: authenticate client with external API (e.g. get client
 #     token if needed)
 #   - do_patron_auth: get a patron-specific bearer token, or just the patron ID
+#   - get_title_info: get basic title details (title, author, optional cover image)
 #   - do_holdings_lookup: how many total/available "copies" are there for this
 #     title? (n/a for OneClickdigital)
 #   - do_availability_lookup: does this title have available "copies"? y/n
@@ -164,6 +165,43 @@ sub do_patron_auth {
     return undef;
 }
 
+# get basic info (title, author, eventually a thumbnail URL) for a title
+sub get_title_info {
+    my $self = shift;
+
+    # External ID for title.  Depending on the API, this could be an ISBN
+    # or an identifier unique to that vendor.
+    my $title_id = shift;
+
+    # Prepare data structure to be used as return value.
+    my $title_info = {
+        title  => '',
+        author => ''
+    };
+
+    # If title lookup fails or title is not found, our return value
+    # is somewhat different.
+    my $title_not_found = {
+        error => 'Title not found.'
+    };
+
+    # For testing purposes, we have only three valid titles (001, 002, 003).
+    # All other title IDs return an error message.
+    if ($title_id eq '001') {
+        $title_info->{title} = 'The Fellowship of the Ring';
+        $title_info->{author} = 'J.R.R. Tolkien';
+    } elsif ($title_id eq '002') {
+        $title_info->{title} = 'The Two Towers';
+        $title_info->{author} = 'J.R.R. Tolkien';
+    } elsif ($title_id eq '003') {
+        $title_info->{title} = 'The Return of the King';
+        $title_info->{author} = 'J.R.R. Tolkien';
+    } else {
+        return $title_not_found;
+    }
+    return $title_info;
+}
+
 # get detailed holdings information (copy counts and formats), OR basic
 # availability if detailed info is not provided by the API
 sub do_holdings_lookup {
diff --git a/Open-ILS/src/perlmods/live_t/20-lp1541559-ebook-api.t b/Open-ILS/src/perlmods/live_t/20-lp1541559-ebook-api.t
index 72054a5..6c0aedb 100644
--- a/Open-ILS/src/perlmods/live_t/20-lp1541559-ebook-api.t
+++ b/Open-ILS/src/perlmods/live_t/20-lp1541559-ebook-api.t
@@ -1,6 +1,6 @@
 #!perl
 use strict; use warnings;
-use Test::More tests => 21; # XXX
+use Test::More tests => 23; # XXX
 use OpenILS::Utils::TestUtils;
 
 diag("Tests Ebook API");
@@ -57,6 +57,18 @@ ok($new_session_id, 'Initiated new EbookAPI session when valid session ID not pr
 # 3. Title availability and holdings.
 # ------------------------------------------------------------ 
 
+# Title details for valid title ID.
+my $title_001_details_req = $ebook_api->request(
+    'open-ils.ebook_api.title.details', $session_id, '001');
+my $title_001_details = $title_001_details_req->recv->content;
+ok(ref($title_001_details) && $title_001_details->{title}, 'Title details check 1/2 (valid title)');
+
+# Title details for invalid title ID.
+my $title_004_details_req = $ebook_api->request(
+    'open-ils.ebook_api.title.details', $session_id, '004');
+my $title_004_details = $title_004_details_req->recv->content;
+ok(ref($title_004_details) && $title_004_details->{error}, 'Title details check 1/2 (invalid title returns error message)');
+
 # Title is not available.
 my $title_001_avail_req = $ebook_api->request(
     'open-ils.ebook_api.title.availability', $session_id, '001');

commit 346994bd5daaa3c6060431dd41b335f022372b39
Author: Jeff Davis <jdavis at sitka.bclibraries.ca>
Date:   Fri Jul 14 11:48:49 2017 -0700

    LP#1673870: process ebook variable in misc_util.tt2
    
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/templates/opac/parts/ebook_api/avail.tt2 b/Open-ILS/src/templates/opac/parts/ebook_api/avail.tt2
index f75a951..9df0465 100644
--- a/Open-ILS/src/templates/opac/parts/ebook_api/avail.tt2
+++ b/Open-ILS/src/templates/opac/parts/ebook_api/avail.tt2
@@ -6,18 +6,7 @@
 # - ebook_id: external ID for title (ISBN for OneClickdigital, unique identifier for OverDrive)
 # - vendor (oneclickdigital, overdrive)
 
-IF args.ebook_test_id;
-    ebook.ebook_id = args.ebook_test_id;
-    ebook.vendor = 'ebook_test';
-ELSIF args.oneclickdigital_id;
-    ebook.ebook_id = args.oneclickdigital_id;
-    ebook.vendor = 'oneclickdigital';
-ELSIF args.overdrive_id;
-    ebook.ebook_id = args.overdrive_id;
-    ebook.vendor = 'overdrive';
-END;
-
-IF ebook.ebook_id;
+IF args.ebook;
 
     IF ctx.page == 'rresult';
         ebook.rec_id = rec.id;
@@ -25,6 +14,9 @@ IF ebook.ebook_id;
         ebook.rec_id = ctx.bre_id;
     END;
 
+    ebook.ebook_id = args.ebook.ebook_id;
+    ebook.vendor = args.ebook.vendor;
+
 # This div is hidden by default. The JS layer will unhide it, use the ebook_id
 # to retrieve holdings/availability info via the appropriate vendor API, and
 # overwrite the div's contents with that information.
diff --git a/Open-ILS/src/templates/opac/parts/misc_util.tt2 b/Open-ILS/src/templates/opac/parts/misc_util.tt2
index b1ca125..97cbd1e 100644
--- a/Open-ILS/src/templates/opac/parts/misc_util.tt2
+++ b/Open-ILS/src/templates/opac/parts/misc_util.tt2
@@ -732,6 +732,17 @@
         END;
         args.bibid = args.bibid.0; 
 
+        IF args.ebook_test_id;
+            args.ebook.ebook_id = args.ebook_test_id;
+            args.ebook.vendor = 'ebook_test';
+        ELSIF args.oneclickdigital_id;
+            args.ebook.ebook_id = args.oneclickdigital_id;
+            args.ebook.vendor = 'oneclickdigital';
+        ELSIF args.overdrive_id;
+            args.ebook.ebook_id = args.overdrive_id;
+            args.ebook.vendor = 'overdrive';
+        END;
+
     END;
 
     # Get the library or location group

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

Summary of changes:
 .../perlmods/lib/OpenILS/Application/EbookAPI.pm   |   80 ++++++-
 .../Application/EbookAPI/OneClickdigital.pm        |   35 +++-
 .../lib/OpenILS/Application/EbookAPI/OverDrive.pm  |  227 ++++++++++++++++++-
 .../lib/OpenILS/Application/EbookAPI/Test.pm       |   64 +++++
 .../src/perlmods/live_t/20-lp1541559-ebook-api.t   |   22 ++-
 .../myopac/{ebook_circs.tt2 => ebook_checkout.tt2} |   26 +--
 Open-ILS/src/templates/opac/myopac/ebook_circs.tt2 |    6 +-
 Open-ILS/src/templates/opac/myopac/ebook_holds.tt2 |    3 +
 .../templates/opac/myopac/ebook_holds_ready.tt2    |    1 +
 .../src/templates/opac/myopac/ebook_place_hold.tt2 |   36 +++
 .../src/templates/opac/parts/ebook_api/avail.tt2   |   16 +-
 .../src/templates/opac/parts/ebook_api/base_js.tt2 |   24 ++
 Open-ILS/src/templates/opac/parts/js.tt2           |    1 -
 Open-ILS/src/templates/opac/parts/misc_util.tt2    |   11 +
 .../src/templates/opac/parts/record/summary.tt2    |   18 ++
 Open-ILS/src/templates/opac/parts/result/table.tt2 |   22 ++
 .../js/ui/default/opac/ebook_api/avail.js}         |   11 +-
 Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js |  126 ++++++++++-
 .../web/js/ui/default/opac/ebook_api/loggedin.js   |  245 ++++++++++++++++++--
 .../web/js/ui/default/opac/ebook_api/relation.js   |    4 +
 docs/opac/ebook_transactions.adoc                  |   36 +++
 21 files changed, 942 insertions(+), 72 deletions(-)
 copy Open-ILS/src/templates/opac/myopac/{ebook_circs.tt2 => ebook_checkout.tt2} (51%)
 create mode 100644 Open-ILS/src/templates/opac/myopac/ebook_place_hold.tt2
 rename Open-ILS/{src/templates/opac/parts/ebook_api/avail_js.tt2 => web/js/ui/default/opac/ebook_api/avail.js} (80%)
 create mode 100644 docs/opac/ebook_transactions.adoc


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list