From 2e43ad87fe3f8e4190dd3bd2745c187abfc07ee7 Mon Sep 17 00:00:00 2001 From: Andrew Isherwood Date: Thu, 11 Jul 2019 14:15:55 +0100 Subject: [PATCH] Bug 23173: Provide core infrastructure This patch adds the required infrastructure to enable ILL availability plugins to intercept the request creation process and, using the supplied metadata, search for and display possible relevant items from whichever availability plugins are installed. Currently three availability plugins exist: z39.50 - Searches any number of the Koha instance's configured Z targets https://github.com/PTFS-Europe/koha-plugin-ill-avail-z3950 EDS - Searches the EBSCO Discovery Service https://github.com/PTFS-Europe/koha-plugin-ill-avail-eds Unpaywall - Searches the Unpaywall API for possible open access versions of the requested item https://github.com/PTFS-Europe/koha-plugin-ill-avail-unpaywall The Unpaywall plugin is intended to serve as a "reference" plugin as the API it deals with is extremely simple --- Koha/Illrequest/Availability.pm | 126 ++++++++++++ ill/ill-requests.pl | 82 +++++++- .../intranet-tmpl/prog/css/src/staff-global.scss | 19 ++ .../prog/en/includes/ill-availability-table.inc | 17 ++ .../prog/en/modules/ill/ill-requests.tt | 84 +++++++- .../prog/en/modules/plugins/plugins-home.tt | 1 + .../prog/js/ill-availability-partner.js | 24 +++ .../intranet-tmpl/prog/js/ill-availability.js | 222 +++++++++++++++++++++ koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss | 9 + .../en/includes/ill-availability-table.inc | 17 ++ .../bootstrap/en/modules/opac-illrequests.tt | 47 +++++ .../opac-tmpl/bootstrap/js/ill-availability.js | 178 +++++++++++++++++ opac/opac-illrequests.pl | 43 ++++ 13 files changed, 861 insertions(+), 8 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-partner.js 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 diff --git a/Koha/Illrequest/Availability.pm b/Koha/Illrequest/Availability.pm new file mode 100644 index 0000000000..099f88596e --- /dev/null +++ b/Koha/Illrequest/Availability.pm @@ -0,0 +1,126 @@ +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($params); + +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. Optionally accept a hashref specifying additional filter +parameters + +=cut + +sub get_services { + my ( $self, $params ) = @_; + + my $plugin_filter = { + method => 'ill_availability_services' + }; + + if ($params->{metadata}) { + $plugin_filter->{metadata} = $params->{metadata}; + } + + my @candidates = Koha::Plugins->new()->GetPlugins($plugin_filter); + my @services = (); + foreach my $plugin(@candidates) { + my $valid_service = $plugin->ill_availability_services({ + metadata => $self->{metadata}, + ui_context => $params->{ui_context} + }); + 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 ) = @_; + + # We sort the metadata hashref by key before encoding it, primarily + # so this function returns something predictable that we can test! + my $json = JSON->new; + $json->canonical([1]); + return uri_escape(encode_base64($json->encode($metadata))); +} + +=head1 AUTHOR + +Andrew Isherwood + +=cut + +1; diff --git a/ill/ill-requests.pl b/ill/ill-requests.pl index 010c61e618..fb206e5f88 100755 --- a/ill/ill-requests.pl +++ b/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,55 @@ 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({ + ui_context => 'staff' + }); + 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 @@ -239,10 +284,35 @@ if ( $backends_available ) { $request = Koha::Illrequests->find($params->{illrequest_id}); $params->{current_branchcode} = C4::Context->mybranch; $backend_result = $request->generic_confirm($params); + $template->param( whole => $backend_result, request => $request, ); + + # Prepare availability searching, if required + # Get the definition for the z39.50 plugin + my $availability = Koha::Illrequest::Availability->new($request->metadata); + my $services = $availability->get_services({ + ui_context => 'partners', + metadata => { + name => 'ILL availability - z39.50' + } + }); + # Only pass availability searching stuff to the template if + # appropriate + if ( + C4::Context->preference('ILLCheckAvailability') && + scalar @{$services} > 0 + ) { + my $metadata = $availability->prep_metadata($request->metadata); + $template->param( metadata => $metadata ); + $template->param( + services_json => scalar encode_json($services) + ); + $template->param( services => $services ); + } + $template->param( error => $params->{error} ) if $params->{error}; } diff --git a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss index 2ffaef1a78..a627ea0c1b 100644 --- a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss +++ b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss @@ -3750,6 +3750,21 @@ input.renew { top: 50%; transform: translateY(-50%); } + + #generic_confirm_search { + display: block; + visibility: hidden; + margin: 1em 0 1em 10em; + } + + #partnerSearch { + .modal-dialog { + width: 50vw; + } + .modal-body { + max-height: 70vh; + } + } } .ill-view-panel { @@ -3782,6 +3797,10 @@ input.renew { } } +.ill_availability_sourcename { + margin-top: 20px; +} + #stockrotation { h3 { margin: 30px 0 10px 0; diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-availability-table.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-availability-table.inc new file mode 100644 index 0000000000..3cd00e709d --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-availability-table.inc @@ -0,0 +1,17 @@ +
+
[% service.name %]
+ + + + + + + + + + + + + +
SourceTitleAuthorISBNISSNDate
+
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt index a39b93c235..27ccaf5ad9 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt @@ -144,12 +144,14 @@ - + [% IF Koha.Preference('ILLCheckAvailability') %] + + [% END %]
  • @@ -169,6 +171,29 @@ Cancel + [% IF Koha.Preference('ILLCheckAvailability') %] + + [% END %] + [% ELSE %]
    Interlibrary loan request details @@ -518,6 +543,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,9 +598,30 @@ 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 %] [% INCLUDE 'ill-list-table-strings.inc' %] [% Asset.js("js/ill-list-table.js") | $raw %] + [% IF (query_type == 'availability' || query_type == 'generic_confirm') && Koha.Preference('ILLCheckAvailability') %] + [% Asset.js("js/ill-availability.js") | $raw %] + [% END %] + [% IF query_type == 'availability' && Koha.Preference('ILLCheckAvailability') %] + + [% END %] + [% IF query_type == 'generic_confirm' && Koha.Preference('ILLCheckAvailability') %] + [% Asset.js("js/ill-availability-partner.js") | $raw %] + [% END %] [% END %] [% TRY %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-home.tt index 5d598c525e..6c2d30acfc 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-home.tt +++ b/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
  • diff --git a/koha-tmpl/intranet-tmpl/prog/js/ill-availability-partner.js b/koha-tmpl/intranet-tmpl/prog/js/ill-availability-partner.js new file mode 100644 index 0000000000..07f3664c20 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/ill-availability-partner.js @@ -0,0 +1,24 @@ +$(document).ready(function() { + $('#partners').change(function() { + var selected = []; + $('#partners option:selected').each(function() { + selected.push($(this).data('partner-id')); + }); + if (selected.length > 0) { + $('#generic_confirm_search').css('visibility', 'initial'); + } else { + $('#generic_confirm_search').css('visibility', 'hidden'); + } + $('#service_id_restrict'). + attr('data-service_id_restrict_ids', selected.join('|')); + }); + $('#generic_confirm_search').click(function(e) { + $('#partnerSearch').modal({show:true}); + }); + $('#partnerSearch').on('show.bs.modal', function() { + doSearch(); + }); + $('#partnerSearch').on('hide.bs.modal', function() { + $.fn.dataTable.tables({ api: true }).destroy(); + }); +}); diff --git a/koha-tmpl/intranet-tmpl/prog/js/ill-availability.js b/koha-tmpl/intranet-tmpl/prog/js/ill-availability.js new file mode 100644 index 0000000000..289b934ffa --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/ill-availability.js @@ -0,0 +1,222 @@ +$(document).ready(function() { + + window.doSearch = 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; + }, + source: function(data, type, row) { + return row.opac_url ? + ''+row.source+'' : + row.source; + } + }; + + 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 ( + data.hasOwnProperty('recordsFiltered') || + data.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; + } + // We may need to restrict the service IDs being queries, this + // needs to be handled in the plugin's API module + var restrict = $('#service_id_restrict'). + attr('data-service_id_restrict_ids'); + if (restrict && restrict.length > 0) { + data.restrict = restrict; + } + }; + // 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 + // Since we're not able to use the columns settings in core, + // we need to mock the object that it would return + var columns_settings = [ + { + cannot_be_modified: 0, + cannot_be_toggled: 0, + columnname: 'source', + is_hidden: 0 + }, + { + cannot_be_modified: 0, + cannot_be_toggled: 0, + columnname: 'title', + is_hidden: 0 + }, + { + cannot_be_modified: 0, + cannot_be_toggled: 0, + columnname: 'author', + is_hidden: 0 + }, + { + cannot_be_modified: 0, + cannot_be_toggled: 0, + columnname: 'isbn', + is_hidden: 0 + }, + { + cannot_be_modified: 0, + cannot_be_toggled: 0, + columnname: 'issn', + is_hidden: 0 + }, + { + cannot_be_modified: 0, + cannot_be_toggled: 0, + columnname: 'date', + is_hidden: 0 + } + ]; + // Hide pagination buttons if appropriate + tableDef.drawCallback = function() { + var pagination = $(this).closest('.dataTables_wrapper') + .find('.dataTables_paginate'); + pagination.toggle(this.api().page.info().pages > 1); + } + KohaTable(service.id, tableDef, columns_settings); + }); + } + + +}); diff --git a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss b/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss index b9d70b13f4..68687da920 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss +++ b/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss @@ -2883,6 +2883,15 @@ button { .dropdown:hover .dropdown-menu.nojs { display: block; } + +} + +.ill_availability_sourcename { + margin-top: 20px; +} + +#continue-request-row { + text-align: center; } #dc_fieldset { diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/ill-availability-table.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/ill-availability-table.inc new file mode 100644 index 0000000000..a2597bc3cf --- /dev/null +++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/ill-availability-table.inc @@ -0,0 +1,17 @@ +
    +
    [% service.name %]
    + + + + + + + + + + + + + +
    SourceTitleAuthorISBNISSNDate
    +
    diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt index 218a6e2aa2..a2b0571329 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt +++ b/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,24 @@ "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 %] //]]> +[% IF method == 'availability' %] + [% Asset.js("js/ill-availability.js") | $raw %] + +[% END %] [% TRY %] [% PROCESS backend_jsinclude %] [% CATCH %] diff --git a/koha-tmpl/opac-tmpl/bootstrap/js/ill-availability.js b/koha-tmpl/opac-tmpl/bootstrap/js/ill-availability.js new file mode 100644 index 0000000000..9c0be4c105 --- /dev/null +++ b/koha-tmpl/opac-tmpl/bootstrap/js/ill-availability.js @@ -0,0 +1,178 @@ +$(document).ready(function() { + + window.doSearch = 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; + }, + source: function(data, type, row) { + return row.opac_url ? + ''+row.source+'' : + row.source; + } + }; + + 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 ( + data.hasOwnProperty('recordsFiltered') || + data.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; + } + // We may need to restrict the service IDs being queries, this + // needs to be handled in the plugin's API module + var restrict = $('#service_id_restrict'). + attr('data-service_id_restrict_ids'); + if (restrict && restrict.length > 0) { + data.restrict = restrict; + } + }; + // 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) + ); + }); + } + + +}); diff --git a/opac/opac-illrequests.pl b/opac/opac-illrequests.pl index f35b19ada9..f81edf41ce 100755 --- a/opac/opac-illrequests.pl +++ b/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,46 @@ 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({ + ui_context => 'opac' + }); + 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; -- 2.11.0