@@ -, +, @@ --- Koha/Illrequest/Availability.pm | 114 ++++++++++++++ ill/ill-requests.pl | 55 ++++++- .../intranet-tmpl/prog/css/src/staff-global.scss | 4 + .../prog/en/includes/ill-availability-table.inc | 17 +++ .../prog/en/modules/ill/ill-requests.tt | 43 ++++++ .../prog/en/modules/plugins/plugins-home.tt | 1 + .../intranet-tmpl/prog/js/ill-availability.js | 161 ++++++++++++++++++++ koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss | 9 ++ .../en/includes/ill-availability-table.inc | 17 +++ .../bootstrap/en/modules/opac-illrequests.tt | 40 +++++ .../opac-tmpl/bootstrap/js/ill-availability.js | 163 +++++++++++++++++++++ opac/opac-illrequests.pl | 41 ++++++ 12 files changed, 659 insertions(+), 6 deletions(-) create mode 100644 Koha/Illrequest/Availability.pm create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/ill-availability-table.inc create mode 100644 koha-tmpl/intranet-tmpl/prog/js/ill-availability.js create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/includes/ill-availability-table.inc create mode 100644 koha-tmpl/opac-tmpl/bootstrap/js/ill-availability.js --- a/Koha/Illrequest/Availability.pm +++ a/Koha/Illrequest/Availability.pm @@ -0,0 +1,114 @@ +package Koha::Illrequest::Availability; + +# Copyright 2019 PTFS Europe Ltd +# +# This file is part of Koha. +# +# Koha 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 3 of the License, or (at your option) any later +# version. +# +# Koha 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 Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use JSON; +use MIME::Base64 qw( encode_base64 ); +use URI::Escape qw ( uri_escape ); + +use Koha::Plugins; + +=head1 NAME + +Koha::Illrequest::Availability - Koha ILL Availability Searching + +=head1 SYNOPSIS + +Object-oriented class that provides availability searching via +availability plugins + +=head1 DESCRIPTION + +This class provides the ability to identify and fetch API services +that can be used to search for item availability + +=head1 API + +=head2 Class Methods + +=head3 new + + my $availability = Koha::Illrequest::Logger->new($metadata); + +Create a new Koha::Illrequest::Availability object. +We also store the metadata to be used for searching + +=cut + +sub new { + my ( $class, $metadata ) = @_; + my $self = {}; + + $self->{metadata} = $metadata; + + bless $self, $class; + + return $self; +} + +=head3 get_services + + my $services = Koha::Illrequest::Availability->get_services(); + +Given our metadata, iterate plugins with the right method and +check if they can service our request and, if so, return an arrayref +of services + +=cut + +sub get_services { + my ( $self ) = @_; + + my @candidates = Koha::Plugins->new()->GetPlugins({ + method => 'ill_availability_services' + }); + my @services = (); + foreach my $plugin(@candidates) { + my $valid_service = $plugin->ill_availability_services( + $self->{metadata} + ); + push @services, $valid_service if $valid_service; + } + + return \@services; +} + +=head3 prep_metadata + + my $prepared = Koha::Illrequest::Availability->prep_metadata($metadata); + +Given our metadata, return a string representing that metadata that can be +passed in a URL (encoded in JSON then Base64 encoded) + +=cut + +sub prep_metadata { + my ( $self, $metadata ) = @_; + + return uri_escape(encode_base64(encode_json($metadata))); +} + +=head1 AUTHOR + +Andrew Isherwood + +=cut + +1; --- a/ill/ill-requests.pl +++ a/ill/ill-requests.pl @@ -26,11 +26,13 @@ use C4::Output; use Koha::AuthorisedValues; use Koha::Illcomment; use Koha::Illrequests; +use Koha::Illrequest::Availability; use Koha::Libraries; use Koha::Token; use Try::Tiny; use URI::Escape; +use JSON; our $cgi = CGI->new; my $illRequests = Koha::Illrequests->new; @@ -81,12 +83,53 @@ if ( $backends_available ) { } elsif ( $op eq 'create' ) { # We're in the process of creating a request my $request = Koha::Illrequest->new->load_backend( $params->{backend} ); - my $backend_result = $request->backend_create($params); - $template->param( - whole => $backend_result, - request => $request - ); - handle_commit_maybe($backend_result, $request); + # Does this backend enable us to insert an availability stage and should + # we? If not, proceed as normal. + if ( + C4::Context->preference("ILLCheckAvailability") && + $request->_backend_capability( + 'should_display_availability', + $params + ) && + # If the user has elected to continue with the request despite + # having viewed availability info, this flag will be set + !$params->{checked_availability} + ) { + # Establish which of the installed availability providers + # can service our metadata + my $availability = Koha::Illrequest::Availability->new($params); + my $services = $availability->get_services(); + if (scalar @{$services} > 0) { + # Modify our method so we use the correct part of the + # template + $op = 'availability'; + $params->{method} = 'availability'; + delete $params->{stage}; + # Prepare the metadata we're sending them + my $metadata = $availability->prep_metadata($params); + $template->param( + whole => $params, + metadata => $metadata, + services_json => scalar encode_json($services), + services => $services + ); + } else { + # No services can process this metadata, so continue as normal + my $backend_result = $request->backend_create($params); + $template->param( + whole => $backend_result, + request => $request + ); + handle_commit_maybe($backend_result, $request); + } + } else { + my $backend_result = $request->backend_create($params); + $template->param( + whole => $backend_result, + request => $request + ); + handle_commit_maybe($backend_result, $request); + } } elsif ( $op eq 'migrate' ) { # We're in the process of migrating a request --- a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss +++ a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss @@ -3967,6 +3967,10 @@ input.renew { } } +.ill_availability_sourcename { + margin-top: 20px; +} + #stockrotation { h3 { margin: 30px 0 10px 0; --- a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-availability-table.inc +++ a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-availability-table.inc @@ -0,0 +1,17 @@ +
+
[% service.name %]
+ + + + + + + + + + + + + +
SourceTitleAuthorISBNISSNDate
+
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt @@ -518,6 +518,40 @@ [% INCLUDE 'ill-list-table.inc' %] + [% ELSIF query_type == 'availability' %] + +

Availability

+
+

Displaying availability results

+
+ [% FOREACH key IN whole.keys %] + [% value = whole.$key %] + [% IF key != 'method' && key != 'custom_key' && key != 'custom_value' %] + + [% END %] + [% END %] + [% custom_keys = whole.custom_key.split('\0') %] + [% custom_values = whole.custom_value.split('\0') %] + [% i = 0 %] + [% FOREACH custom_key IN custom_keys %] + + + [% i = i + 1 %] + [% END %] + + + +
+ If you can't find what you are looking for, you can + or + cancel your request +
+
+ [% FOR service IN services %] +

[% service.plugin %]

+ [% INCLUDE 'ill-availability-table.inc' service=service %] + [% END %] +
[% ELSE %] [% PROCESS $whole.template %] @@ -539,11 +573,20 @@ var prefilters = '[% prefilters | $raw %]'; // Set column settings var columns_settings = [% ColumnsSettings.GetColumns( 'illrequests', 'ill-requests', 'ill-requests', 'json' ) %]; + [% IF services_json.length > 0 %] + var services = [% services_json | $raw %]; + [% ELSE %] + var services = []; + [% END %] + [% IF metadata.length > 0 %] + var metadata = "[% metadata | $raw %]"; + [% END %] [% IF query_type == 'illlist' %] [% INCLUDE 'ill-list-table-strings.inc' %] [% Asset.js("js/ill-list-table.js") | $raw %] [% END %] + [% Asset.js("js/ill-availability.js") | $raw %] [% END %] [% TRY %] --- a/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-home.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-home.tt @@ -36,6 +36,7 @@
  • View MARC conversion plugins
  • View online payment plugins
  • View intranet catalog biblio enhancement plugins
  • +
  • View ILL availability plugins
  • --- a/koha-tmpl/intranet-tmpl/prog/js/ill-availability.js +++ a/koha-tmpl/intranet-tmpl/prog/js/ill-availability.js @@ -0,0 +1,161 @@ +$(document).ready(function() { + + // In case the source doesn't supply data required for DT to calculate + // pagination, we need to do it ourselves + var ownPagination = false; + var directionSet = false; + var start = 0; + var forward = true; // true == forward, false == backwards + // Arbitrary starting value, it will be corrected by the first + // page of results + var pageSize = 20; + + var tableTmpl = { + ajax: { + cache: true, // Prevent DT appending a "_" cache param + }, + columns: [ + // defaultContent prevents DT from choking if + // the API response doesn't return a column + { + title: 'Source', + data: 'source', + defaultContent: '' + }, + { + data: 'title', + defaultContent: '' + }, + { + data: 'author', + defaultContent: '' + }, + { + data: 'isbn', + defaultContent: '' + }, + { + data: 'issn', + defaultContent: '' + }, + { + data: 'date', + defaultContent: '' + } + ] + }; + + // render functions don't get copied across when we make a dereferenced + // copy of them, so we have to reattach them once we have a copy + // Here we store them + var renders = { + title: function(data, type, row) { + return row.url ? + ''+row.title+'' : + row.title; + } + }; + + services.forEach(function(service) { + // Create a deferenced copy of our table definition object + var tableDef = JSON.parse(JSON.stringify(tableTmpl)); + // Iterate the table's columns array and add render functions + // as necessary + tableDef.columns.forEach(function(column) { + if (renders[column.data]) { + column.render = renders[column.data]; + } + }); + tableDef.ajax.dataSrc = function(data) { + var results = data.results.search_results; + // The source appears to be returning it's own pagination + // data + if ( + results.hasOwnProperty('recordsFiltered') || + results.hasOwnProperty('recordsTotal') + ) { + return results; + } + // Set up our own pagination values based on what we just + // got back + ownPagination = true; + directionSet = false; + pageSize = results.length; + // These values are completely arbitrary, but they enable + // us to display pagination links + data.recordsFiltered = 5000, + data.recordsTotal = 5000; + + return results; + }; + tableDef.ajax.data = function(data) { + // Datatables sends a bunch of superfluous params + // that we don't want to litter our API schema + // with, so just remove them from the request + if (data.hasOwnProperty('columns')) { + delete data.columns; + } + if (data.hasOwnProperty('draw')) { + delete data.draw; + } + if (data.hasOwnProperty('order')) { + delete data.order; + } + if (data.hasOwnProperty('search')) { + delete data.search; + } + // If we're handling our own pagination, set the properties + // that DT will send in the request + if (ownPagination) { + start = forward ? start + pageSize : start - pageSize; + data.start = start; + data.length = pageSize; + } + }; + // Add any datatables config options passed from the service + // to the table definition + tableDef.ajax.url = service.endpoint + metadata; + if (service.hasOwnProperty('datatablesConfig')) { + var conf = service.datatablesConfig; + for (var key in conf) { + // The config from the service definition comes from a Perl + // hashref, therefore can't contain true/false, so we + // special case it + if (conf.hasOwnProperty(key)) { + if (conf[key] == 'false') { + // Special case false values + tableDef[key] = false; + } else if (conf[key] == 'true') { + // Special case true values + tableDef[key] = true; + } else { + // Copy the property value + tableDef[key] = conf[key]; + } + } + } + } + // Create event watchers for the "next" and "previous" pagination + // links, this enables us to set the direction the next request is + // going in when we're doing our own pagination. We use "hover" + // because the click event is caught after the request has been + // sent + tableDef.drawCallback = function() { + $('.paginate_button.next:not(.disabled)', + this.api().table().container() + ).on('hover', function() { + forward = true; + directionSet = true; + }); + $('.paginate_button.previous:not(.disabled)', + this.api().table().container() + ).on('hover', function() { + forward = false; + directionSet = true; + }); + } + // Initialise the table + KohaTable(service.id, tableDef); + }); + +}); --- a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss +++ a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss @@ -3038,6 +3038,15 @@ button.closebtn { .dropdown:hover .dropdown-menu.nojs { display: block; } + +} + +.ill_availability_sourcename { + margin-top: 20px; +} + +#continue-request-row { + text-align: center; } #dc_fieldset { --- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/ill-availability-table.inc +++ a/koha-tmpl/opac-tmpl/bootstrap/en/includes/ill-availability-table.inc @@ -0,0 +1,17 @@ +
    +
    [% service.name %]
    + + + + + + + + + + + + + +
    SourceTitleAuthorISBNISSNDate
    +
    --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt +++ a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt @@ -224,6 +224,37 @@ Cancel + [% ELSIF method == 'availability' %] +

    Interlibrary loan item availability

    +
    +

    Displaying availability results

    +
    + [% FOREACH key IN whole.keys %] + [% value = whole.$key %] + [% IF key != 'custom_key' && key != 'custom_value' %] + + [% END %] + [% END %] + [% custom_keys = whole.custom_key.split('\0') %] + [% custom_values = whole.custom_value.split('\0') %] + [% i = 0 %] + [% FOREACH custom_key IN custom_keys %] + + + [% i = i + 1 %] + [% END %] + +
    + If you can't find what you are looking for, you can + or + cancel your request +
    +
    + [% FOR service IN services %] +

    [% service.plugin %]

    + [% INCLUDE 'ill-availability-table.inc' service=service %] + [% END %] +
    [% END %] [% END %] @@ -247,8 +278,17 @@ "deferRender": true })); $("#backend-dropdown-options").removeClass("nojs"); + [% IF services_json.length > 0 %] + var services = [% services_json | $raw %]; + [% ELSE %] + var services = []; + [% END %] + [% IF metadata.length > 0 %] + var metadata = "[% metadata | $raw %]"; + [% END %] //]]> +[% Asset.js("js/ill-availability.js") | $raw %] [% TRY %] [% PROCESS backend_jsinclude %] [% CATCH %] --- a/koha-tmpl/opac-tmpl/bootstrap/js/ill-availability.js +++ a/koha-tmpl/opac-tmpl/bootstrap/js/ill-availability.js @@ -0,0 +1,163 @@ +$(document).ready(function() { + + // In case the source doesn't supply data required for DT to calculate + // pagination, we need to do it ourselves + var ownPagination = false; + var directionSet = false; + var start = 0; + var forward = true; // true == forward, false == backwards + // Arbitrary starting value, it will be corrected by the first + // page of results + var pageSize = 20; + + var tableTmpl = { + ajax: { + cache: true, // Prevent DT appending a "_" cache param + }, + columns: [ + // defaultContent prevents DT from choking if + // the API response doesn't return a column + { + title: 'Source', + data: 'source', + defaultContent: '' + }, + { + data: 'title', + defaultContent: '' + }, + { + data: 'author', + defaultContent: '' + }, + { + data: 'isbn', + defaultContent: '' + }, + { + data: 'issn', + defaultContent: '' + }, + { + data: 'date', + defaultContent: '' + } + ] + }; + + // render functions don't get copied across when we make a dereferenced + // copy of them, so we have to reattach them once we have a copy + // Here we store them + var renders = { + title: function(data, type, row) { + return row.url ? + ''+row.title+'' : + row.title; + } + }; + + services.forEach(function(service) { + // Create a deferenced copy of our table definition object + var tableDef = JSON.parse(JSON.stringify(tableTmpl)); + // Iterate the table's columns array and add render functions + // as necessary + tableDef.columns.forEach(function(column) { + if (renders[column.data]) { + column.render = renders[column.data]; + } + }); + tableDef.ajax.dataSrc = function(data) { + var results = data.results.search_results; + // The source appears to be returning it's own pagination + // data + if ( + results.hasOwnProperty('recordsFiltered') || + results.hasOwnProperty('recordsTotal') + ) { + return results; + } + // Set up our own pagination values based on what we just + // got back + ownPagination = true; + directionSet = false; + pageSize = results.length; + // These values are completely arbitrary, but they enable + // us to display pagination links + data.recordsFiltered = 5000, + data.recordsTotal = 5000; + + return results; + }; + tableDef.ajax.data = function(data) { + // Datatables sends a bunch of superfluous params + // that we don't want to litter our API schema + // with, so just remove them from the request + if (data.hasOwnProperty('columns')) { + delete data.columns; + } + if (data.hasOwnProperty('draw')) { + delete data.draw; + } + if (data.hasOwnProperty('order')) { + delete data.order; + } + if (data.hasOwnProperty('search')) { + delete data.search; + } + // If we're handling our own pagination, set the properties + // that DT will send in the request + if (ownPagination) { + start = forward ? start + pageSize : start - pageSize; + data.start = start; + data.length = pageSize; + } + }; + // Add any datatables config options passed from the service + // to the table definition + tableDef.ajax.url = service.endpoint + metadata; + if (service.hasOwnProperty('datatablesConfig')) { + var conf = service.datatablesConfig; + for (var key in conf) { + // The config from the service definition comes from a Perl + // hashref, therefore can't contain true/false, so we + // special case it + if (conf.hasOwnProperty(key)) { + if (conf[key] == 'false') { + // Special case false values + tableDef[key] = false; + } else if (conf[key] == 'true') { + // Special case true values + tableDef[key] = true; + } else { + // Copy the property value + tableDef[key] = conf[key]; + } + } + } + } + // Create event watchers for the "next" and "previous" pagination + // links, this enables us to set the direction the next request is + // going in when we're doing our own pagination. We use "hover" + // because the click event is caught after the request has been + // sent + tableDef.drawCallback = function() { + $('.paginate_button.next:not(.disabled)', + this.api().table().container() + ).on('hover', function() { + forward = true; + directionSet = true; + }); + $('.paginate_button.previous:not(.disabled)', + this.api().table().container() + ).on('hover', function() { + forward = false; + directionSet = true; + }); + } + // Initialise the table + $('#'+service.id ).dataTable( + $.extend(true, {}, dataTablesDefaults, tableDef) + ); + }); + +}); --- a/opac/opac-illrequests.pl +++ a/opac/opac-illrequests.pl @@ -19,6 +19,8 @@ use Modern::Perl; +use JSON qw( encode_json ); + use CGI qw ( -utf8 ); use C4::Auth; use C4::Koha; @@ -28,6 +30,7 @@ use Koha::Illrequest::Config; use Koha::Illrequests; use Koha::Libraries; use Koha::Patrons; +use Koha::Illrequest::Availability; my $query = new CGI; @@ -110,6 +113,44 @@ if ( $op eq 'list' ) { } else { my $request = Koha::Illrequest->new ->load_backend($params->{backend}); + + # Does this backend enable us to insert an availability stage and should + # we? If not, proceed as normal. + if ( + C4::Context->preference("ILLCheckAvailability") && + $request->_backend_capability( + 'should_display_availability', + $params + ) && + # If the user has elected to continue with the request despite + # having viewed availability info, this flag will be set + !$params->{checked_availability} + ) { + # Establish which of the installed availability providers + # can service our metadata, if so, jump in + my $availability = Koha::Illrequest::Availability->new($params); + my $services = $availability->get_services(); + if (scalar @{$services} > 0) { + # Modify our method so we use the correct part of the + # template + $op = 'availability'; + # Prepare the metadata we're sending them + my $metadata = $availability->prep_metadata($params); + $template->param( + metadata => $metadata, + services_json => encode_json($services), + services => $services, + illrequestsview => 1, + message => $params->{message}, + method => $op, + whole => $params + ); + output_html_with_http_headers $query, $cookie, + $template->output, undef, { force_no_caching => 1 }; + exit; + } + } + $params->{cardnumber} = Koha::Patrons->find({ borrowernumber => $loggedinuser })->cardnumber; --