LP#1541559: ebook API integration for TPAC
authorJeff Davis <jdavis@sitka.bclibraries.ca>
Tue, 7 Feb 2017 23:29:39 +0000 (15:29 -0800)
committerKathy Lussier <klussier@masslnc.org>
Mon, 20 Feb 2017 23:54:38 +0000 (18:54 -0500)
When this feature is enabled, Evergreen will use the open-ils.ebook_api
service to look up title and patron information from specified vendor
APIs and display that information in the TPAC.  (The service should be
configured using org settings before being enabled in config.tt2.)

This frontend is essentially a JS layer over top of the OPAC, with some
light use of Dojo since we're already using it, plus a few additions to
TT2 templates.  The JS layer uses OpenSRF JS bindings to talk to the
backend service, which in turn makes the appropriate calls to the
third-party API.  Session IDs and (if logged in) patron information are
stored in cookies, which are cleared when the patron logs out.

The user will see the following changes:

- On search results and record summary, for any records from a known
  e-book vendor, Evergreen will automatically look up holdings info from
  the vendor API.  If detailed information on formats and available
  "copies" is provided by the API (e.g. for OverDrive), that information
  is displayed in a table within the record; if only basic availability
  info is available (e.g. for OneClickdigital), a line is added to each
  record indicating whether the title is available.  (Eventually, "Place
  Hold" or "Check Out" links will be added to allow patrons to
  checkout/hold titles without leaving the TPAC.)

- When the user is logged in, the dashboard will show a count of e-book
  checkouts and holds for all enabled e-book vendors, as will the
  account summary.  This is separate from the "main" checkouts/holds
  display, since checkouts/holds on titles from third-party vendors are
  unrelated to checkouts/holds in Evergreen.

- When the user is logged in, additional tabs will be available in My
  Account for displaying detailed information on the patron's ebook
  checkouts and holds.  (Eventually, functionality will be added to My
  Account allowing the user to download or renew titles, suspend or
  cancel holds, etc.)

Signed-off-by: Jeff Davis <jdavis@sitka.bclibraries.ca>
Signed-off-by: Kathy Lussier <klussier@masslnc.org>
24 files changed:
Open-ILS/src/templates/opac/css/style.css.tt2
Open-ILS/src/templates/opac/myopac/circ_history.tt2
Open-ILS/src/templates/opac/myopac/circs.tt2
Open-ILS/src/templates/opac/myopac/ebook_circs.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/myopac/ebook_holds.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/myopac/ebook_holds_ready.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/myopac/hold_history.tt2
Open-ILS/src/templates/opac/myopac/holds.tt2
Open-ILS/src/templates/opac/parts/config.tt2
Open-ILS/src/templates/opac/parts/ebook_api/avail.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/parts/ebook_api/avail_js.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/parts/ebook_api/base_js.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/parts/ebook_api/login_js.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/parts/header.tt2
Open-ILS/src/templates/opac/parts/js.tt2
Open-ILS/src/templates/opac/parts/misc_util.tt2
Open-ILS/src/templates/opac/parts/myopac/main_base.tt2
Open-ILS/src/templates/opac/parts/record/summary.tt2
Open-ILS/src/templates/opac/parts/result/table.tt2
Open-ILS/src/templates/opac/parts/topnav.tt2
Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/opac/ebook_api/relation.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/opac/ebook_api/session.js [new file with mode: 0644]

index 864304b..98d8749 100644 (file)
@@ -114,12 +114,12 @@ div.select-box-wrapper {
     display:inline-block;
 }
 
-#dashboard {
+#dashboard, #dashboard_e {
     margin-top: 1em;
     height: 3em;
 }
 
-#dashboard span.dash-align a {
+#dashboard span.dash-align a, #dashboard_e span.dash-align a {
     font-weight: bold;
     text-decoration: none;
 }
@@ -130,9 +130,9 @@ div.select-box-wrapper {
 
 #logout_link { left: 1px; }
 
-#dash_checked { color: [% css_colors.text_attention %]; }
-#dash_holds { color: [% css_colors.text_attention %]; }
-#dash_pickup { color: [% css_colors.text_goodnews %]; }
+#dash_checked, #dash_e_checked { color: [% css_colors.text_attention %]; }
+#dash_holds, #dash_e_holds { color: [% css_colors.text_attention %]; }
+#dash_pickup, #dash_e_pickup { color: [% css_colors.text_goodnews %]; }
 
 /*  
 #dash_fines { color: [% css_colors.text_badnews %]; }
index 7e6edc4..8e989c4 100644 (file)
         <div class="align">
             <a href='[% mkurl('circs',{},1) %]'>[% l("Current Items Checked Out") %]</a>
         </div>
+        [%- IF ebook_api.enabled %]
+        <div class="align">
+            <a href="[% mkurl('ebook_circs',{},1) %]">[% l("E-Items Currently Checked Out") %]</a>
+        </div>
+        [%- END %]
         <div class="align selected">
             <a href="#">[% l("Check Out History") %]</a>
         </div>
index 4715925..bd93d7b 100644 (file)
         <div class="align selected">
             <a href="#">[% l("Current Items Checked Out") %]</a>
         </div>
+        [%- IF ebook_api.enabled %]
+        <div class="align">
+            <a href="[% mkurl('ebook_circs',{},1) %]">[% l("E-Items Currently Checked Out") %]</a>
+        </div>
+        [%- END %]
         <div class="align">
             <a href="[% mkurl('circ_history',{},1) %]">[% l("Check Out History") %]</a>
         </div>
diff --git a/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_circs.tt2
new file mode 100644 (file)
index 0000000..4a75f23
--- /dev/null
@@ -0,0 +1,42 @@
+[%  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_circs"  %]
+<h3 class="sr-only">[% l('E-Items Currently Checked Out') %]</h3>
+<div id='myopac_checked_div'>
+
+    <div id="acct_checked_tabs">
+        <div class="align">
+            <a href="[% mkurl('circ_history',{},1) %]">[% l("Current Items Checked Out") %]</a>
+        </div>
+        <div class="align selected">
+            <a href="#">[% l("E-Items Currently Checked Out") %]</a>
+        </div>
+        <div class="align">
+            <a href="[% mkurl('circ_history',{},1) %]">[% l("Check Out History") %]</a>
+        </div>
+    </div>
+
+    <div class="header_middle">
+        <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_circs_main' class="hidden">
+        <table id="ebook_circs_main_table"
+            title="[% l('E-Items Currently Checked Out') %]"
+            class="table_no_border_space table_no_cell_pad item_list_padding">
+            <thead>
+            <tr>
+                <th>[% sort_head("sort_title", l("Title")) %]</th>
+                <th>[% sort_head("author", 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_holds.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_holds.tt2
new file mode 100644 (file)
index 0000000..f578a9f
--- /dev/null
@@ -0,0 +1,51 @@
+[%  PROCESS "opac/parts/header.tt2";
+    PROCESS "opac/parts/misc_util.tt2";
+    PROCESS "opac/parts/hold_status.tt2";
+    PROCESS "opac/parts/myopac/column_sort_support.tt2";
+    WRAPPER "opac/parts/myopac/base.tt2";
+    myopac_page = "ebook_holds";
+    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;
+%]
+<h3 class="sr-only">[% l('My E-Item Holds') %]</h3>
+<div id='myopac_holds_div'>
+
+    <div id="acct_holds_tabs">
+        <div class="align">
+            <a href='[% mkurl('holds', {}, ['limit','offset','available','sort','sort_type']) %]'>[% l("Items on Hold") %]</a>
+        </div>
+        <div class="align selected">
+            <a href='#'>[% l("E-Items on Hold") %]</a>
+        </div>
+        <div class="align">
+            <a href='[% mkurl('ebook_holds_ready', {}, ['limit','offset','available','sort','sort_type']) %]'>[% l("E-Items Ready for Checkout") %]</a>
+        </div>
+        <div class="align">
+            <a href='[% mkurl('hold_history', {}, ['limit','offset','available','sort','sort_type']) %]'>[% l("Holds History") %]</a>
+        </div>
+    </div>
+
+    <div class="header_middle">
+        <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_holds_main' class="hidden">
+        <table id="ebook_holds_main_table"
+            title="[% l('E-Items on Hold') %]"
+            class="table_no_border_space table_no_cell_pad item_list_padding">
+            <thead>
+            <tr>
+                <th>[% sort_head("sort_title", l("Title")) %]</th>
+                <th>[% sort_head("author", 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/myopac/ebook_holds_ready.tt2 b/Open-ILS/src/templates/opac/myopac/ebook_holds_ready.tt2
new file mode 100644 (file)
index 0000000..b93bc95
--- /dev/null
@@ -0,0 +1,50 @@
+[%  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_holds_ready";
+    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;
+%]
+<h3 class="sr-only">[% l('E-Items Ready for Checkout') %]</h3>
+<div id='myopac_holds_div'>
+
+    <div id="acct_holds_tabs">
+        <div class="align">
+            <a href='[% mkurl('holds', {}, ['limit','offset','available','sort','sort_type']) %]'>[% l("Items on Hold") %]</a>
+        </div>
+        <div class="align">
+            <a href='[% mkurl('ebook_holds', {}, ['limit','offset','available','sort','sort_type']) %]'>[% l("E-Items on Hold") %]</a>
+        </div>
+        <div class="align selected">
+            <a href='#'>[% l("E-Items Ready for Checkout") %]</a>
+        </div>
+        <div class="align">
+            <a href='[% mkurl('hold_history', {}, ['limit','offset','available','sort','sort_type']) %]'>[% l("Holds History") %]</a>
+        </div>
+    </div>
+
+    <div class="header_middle">
+        <span class="float-left">[% l('E-Items Ready for Checkout') %]</span>
+    </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_holds_main' class="hidden">
+        <table id="ebook_holds_main_table"
+            title="[% l('E-Items Ready for Checkout') %]"
+            class="table_no_border_space table_no_cell_pad item_list_padding">
+            <thead>
+            <tr>
+                <th>[% sort_head("sort_title", l("Title")) %]</th>
+                <th>[% sort_head("author", l("Author")) %]</th>
+                <th>[% l("Expire Date") %]</th>
+                <th>[% l("Actions") %]</th>
+            </tr>
+            </thead>
+            <tbody id="ebook_holds_main_table_body"></tbody>
+        </table>
+    </div>
+</div>
+[% END %]
index 9905e6c..16b4c02 100644 (file)
         <div class="align">
             <a href='[% mkurl('holds',{},['limit','offset']) %]'>[% l("Items on Hold") %]</a>
         </div>
+        [% IF ebook_api.enabled %]
+        <div class="align">
+            <a href='[% mkurl('ebook_holds', {}, ['limit','offset','available','sort','sort_type']) %]'>[% l("E-Items on Hold") %]</a>
+        </div>
+        <div class="align">
+            <a href='[% mkurl('ebook_holds_ready', {}, ['limit','offset','available','sort','sort_type']) %]'>[% l("E-Items Ready for Checkout") %]</a>
+        </div>
+        [% END %]
         <div class="align selected">
             <a href="#">[% l("Holds History") %]</a>
         </div>
index 7fc808c..1ba5c9c 100644 (file)
         <div class="align selected">
             <a href='#'>[% l("Items on Hold") %]</a>
         </div>
+        [% IF ebook_api.enabled %]
+        <div class="align">
+            <a href='[% mkurl('ebook_holds', {}, ['limit','offset','available','sort','sort_type']) %]'>[% l("E-Items on Hold") %]</a>
+        </div>
+        <div class="align">
+            <a href='[% mkurl('ebook_holds_ready', {}, ['limit','offset','available','sort','sort_type']) %]'>[% l("E-Items Ready for Checkout") %]</a>
+        </div>
+        [% END %]
         <div class="align">
             <a href='[% mkurl('hold_history', {}, ['limit','offset','available','sort','sort_type']) %]'>[% l("Holds History") %]</a>
         </div>
index 4e0e3b9..3af7bb7 100644 (file)
@@ -51,6 +51,17 @@ google_analytics.enabled = 'false';
 google_analytics.code = 'UA-9999999-99';
 
 ##############################################################################
+# Ebook API integration
+##############################################################################
+ebook_api.enabled = 'false';
+ebook_api.ebook_test.enabled = 'false';
+ebook_api.ebook_test.base_uris = [ 'http://example.com/ebookapi/t/' ];
+ebook_api.oneclickdigital.enabled = 'false';
+ebook_api.oneclickdigital.base_uris = [ 'http://example.oneclickdigital.com/Products/ProductDetail.aspx' ];
+ebook_api.overdrive.enabled = 'false';
+ebook_api.overdrive.base_uris = [ 'http://elm.lib.overdrive.com/' ];
+
+##############################################################################
 # Enable "Forgot your password?" prompt at login
 ##############################################################################
 reset_password = 'true';
diff --git a/Open-ILS/src/templates/opac/parts/ebook_api/avail.tt2 b/Open-ILS/src/templates/opac/parts/ebook_api/avail.tt2
new file mode 100644 (file)
index 0000000..f75a951
--- /dev/null
@@ -0,0 +1,49 @@
+[%-
+# Display holdings/availability info from ebook API
+#
+# We require the following info:
+# - rec_id: internal ID for this record (rec.id in search results, ctx.bre_id in record summary)
+# - 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 ctx.page == 'rresult';
+        ebook.rec_id = rec.id;
+    ELSE;
+        ebook.rec_id = ctx.bre_id;
+    END;
+
+# 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.
+-%]
+<div id="[% ebook.rec_id %]" class="ebook_avail hidden">
+    <div id="[% ebook.ebook_id %]" class="[% ebook.vendor %]_avail">
+        <table id="[% ebook.rec_id %]_ebook_holdings" class="result_holdings_table hidden">
+            <thead>
+                <tr>
+                    <th>[% l('Available Formats') %]</th>
+                    <th>[% l('Status') %]</th>
+                </tr>
+            <tbody>
+                <tr>
+                    <td id="[% ebook.rec_id %]_formats"></td>
+                    <td id="[% ebook.rec_id %]_status"></td>
+                </tr>
+            </tbody>
+        </table>
+    </div>
+</div>
+[% END %]
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
new file mode 100644 (file)
index 0000000..f70f115
--- /dev/null
@@ -0,0 +1,49 @@
+<script type="text/javascript">
+dojo.addOnLoad(function() {
+
+    // detect ebooks on current page for each vendor
+    dojo.forEach(vendor_list, function(v) {
+        var vendor = new Vendor(v);
+        var ebook_nodes = dojo.query("." + v + "_avail");
+        console.log('found ' + ebook_nodes.length + ' ebooks on this page');
+
+        // we have ebooks for this vendor, so let's get availability info etc.
+        if (ebook_nodes.length > 0) {
+            checkSession(v, function(v,ses) {
+                ebook_nodes.forEach(function(node) {
+                    var ebook = new Ebook(v, node.getAttribute("id"));
+                    ebook.rec_id = node.parentNode.getAttribute("id");
+                    vendor.ebooks.push(ebook);
+
+                    ebook.getHoldings( function(holdings) {
+                        if (typeof holdings.available !== 'undefined') {
+                            var avail = holdings.available;
+                            if (avail == 1) {
+                                node.innerHTML = 'This title is available online.';
+                            } else if (avail == 0) {
+                                node.innerHTML = 'This title is not currently available.';
+                            } else {
+                                console.log(ebook.id + ' has bad availability: ' + avail);
+                            }
+                        } else {
+                            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);
+                                });
+                                var status_node = dojo.byId(ebook.rec_id + '_status');
+                                var status_str = holdings.copies_available + ' of ' + holdings.copies_owned + ' available';
+                                status_node.innerHTML = status_str;
+                                dojo.removeClass(ebook.rec_id + '_ebook_holdings', "hidden");
+                            }
+                        }
+                        // unhide holdings/availability info now that it's populated
+                        removeClass(node.parentNode, "hidden");
+                    });
+                });
+            });
+        }
+    });
+
+});
+</script>
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
new file mode 100644 (file)
index 0000000..632e534
--- /dev/null
@@ -0,0 +1,71 @@
+[%
+# HTML display chunks
+progress_icon = '<img id="ebook_avail_spinner" src="/opac/images/progressbar_green.gif" alt="' _ l("Checking availability for this item...") _ '"/>'
+%]
+
+<script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/opac/ebook_api/session.js"></script>
+<script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/opac/ebook_api/ebook.js"></script>
+<script type="text/javascript">
+
+// translatable strings as JS variables
+var l_strings = {};
+l_strings.download = '[% l('Download') %]';
+l_strings.ready_for_checkout = '[% l('Ready for Checkout') %]';
+l_strings.suspended = '[% l('Suspended') %]';
+
+// give us cookies!
+dojo.require("dojo.cookie");
+
+// context org unit
+[% IF !ctx.page OR ctx.page != 'rresult';
+    PROCESS get_library;
+END %]
+var ou = [% loc_value %];
+
+// list of enabled vendors
+var vendor_list = [];
+[% IF ebook_api.ebook_test.enabled == 'true' %]
+vendor_list.push('ebook_test');
+[% END %]
+[% IF ebook_api.oneclickdigital.enabled == 'true' %]
+vendor_list.push('oneclickdigital');
+[% END %]
+[% IF ebook_api.overdrive.enabled == 'true' %]
+vendor_list.push('overdrive');
+[% END %]
+
+var cookie_registry = [ 'ebook_xact_cache' ];
+dojo.forEach(vendor_list, function(v) {
+    cookie_registry.push(v);
+});
+
+[% IF ctx.user %]
+// user- or login-specific vars
+var authtoken = '[% ctx.authtoken %]';
+var patron_id = '[% ctx.active_card %]'; // using barcode of active card as patron ID
+
+var myopac_page;
+[% IF myopac_page %]
+myopac_page = "[% myopac_page %]";
+[% END %]
+
+[% END %]
+
+// enforce removal of ebook API cookies on logout
+dojo.addOnLoad(function() {
+    var logout_handle = dojo.connect(dojo.byId('#logout_link'), 'onclick', function() {
+        dojo.forEach(cookie_registry, function(cookie) {
+            dojo.cookie(cookie, '', {path: '/', expires: '-1h'});
+        });
+        // When we switch to jQuery, use .one()
+        // instead of dojo's .connect() and .disconnect()
+        dojo.disconnect(logout_handle);
+    });
+});
+</script>
+
+[%- IF ctx.user %]
+<script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/opac/ebook_api/relation.js"></script>
+<script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/opac/ebook_api/loggedin.js"></script>
+[%- END %]
+
diff --git a/Open-ILS/src/templates/opac/parts/ebook_api/login_js.tt2 b/Open-ILS/src/templates/opac/parts/ebook_api/login_js.tt2
new file mode 100644 (file)
index 0000000..23ed256
--- /dev/null
@@ -0,0 +1,41 @@
+<script type="text/javascript">
+var vendors_requiring_password = [];
+
+[% IF !loc_value; PROCESS get_library; END; %]
+[% IF ebook_api.overdrive.enabled == 'true'
+    AND loc_value
+    AND ctx.get_org_setting(loc_value, 'ebook_api.overdrive.password_required') %]
+vendors_requiring_password.push('overdrive');
+[% END %]
+
+dojo.addOnLoad(function() {
+    var handle = dojo.connect(dojo.byId('#login-form-box'), 'onclick', function(evt) {
+        // disconnect this event since it's one-time-only
+        // (when we switch to jQuery, we can use .one() here)
+        dojo.disconnect(handle);
+
+        // we cache the username (and password) for now, but will
+        // replace that with the patron's active barcode later
+        vendors_requiring_password.forEach(function(v) {
+            if (vendor_list.includes(v)) {
+                checkSession(v, function(v,ses) {
+                    var username = dojo.byId('#username_field').value;
+                    var password = dojo.byId('#password_field').value;
+                    new OpenSRF.ClientSession('open-ils.ebook_api').request({
+                        method: 'open-ils.ebook_api.patron.cache_password',
+                        params: [ ses, password ],
+                        async: true,
+                        oncomplete: function(r) {
+                            var resp = r.recv();
+                            if (resp) {
+                                console.log('patron password has been cached');
+                                return;
+                            }
+                        }
+                    }).send();
+                });
+            }
+        });
+    });
+});
+</script>
index 8b60ba9..5b34c16 100644 (file)
         want_dojo = 1;
     END;
 
+    IF ebook_api.enabled == 'true';
+        want_dojo = 1;
+    END;
+
     # Especially useful for image 'alt' tags and link title tags,
     # where the content may need to be unique (making it longer)
     # but should not exceed 75 chars for ideal screen reader support.
index 5cfdebe..245af43 100644 (file)
     src="[% ctx.media_prefix %]/js/ui/default/opac/copyloc.js"></script>
 [% END %]
 
+[% 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 %]
+
 <!-- provide a JS friendly org unit hash -->
 <script type="text/javascript">
 var aou_hash = {
index d7ab819..69ca1b4 100644 (file)
             END;
         END;
 
+        IF ebook_api.overdrive.enabled == 'true';
+            FOR marc037 IN xml.findnodes('//*[@tag="037"]');
+                marc037_id = marc037.findnodes('./*[@code="a"]').textContent;
+                marc037_source = marc037.findnodes('./*[@code="b"]').textContent;
+                IF marc037_source.match('OverDrive') AND marc037_id;
+                    args.overdrive_id = marc037_id;
+                    LAST;
+                END;
+            END;
+        END;
+
         # Extract the 856 URLs that are not otherwise represented by asset.uri's
         args.online_res = [];
         FOR node IN xml.findnodes('//*[@tag="856" and @ind1="4" and (@ind2="0" or @ind2="1")]');
                         res.note = '';
                     END;
                     args.uris.push(res);
+
+                    IF ebook_api.ebook_test.enabled == 'true';
+                        IF !args.ebook_test_id;
+                            FOR base_uri IN ebook_api.ebook_test.base_uris;
+                                IF res.href.match(base_uri);
+                                    args.ebook_test_id = res.href.remove(base_uri);
+                                    LAST;
+                                END;
+                            END;
+                        END;
+                    END;
+
+                    IF ebook_api.oneclickdigital.enabled == 'true';
+                        # A record might conceivably have multiple OneClickdigital URIs,
+                        # but we use (the same) ISBN as the ebook ID in each case.
+                        IF !args.oneclickdigital_id;
+                            FOR base_uri IN ebook_api.oneclickdigital.base_uris;
+                                IF res.href.match(base_uri);
+                                    # found a OneClickdigital URI, let's grab our ID and move on
+                                    args.oneclickdigital_id = clean_isbn;
+                                    LAST;
+                                END;
+                            END;
+                        END;
+                    END;
+
+                    IF ebook_api.overdrive.enabled == 'true';
+                        # Ideally we already have an OverDrive record ID from MARC 037 (see above).
+                        # But for older records, it will be embedded in the URI in MARC 856.
+                        IF !args.overdrive_id;
+                            FOR base_uri IN ebook_api.overdrive.base_uris;
+                                IF res.href.match(base_uri);
+                                    args.overdrive_id = res.href.remove('^.*/ContentDetails.htm\?ID=');
+                                    LAST;
+                                END;
+                            END;
+                        END;
+                    END;
                 END;
+
                 NEXT;
             ELSE;
                 copies = volume.findnodes('./*[local-name()="copies"]/*[local-name()="copy"]');
index 16ad59b..705ba02 100644 (file)
                     <td class='td-right'>
                         <a href="[% mkurl(ctx.opac_root _ '/myopac/circs') %]"
                             title="[% l('View My Checked Out Items') %]">
-                            [% l("View All") %]
+                            [% l("Items Currently Checked out ([_1])", ctx.user_stats.checkouts.total_out) %]
+                        </a>
+                    </td>
+                    <td class="td-right hidden" id="acct_sum_ebook_circs">
+                        <a href="[% mkurl(ctx.opac_root _ '/myopac/circs?e_items') %]"
+                            title="[% l('View My Checked Out E-Items') %]">
+                            [% l("E-Items Currently Checked out") %] (<span id="acct_sum_ebook_circ_total">-</span>)
                         </a>
                     </td>
                 </tr>
                     <td class='td-right'>
                         <a href="[% mkurl(ctx.opac_root _ '/myopac/holds') %]"
                             title="[% l('View My Holds') %]">
-                            [% l('View All') %]
+                            [% l('Items Currently on Hold ([_1])', ctx.user_stats.holds.total) %]
+                        </a>
+                    </td>
+                    <td class="td-right hidden" id="acct_sum_ebook_holds">
+                        <a href="[% mkurl(ctx.opac_root _ '/myopac/holds?e_items') %]"
+                            title="[% l('View My E-Items On Hold') %]">
+                            [% l("E-Items Currently on Hold") %] (<span id="acct_sum_ebook_hold_total">-</span>)
                         </a>
                     </td>
                 </tr>
                     <td class='td-right'>
                         <a href="[% mkurl(ctx.opac_root _ '/myopac/holds', {available => 1}) %]"
                             title="[% l('View My Holds Ready for Pickup') %]">
-                            [% l('View All') %]
+                            [% l('Items ready for pickup ([_1])', ctx.user_stats.holds.ready) %]
+                        </a>
+                    </td>
+                    <td class="td-right hidden" id="acct_sum_ebook_holds_ready">
+                        <a href="[% mkurl(ctx.opac_root _ '/myopac/holds?e_items&available=1') %]"
+                            title="[% l('View My E-Items Ready for Pickup') %]">
+                            [% l("E-Items ready for pickup") %] (<span id="acct_sum_ebook_hold_ready_total">-</span>)
                         </a>
                     </td>
                 </tr>
index ff822d6..b125b03 100644 (file)
@@ -186,6 +186,11 @@ IF num_uris > 0;
     [%- END %]
     [%- IF num_uris > 1 %]</ul>[% END %]
 </div>
+[%
+IF ebook_api.enabled == 'true';
+    INCLUDE "opac/parts/ebook_api/avail.tt2";
+END;
+%]
 [%- END %]
 <div id="copy_hold_counts">
 [%-
index 5f3cbc3..ddfeacb 100644 (file)
@@ -381,6 +381,11 @@ END;
                                                              %]
                                                         [% END %] <!-- END detail_record_view -->
                                                     </table>
+                                                    [% 
+                                                        IF ebook_api.enabled == 'true';
+                                                            INCLUDE "opac/parts/ebook_api/avail.tt2";
+                                                        END;
+                                                    %]
                                                     [% PROCESS "opac/parts/result/copy_counts.tt2" %]
                                                     [% IF rec.user_circulated %]
                                                     <div class="result_item_circulated">
index 719ffa1..fc12a35 100644 (file)
                         %]</span> [% l("Fines") %]</a>
                 </span>
             </div>
+            <div id="dashboard_e" class="hidden">
+                <span class="dash-align">
+                    <a class="dash-link" href="[% mkurl(ctx.opac_root _ '/myopac/ebook_circs')
+                        %]"><span id="dash_e_checked">-</span> [% l("E-Items Checked Out") %]</a>
+                </span>
+                <span class="dash_divider">|</span>
+                <span class="dash-align">
+                    <a class="dash-link" href="[% mkurl(ctx.opac_root _ '/myopac/ebook_holds')
+                        %]"><span id="dash_e_holds">-</span> [% l("E-Items on Hold") %]</a>
+                </span>
+                <span class="dash_divider">|</span>
+                <span class="dash-align">
+                    <a class="dash-link" href="[% mkurl(ctx.opac_root _ '/myopac/ebook_holds_ready')
+                        %]"><span id="dash_e_pickup">-</span> [% l("E-Items Ready for Checkout") %]</a>
+                </span>
+            </div>
         </div>
         [% END %]
     </div>
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
new file mode 100644 (file)
index 0000000..f933b69
--- /dev/null
@@ -0,0 +1,48 @@
+// define our classes
+function Vendor(name) {
+    this.name = name;
+    this.ebooks = [];
+}
+
+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.avail;   // availability info for this title
+    this.holdings = {}; // holdings info
+}
+
+Ebook.prototype.getAvailability = function(callback) {
+    var ses = dojo.cookie(this.vendor);
+    new OpenSRF.ClientSession('open-ils.ebook_api').request({
+        method: 'open-ils.ebook_api.title.availability',
+        params: [ ses, this.id ],
+        async: true,
+        oncomplete: function(r) {
+            var resp = r.recv();
+            if (resp) {
+                console.log('availability response: ' + resp.content());
+                this.avail = resp.content();
+                return callback(resp.content());
+            }
+        }
+    }).send();
+}
+
+Ebook.prototype.getHoldings = function(callback) {
+    var ses = dojo.cookie(this.vendor);
+    new OpenSRF.ClientSession('open-ils.ebook_api').request({
+        method: 'open-ils.ebook_api.title.holdings',
+        params: [ ses, this.id ],
+        async: true,
+        oncomplete: function(r) {
+            var resp = r.recv();
+            if (resp) {
+                console.log('holdings response: ' + resp.content());
+                this.holdings = 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
new file mode 100644 (file)
index 0000000..725d2e6
--- /dev/null
@@ -0,0 +1,221 @@
+/*
+ * variables defined in base_js.tt2:
+ *
+ * ou
+ * vendor_list = [ 'ebook_test' ]
+ * authtoken
+ * patron_id (barcode)
+ * myopac_page
+ * progress_icon (probably not done right)
+ *
+ * base_js.tt2 also "imports" dojo.cookie and a bunch of ebook_api JS
+ */
+
+// Array of objects representing this patron's relationship with a specific vendor.
+var relations = [];
+
+// Transaction cache.
+var xacts = {
+    checkouts: [],
+    holds_pending: [],
+    holds_ready: []
+};
+
+dojo.addOnLoad(function() {
+
+    dojo.forEach(vendor_list, function(v) {
+        var rel = new Relation(v, patron_id);
+        relations.push(rel);
+    });
+
+    // Pull patron transaction info from cache (cookie), if available.
+    // Otherwise, do a live lookup against all enabled vendors.
+    if (dojo.cookie('ebook_xact_cache')) {
+        getCachedTransactions();
+        addTotalsToPage();
+        addTransactionsToPage();
+    } else {
+        console.log('retrieving patron transaction info for all vendors');
+        dojo.forEach(relations, function(rel) {
+            checkSession(rel.vendor, function(ses) {
+                rel.getTransactions( function(r) {
+                    addTransactionsToCache(r);
+                });
+            });
+        });
+    }
+
+});
+
+// Update current page with cross-vendor transaction totals.
+function addTotalsToPage() {
+    console.log('updating page with transaction totals');
+    updateDashboard();
+    updateMyAccountSummary();
+}
+
+// Update current page with detailed transaction info, where appropriate.
+function addTransactionsToPage() {
+    if (myopac_page) {
+        console.log('updating page with cached transaction details, if applicable');
+        if (myopac_page === 'ebook_circs')
+            updateCheckoutView();
+        if (myopac_page === 'ebook_holds')
+            updateHoldView();
+        if (myopac_page === 'ebook_holds_ready')
+            updateHoldReadyView();
+    }
+}
+        
+function updateDashboard() {
+    console.log('updating dashboard');
+    var total_checkouts = (typeof xacts.checkouts === 'undefined') ? '-' : xacts.checkouts.length;
+    var total_holds_pending = (typeof xacts.holds_pending === 'undefined') ? '-' : xacts.holds_pending.length;
+    var total_holds_ready = (typeof xacts.holds_ready === 'undefined') ? '-' : xacts.holds_ready.length;
+    // update totals
+    dojo.byId('dash_e_checked').innerHTML = total_checkouts;
+    dojo.byId('dash_e_holds').innerHTML = total_holds_pending;
+    dojo.byId('dash_e_pickup').innerHTML = total_holds_ready;
+    // unhide ebook dashboard
+    dojo.removeClass('dashboard_e', "hidden");
+}
+
+function updateMyAccountSummary() {
+    if (myopac_page === 'main') {
+        console.log('updating account summary');
+        var total_checkouts = (typeof xacts.checkouts === 'undefined') ? '-' : xacts.checkouts.length;
+        var total_holds_pending = (typeof xacts.holds_pending === 'undefined') ? '-' : xacts.holds_pending.length;
+        var total_holds_ready = (typeof xacts.holds_ready === 'undefined') ? '-' : xacts.holds_ready.length;
+        // update totals
+        dojo.byId('acct_sum_ebook_circ_total').innerHTML = total_checkouts;
+        dojo.byId('acct_sum_ebook_hold_total').innerHTML = total_holds_pending;
+        dojo.byId('acct_sum_ebook_hold_ready_total').innerHTML = total_holds_ready;
+        // unhide display elements
+        dojo.removeClass('acct_sum_ebook_circs', "hidden");
+        dojo.removeClass('acct_sum_ebook_holds', "hidden");
+        dojo.removeClass('acct_sum_ebook_holds_ready', "hidden");
+    }
+}
+
+function updateCheckoutView() {
+    if (xacts.checkouts.length < 1) {
+        dojo.removeClass('no_ebook_circs', "hidden");
+    } else {
+        dojo.forEach(xacts.checkouts, function(x) {
+            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);
+            dojo.create("td", { innerHTML: x.author }, tr);
+            dojo.create("td", { innerHTML: x.due_date }, tr);
+            dojo.create("td", { innerHTML: dl_link}, tr);
+            // TODO: more actions (renew, checkin)
+        });
+        dojo.addClass('no_ebook_circs', "hidden");
+        dojo.removeClass('ebook_circs_main', "hidden");
+    }
+}
+
+function updateHoldView() {
+    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);
+
+    if (holds.length < 1) {
+        dojo.removeClass('no_ebook_holds', "hidden");
+    } else {
+        dojo.forEach(holds, function(h) {
+            var hold_status;
+            if (h.is_ready) {
+                hold_status = l_strings.ready_for_checkout;
+            } else if (h.is_frozen) {
+                hold_status = l_strings.suspended;
+            } else {
+                hold_status = h.queue_position + ' / ' + h.queue_size;
+            }
+            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);
+            dojo.create("td", { innerHTML: hold_status }, tr);
+            dojo.create("td", null, tr); // TODO actions
+        });
+        dojo.addClass('no_ebook_holds', "hidden");
+        dojo.removeClass('ebook_holds_main', "hidden");
+    }
+}
+
+function updateHoldReadyView() {
+    var holds = xacts.holds_ready;
+    if (holds.length < 1) {
+        dojo.removeClass('no_ebook_holds', "hidden");
+    } else {
+        dojo.forEach(holds, function(h) {
+            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);
+            dojo.create("td", null, tr); // TODO actions
+        });
+        dojo.addClass('no_ebook_holds', "hidden");
+        dojo.removeClass('ebook_holds_main', "hidden");
+    }
+}
+
+// deserialize transactions from cache, returning them as a JS object
+function getCachedTransactions() {
+    console.log('retrieving cached transaction details');
+    var cache_obj;
+    var current_cache = dojo.cookie('ebook_xact_cache');
+    if (current_cache) {
+        cache_obj = JSON.parse(current_cache);
+        xacts.checkouts = cache_obj.checkouts;
+        xacts.holds_pending = cache_obj.holds_pending;
+        xacts.holds_ready = cache_obj.holds_ready;
+    }
+    return cache_obj;
+}
+
+// add a single vendor's transactions to transaction cache
+function addTransactionsToCache(rel) {
+    console.log('updating transaction cache');
+    var v = rel.vendor;
+    var updated_xacts = {
+        checkouts: [],
+        holds_pending: [],
+        holds_ready: []
+    };
+    // preserve any transactions with other vendors
+    dojo.forEach(xacts.checkouts, function(xact) {
+        if (xact.vendor !== v)
+            updated_xacts.checkouts.push(xact);
+    });
+    dojo.forEach(xacts.holds_pending, function(xact) {
+        if (xact.vendor !== v)
+            updated_xacts.holds_pending.push(xact);
+    });
+    dojo.forEach(xacts.holds_ready, function(xact) {
+        if (xact.vendor !== v)
+            updated_xacts.holds_ready.push(xact);
+    });
+    // add transactions from current vendor
+    dojo.forEach(rel.checkouts, function(xact) {
+        updated_xacts.checkouts.push(xact);
+    });
+    dojo.forEach(rel.holds_pending, function(xact) {
+        updated_xacts.holds_pending.push(xact);
+    });
+    dojo.forEach(rel.holds_ready, function(xact) {
+        updated_xacts.holds_ready.push(xact);
+    });
+    // TODO sort transactions by date
+    // save transactions to cache
+    xacts = updated_xacts;
+    var new_cache = JSON.stringify(xacts);
+    dojo.cookie('ebook_xact_cache', new_cache, {path: '/'});
+    // update current page
+    addTotalsToPage();
+    addTransactionsToPage();
+}
+
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
new file mode 100644 (file)
index 0000000..d423f33
--- /dev/null
@@ -0,0 +1,80 @@
+function Relation(vendor, patron_id) {
+    this.vendor = vendor;
+    this.patron_id = patron_id;
+    this.checkouts = [];
+    this.holds_pending = [];
+    this.holds_ready = [];
+}
+
+Relation.prototype.getCheckouts = function(callback) {
+    var ses = dojo.cookie(this.vendor);
+    var rel = this;
+    new OpenSRF.ClientSession('open-ils.ebook_api').request({
+        method: 'open-ils.ebook_api.patron.get_checkouts',
+        params: [ authtoken, ses, rel.patron_id ],
+        async: false,
+        oncomplete: function(r) {
+            var resp = r.recv();
+            if (resp) {
+                console.log('retrieved checkouts for patron');
+                rel.checkouts = [];
+                dojo.forEach(resp.content(), function(checkout) {
+                    rel.checkouts.push(checkout);
+                });
+                return callback(rel);
+            }
+        }
+    }).send();
+}
+
+Relation.prototype.getHolds = function(callback) {
+    var ses = dojo.cookie(this.vendor);
+    var rel = this;
+    new OpenSRF.ClientSession('open-ils.ebook_api').request({
+        method: 'open-ils.ebook_api.patron.get_holds',
+        params: [ authtoken, ses, rel.patron_id ],
+        async: false,
+        oncomplete: function(r) {
+            var resp = r.recv();
+            if (resp) {
+                console.log('retrieved holds for patron');
+                dojo.forEach(resp.content(), function(hold) {
+                    if (hold.is_ready === 1) {
+                        rel.holds_ready.push(hold);
+                    } else {
+                        rel.holds_pending.push(hold);
+                    }
+                });
+                return callback(rel);
+            }
+        }
+    }).send();
+}
+
+Relation.prototype.getTransactions = function(callback) {
+    var ses = dojo.cookie(this.vendor);
+    var rel = this;
+    new OpenSRF.ClientSession('open-ils.ebook_api').request({
+        method: 'open-ils.ebook_api.patron.get_transactions',
+        params: [ authtoken, ses, rel.patron_id ],
+        async: false,
+        oncomplete: function(r) {
+            var resp = r.recv();
+            if (resp) {
+                console.log('retrieved holds for patron');
+                var xacts = resp.content();
+                dojo.forEach(xacts.checkouts, function(checkout) {
+                    rel.checkouts.push(checkout);
+                });
+                dojo.forEach(xacts.holds, function(hold) {
+                    if (hold.is_ready === 1) {
+                        rel.holds_ready.push(hold);
+                    } else {
+                        rel.holds_pending.push(hold);
+                    }
+                });
+                return callback(rel);
+            }
+        }
+    }).send();
+}
diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/session.js b/Open-ILS/web/js/ui/default/opac/ebook_api/session.js
new file mode 100644 (file)
index 0000000..914abe6
--- /dev/null
@@ -0,0 +1,40 @@
+// initialize an API session
+// XXX Are there any cases where checkSession does not suffice for this?
+function startSession(vendor, callback) {
+    console.log('starting ebook API session for ' + vendor);
+    new OpenSRF.ClientSession('open-ils.ebook_api').request({
+        method: 'open-ils.ebook_api.start_session',
+        params: [ vendor, ou ],
+        async: false, // XXX
+        oncomplete: function(r) {
+            var resp = r.recv();
+            if (resp) {
+                var ses = resp.content();
+                dojo.cookie(vendor, ses, {path: '/'});
+                return callback(vendor,ses);
+            }
+        }
+    }).send();
+}
+
+// validate or initialize API session
+// (check_session method will fallback to start_session if no session ID is provided)
+function checkSession(vendor, callback) {
+    var ses = dojo.cookie(vendor) || null;
+    if (ses == null)
+        return startSession(vendor,callback);
+    console.log('checking ebook API session for ' + vendor);
+    new OpenSRF.ClientSession('open-ils.ebook_api').request({
+        method: 'open-ils.ebook_api.check_session',
+        params: [ ses, vendor, ou ],
+        async: false, // XXX
+        oncomplete: function(r) {
+            var resp = r.recv();
+            if (resp) {
+                var new_ses = resp.content();
+                dojo.cookie(vendor, new_ses, {path: '/'});
+                return callback(vendor,new_ses);
+            }
+        }
+    }).send();
+}