From 556d1ceb81ff25ce9aa6c0938de1b6ff84a36871 Mon Sep 17 00:00:00 2001 From: Jeff Davis Date: Tue, 7 Feb 2017 15:27:48 -0800 Subject: [PATCH] LP#1541559: ebook API handler for OneClickdigital Signed-off-by: Jeff Davis Signed-off-by: Kathy Lussier --- Open-ILS/src/perlmods/MANIFEST | 1 + .../Application/EbookAPI/OneClickdigital.pm | 305 ++++++++++++++++++ .../t/23-OpenILS-Application-EbookAPI.t | 3 +- Open-ILS/src/sql/Pg/950.data.seed-values.sql | 36 +++ ....org-setting.ebook-api-oneclickdigital.sql | 42 +++ 5 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OneClickdigital.pm create mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX.data.org-setting.ebook-api-oneclickdigital.sql diff --git a/Open-ILS/src/perlmods/MANIFEST b/Open-ILS/src/perlmods/MANIFEST index d3f1deb42c..594e944cf4 100644 --- a/Open-ILS/src/perlmods/MANIFEST +++ b/Open-ILS/src/perlmods/MANIFEST @@ -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 index 0000000000..093b6570a8 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OneClickdigital.pm @@ -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; + } +} + diff --git a/Open-ILS/src/perlmods/t/23-OpenILS-Application-EbookAPI.t b/Open-ILS/src/perlmods/t/23-OpenILS-Application-EbookAPI.t index 44a1d42298..d5f51212a8 100644 --- a/Open-ILS/src/perlmods/t/23-OpenILS-Application-EbookAPI.t +++ b/Open-ILS/src/perlmods/t/23-OpenILS-Application-EbookAPI.t @@ -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' ); } diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index 08f8877a2b..bdd70d7c08 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -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 index 0000000000..754dac0a6a --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.org-setting.ebook-api-oneclickdigital.sql @@ -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; + -- 2.43.2