LP#1541559: ebook API handler for OneClickdigital
authorJeff Davis <jdavis@sitka.bclibraries.ca>
Tue, 7 Feb 2017 23:27:48 +0000 (15:27 -0800)
committerKathy Lussier <klussier@masslnc.org>
Mon, 20 Feb 2017 23:54:38 +0000 (18:54 -0500)
Signed-off-by: Jeff Davis <jdavis@sitka.bclibraries.ca>
Signed-off-by: Kathy Lussier <klussier@masslnc.org>
Open-ILS/src/perlmods/MANIFEST
Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OneClickdigital.pm [new file with mode: 0644]
Open-ILS/src/perlmods/t/23-OpenILS-Application-EbookAPI.t
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.org-setting.ebook-api-oneclickdigital.sql [new file with mode: 0644]

index d3f1deb..594e944 100644 (file)
@@ -42,6 +42,7 @@ lib/OpenILS/Application/Collections.pm
 lib/OpenILS/Application/EbookAPI.pm
 lib/OpenILS/Application/EbookAPI/Test.pm
 lib/OpenILS/Application/EbookAPI/OverDrive.pm
+lib/OpenILS/Application/EbookAPI/OneClickdigital.pm
 lib/OpenILS/Application/Fielder.pm
 lib/OpenILS/Application/PermaCrud.pm
 lib/OpenILS/Application/Proxy.pm
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OneClickdigital.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OneClickdigital.pm
new file mode 100644 (file)
index 0000000..093b657
--- /dev/null
@@ -0,0 +1,305 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2015 BC Libraries Cooperative
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+package OpenILS::Application::EbookAPI::OneClickdigital;
+
+use strict;
+use warnings;
+
+use OpenILS::Application;
+use OpenILS::Application::EbookAPI;
+use base qw/OpenILS::Application::EbookAPI/;
+use OpenSRF::AppSession;
+use OpenSRF::EX qw(:try);
+use OpenSRF::Utils::SettingsClient;
+use OpenSRF::Utils::Logger qw($logger);
+use OpenSRF::Utils::Cache;
+use OpenILS::Application::AppUtils;
+use Data::Dumper;
+
+sub new {
+    my( $class, $args ) = @_;
+    $class = ref $class || $class;
+    return bless $args, $class;
+}
+
+sub ou {
+    my $self = shift;
+    return $self->{ou};
+}
+
+sub vendor {
+    my $self = shift;
+    return $self->{vendor};
+}
+
+sub session_id {
+    my $self = shift;
+    return $self->{session_id};
+}
+
+sub base_uri {
+    my $self = shift;
+    return $self->{base_uri};
+}
+
+sub library_id {
+    my $self = shift;
+    return $self->{library_id};
+}
+
+sub basic_token {
+    my $self = shift;
+    return $self->{basic_token};
+}
+
+sub patron_id {
+    my $self = shift;
+    return $self->{patron_id};
+}
+
+sub initialize {
+    my $self = shift;
+    my $ou = $self->{ou};
+
+    $self->{base_uri} = 'https://api.oneclickdigital.us/v1'; # TODO pull from org setting?
+
+    my $library_id = OpenILS::Application::AppUtils->ou_ancestor_setting_value($ou, 'ebook_api.oneclickdigital.library_id');
+    if ($library_id) {
+        $self->{library_id} = $library_id;
+    } else {
+        $logger->error("EbookAPI: no OneClickdigital library ID found for org unit $ou");
+        return;
+    }
+
+    my $basic_token = OpenILS::Application::AppUtils->ou_ancestor_setting_value($ou, 'ebook_api.oneclickdigital.basic_token');
+    if ($basic_token) {
+        $self->{basic_token} = $basic_token;
+    } else {
+        $logger->error("EbookAPI: no OneClickdigital basic token found for org unit $ou");
+        return;
+    }
+
+    return $self;
+
+}
+
+# OneClickdigital API does not require separate client auth;
+# we just need to include our basic auth token in requests
+sub do_client_auth {
+    my $self = shift;
+    return;
+}
+
+# retrieve OneClickdigital patron ID (if any) based on patron barcode
+# GET http://api.oneclickdigital.us/v1/rpc/libraries/{libraryID}/patrons/{barcode}
+sub do_patron_auth {
+    my ($self, $barcode) = @_;
+    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/rpc/libraries/$library_id/patrons/$barcode"
+    };
+    my $res = $self->request($req, $session_id);
+    # TODO distinguish between unregistered patrons and patron auth failure
+    if (defined ($res) && $res->{content}->{patronId}) {
+        return $res->{content}->{patronId};
+    }
+    $logger->error("EbookAPI: no OneClickdigital patron ID found for barcode $barcode");
+    return;
+}
+
+# does this title have available "copies"? y/n
+# GET http://api.oneclickdigital.us/v1/libraries/{libraryID}/media/{isbn}/availability
+sub do_availability_lookup {
+    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/media/$isbn/availability"
+    };
+    my $res = $self->request($req, $session_id);
+    if (defined ($res)) {
+        $logger->info("EbookAPI: received availability response for ISBN $isbn: " . Dumper $res);
+        return $res->{content}->{availability};
+    } else {
+        $logger->error("EbookAPI: could not retrieve OneClickdigital availability for ISBN $isbn");
+        return;
+    }
+}
+
+# OneClickdigital API does not support detailed holdings lookup,
+# so we return basic availability information.
+sub do_holdings_lookup {
+    my ($self, $isbn) = @_;
+    my $avail = $self->do_availability_lookup($isbn);
+    return { available => $avail };
+}
+
+# checkout an item to a patron
+# item is identified by ISBN, patron ID is their barcode
+# POST //api.{domain}/v1/libraries/{libraryId}/patrons/{patronId}/checkouts/{isbn}
+sub checkout {
+    my ($self, $isbn, $patron_id) = @_;
+    my $base_uri = $self->{base_uri};
+    my $library_id = $self->{library_id};
+    my $session_id = $self->{session_id};
+    my $req = {
+        method => 'POST',
+        uri    => "$base_uri/libraries/$library_id/patrons/$patron_id/checkouts/$isbn"
+    };
+    my $res = $self->request($req, $session_id);
+
+    # TODO: more sophisticated response handling
+    # HTTP 200 response indicates success, HTTP 409 indicates checkout limit reached
+    if (defined ($res)) {
+        if ($res->{is_success}) {
+            return {
+                xact_id => $res->{content}->{transactionId},
+                due_date => $res->{content}->{expiration}
+            };
+        } else {
+            $logger->error("EbookAPI: checkout failed for OneClickdigital title $isbn");
+            return { error_msg => $res->{content} };
+        }
+    } else {
+        $logger->error("EbookAPI: no response received from OneClickdigital server");
+        return;
+    }
+}
+
+# renew a checked-out item
+# item id = ISBN, patron id = barcode
+# PUT //api.{domain}/v1/libraries/{libraryId}/patrons/{patronId}/checkouts/{isbn}
+sub renew {
+    my ($self, $isbn, $patron_id) = @_;
+    my $base_uri = $self->{base_uri};
+    my $library_id = $self->{library_id};
+    my $session_id = $self->{session_id};
+    my $req = {
+        method => 'PUT',
+        uri    => "$base_uri/libraries/$library_id/patrons/$patron_id/checkouts/$isbn"
+    };
+    my $res = $self->request($req, $session_id);
+
+    # TODO: more sophisticated response handling
+    # HTTP 200 response indicates success
+    if (defined ($res)) {
+        if ($res->{is_success}) {
+            return {
+                xact_id => $res->{content}->{transactionId},
+                due_date => $res->{content}->{expiration}
+            };
+        } else {
+            $logger->error("EbookAPI: renewal failed for OneClickdigital title $isbn");
+            return { error_msg => $res->{content} };
+        }
+    } else {
+        $logger->error("EbookAPI: no response received from OneClickdigital server");
+        return;
+    }
+}
+
+# checkin a checked-out item
+# item id = ISBN, patron id = barcode
+# XXX API docs indicate that a bearer token is required!
+# DELETE //api.{domain}/v1/libraries/{libraryId}/patrons/{patronId}/checkouts/{isbn}
+sub checkin {
+}
+
+sub place_hold {
+}
+
+sub cancel_hold {
+}
+
+# GET //api.{domain}/v1/libraries/{libraryId}/patrons/{patronId}/checkouts/all
+sub get_patron_checkouts {
+    my ($self, $patron_id) = @_;
+    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/patrons/$patron_id/checkouts/all"
+    };
+    my $res = $self->request($req, $session_id);
+
+    my $checkouts = [];
+    if (defined ($res)) {
+        $logger->info("EbookAPI: received response for OneClickdigital checkouts: " . Dumper $res);
+        foreach my $checkout (@{$res->{content}}) {
+            push @$checkouts, {
+                xact_id => $checkout->{transactionID},
+                title_id => $checkout->{isbn},
+                due_date => $checkout->{expiration},
+                download_url => $checkout->{downloadURL},
+                title => $checkout->{title},
+                author => $checkout->{authors}
+            };
+        };
+        $logger->info("EbookAPI: retrieved " . scalar(@$checkouts) . " OneClickdigital checkouts for patron $patron_id");
+        $self->{checkouts} = $checkouts;
+        return $self->{checkouts};
+    } else {
+        $logger->error("EbookAPI: failed to retrieve OneClickdigital checkouts for patron $patron_id");
+        return;
+    }
+}
+
+# GET //api.{domain}/v1/libraries/{libraryId}/patrons/{patronId}/holds/all
+sub get_patron_holds {
+    my ($self, $patron_id) = @_;
+    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/patrons/$patron_id/holds/all"
+    };
+    my $res = $self->request($req, $session_id);
+
+    my $holds = [];
+    if (defined ($res)) {
+        $logger->info("EbookAPI: received response for OneClickdigital holds: " . Dumper $res);
+        foreach my $hold (@{$res->{content}}) {
+            push @$holds, {
+                xact_id => $hold->{transactionID},
+                title_id => $hold->{isbn},
+                expire_date => $hold->{expiration},
+                title => $hold->{title},
+                author => $hold->{authors},
+                # XXX queue position/size and pending vs ready info not available via API
+                queue_position => '-',
+                queue_size => '-',
+                is_ready => 0
+            };
+        };
+        $logger->info("EbookAPI: retrieved " . scalar(@$holds) . " OneClickdigital holds for patron $patron_id");
+        $self->{holds} = $holds;
+        return $self->{holds};
+    } else {
+        $logger->error("EbookAPI: failed to retrieve OneClickdigital holds for patron $patron_id");
+        return;
+    }
+}
+
index 44a1d42..d5f5121 100644 (file)
@@ -1,10 +1,11 @@
 #!perl -T
 
-use Test::More tests => 3;
+use Test::More tests => 4;
 
 BEGIN {
     use_ok( 'OpenILS::Application::EbookAPI' );
     use_ok( 'OpenILS::Application::EbookAPI::Test' );
     use_ok( 'OpenILS::Application::EbookAPI::OverDrive' );
+    use_ok( 'OpenILS::Application::EbookAPI::OneClickdigital' );
 }
 
index 08f8877..bdd70d7 100644 (file)
@@ -16800,3 +16800,39 @@ VALUES (
     'ebook_api',
     'bool'
 );
+
+INSERT INTO config.org_unit_setting_type
+    (name, label, description, grp, datatype) 
+VALUES (
+    'ebook_api.oneclickdigital.library_id',
+    oils_i18n_gettext(
+        'ebook_api.oneclickdigital.library_id',
+        'OneClickdigital Library ID',
+        'coust',
+        'label'
+    ),
+    oils_i18n_gettext(
+        'ebook_api.oneclickdigital.library_id',
+        'Identifier assigned to this library by OneClickdigital',
+        'coust',
+        'description'
+    ),
+    'ebook_api',
+    'string'
+),(
+    'ebook_api.oneclickdigital.basic_token',
+    oils_i18n_gettext(
+        'ebook_api.oneclickdigital.basic_token',
+        'OneClickdigital Basic Token',
+        'coust',
+        'label'
+    ),
+    oils_i18n_gettext(
+        'ebook_api.oneclickdigital.basic_token',
+        'Basic token for client authentication with OneClickdigital API (supplied by OneClickdigital)',
+        'coust',
+        'description'
+    ),
+    'ebook_api',
+    'string'
+);
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.org-setting.ebook-api-oneclickdigital.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.org-setting.ebook-api-oneclickdigital.sql
new file mode 100644 (file)
index 0000000..754dac0
--- /dev/null
@@ -0,0 +1,42 @@
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT INTO config.org_unit_setting_type
+    (name, label, description, grp, datatype) 
+VALUES (
+    'ebook_api.oneclickdigital.library_id',
+    oils_i18n_gettext(
+        'ebook_api.oneclickdigital.library_id',
+        'OneClickdigital Library ID',
+        'coust',
+        'label'
+    ),
+    oils_i18n_gettext(
+        'ebook_api.oneclickdigital.library_id',
+        'Identifier assigned to this library by OneClickdigital',
+        'coust',
+        'description'
+    ),
+    'ebook_api',
+    'string'
+),(
+    'ebook_api.oneclickdigital.basic_token',
+    oils_i18n_gettext(
+        'ebook_api.oneclickdigital.basic_token',
+        'OneClickdigital Basic Token',
+        'coust',
+        'label'
+    ),
+    oils_i18n_gettext(
+        'ebook_api.oneclickdigital.basic_token',
+        'Basic token for client authentication with OneClickdigital API (supplied by OneClickdigital)',
+        'coust',
+        'description'
+    ),
+    'ebook_api',
+    'string'
+);
+
+COMMIT;
+