Bug 23173: Provide core infrastructure
authorAndrew Isherwood <andrew.isherwood@ptfs-europe.com>
Thu, 11 Jul 2019 13:15:55 +0000 (14:15 +0100)
committerMartin Renvoize <martin.renvoize@ptfs-europe.com>
Mon, 6 Apr 2020 10:04:19 +0000 (11:04 +0100)
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

Signed-off-by: Niamh Walker-Headon <Niamh.Walker-Headon@tudublin.ie>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>

13 files changed:
Koha/Illrequest/Availability.pm [new file with mode: 0644]
ill/ill-requests.pl
koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss
koha-tmpl/intranet-tmpl/prog/en/includes/ill-availability-table.inc [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt
koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-home.tt
koha-tmpl/intranet-tmpl/prog/js/ill-availability-partner.js [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/ill-availability.js [new file with mode: 0644]
koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss
koha-tmpl/opac-tmpl/bootstrap/en/includes/ill-availability-table.inc [new file with mode: 0644]
koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt
koha-tmpl/opac-tmpl/bootstrap/js/ill-availability.js [new file with mode: 0644]
opac/opac-illrequests.pl

diff --git a/Koha/Illrequest/Availability.pm b/Koha/Illrequest/Availability.pm
new file mode 100644 (file)
index 0000000..099f885
--- /dev/null
@@ -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 <andrew.isherwood@ptfs-europe.com>
+
+=cut
+
+1;
index f31577e..b1bc552 100755 (executable)
@@ -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};
         }
index 0a704fa..403f00e 100644 (file)
@@ -3759,6 +3759,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 {
@@ -3795,6 +3810,10 @@ input.renew {
     margin: 20px 0 30px 0;
 }
 
+.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 (file)
index 0000000..3cd00e7
--- /dev/null
@@ -0,0 +1,17 @@
+<div>
+    <div>[% service.name %]</div>
+    <table class="ill-availability" id="[% service.id %]">
+        <thead id="[% service.id %]-header">
+            <tr>
+                <th>Source</th>
+                <th>Title</th>
+                <th>Author</th>
+                <th>ISBN</th>
+                <th>ISSN</th>
+                <th>Date</th>
+            </tr>
+        </thead>
+        <tbody id="[% service.id %]-body">
+        </tbody>
+    </table>
+</div>
index dc92128..fac382c 100644 (file)
                                         <select size="5" multiple="true" id="partners" name="partners" required="required">
                                             [% FOREACH partner IN whole.value.partners %]
                                                 [% IF partner.email && partner.email.length > 0 %]
-                                                    <option value="[% partner.email | html %]">
+                                                    <option data-partner-id="[% partner.id | html %]" value=[% partner.email | html %]>
                                                         [% partner.branchcode _ " - " _ partner.surname %]
                                                     </option>
                                                 [% END %]
                                             [% END %]
                                         </select>
-
+                                        [% IF Koha.Preference('ILLCheckAvailability') %]
+                                            <button type="button" id="generic_confirm_search">Search selected partners</button>
+                                        [% END %]
                                     </li>
                                     <li>
                                         <label for="subject" class="required">Subject line:</label>
                                 <span><a href="[% ill_url | url %]" title="Return to request details">Cancel</a></span>
                             </fieldset>
                         </form>
+                        [% IF Koha.Preference('ILLCheckAvailability') %]
+                            <div id="partnerSearch" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="partnerSearchLabel" aria-hidden="true">
+                                <div class="modal-dialog">
+                                    <div class="modal-content">
+                                        <div class="modal-header">
+                                            <button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">×</button>
+                                            <h3 id="partnerSearchLabel"> Search partners</h3>
+                                        </div>
+                                        <div class="modal-body">
+                                            [% FOR service IN services %]
+                                                <h4 class="ill_availability_sourcename">[% service.plugin %]</h4>
+                                                [% INCLUDE 'ill-availability-table.inc' service=service %]
+                                            [% END %]
+                                            <span id="service_id_restrict" data-service_id_restrict_plugin="ILL availability - z39.50" data-service_id_restrict_ids=""></span>
+                                        </div>
+                                        <div class="modal-footer">
+                                            <button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Close</button>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        [% END %]
+
                     [% ELSE %]
                         <fieldset class="rows">
                             <legend>Interlibrary loan request details</legend>
                         [% INCLUDE 'ill-list-table.inc' %]
 
                     </div> <!-- /#results -->
+                [% ELSIF query_type == 'availability' %]
+                    <!-- availability -->
+                    <h1>Availability</h1>
+                    <div id="results">
+                        <h3>Displaying availability results</h3>
+                        <form method="POST" action="/cgi-bin/koha/ill/ill-requests.pl">
+                            [% FOREACH key IN whole.keys %]
+                                [% value = whole.$key %]
+                                [% IF key != 'method' && key != 'custom_key' && key != 'custom_value' %]
+                                <input type="hidden" name="[% key | html %]" value="[% value | html %]">
+                                [% END %]
+                            [% END %]
+                            [% custom_keys = whole.custom_key.split('\0') %]
+                            [% custom_values = whole.custom_value.split('\0') %]
+                            [% i = 0 %]
+                            [% FOREACH custom_key IN custom_keys %]
+                                <input type="hidden" name="custom_key" value="[% custom_key %]">
+                                <input type="hidden" name="custom_value" value="[% custom_values.$i %]">
+                            [% i = i + 1 %]
+                            [% END %]
+                            <input type="hidden" name="method" value="create">
+                            <input type="hidden" name="stage" value="form">
+                            <input type="hidden" name="checked_availability" value="1">
+                            <div id="continue-request-row" class="alert">
+                                If you can't find what you are looking for, you can
+                                <button class="button" type="submit">continue creating your request</button> or
+                                <a href="/cgi-bin/koha/ill/ill-requests.pl">cancel your request</a>
+                            </div>
+                        </form>
+                        [% FOR service IN services %]
+                            <h4 class="ill_availability_sourcename">[% service.plugin %]</h4>
+                            [% INCLUDE 'ill-availability-table.inc' service=service %]
+                        [% END %]
+                    </div>
                 [% ELSE %]
                 <!-- Custom Backend Action -->
                 [% PROCESS $whole.template %]
         }).on("change", function(e, value) {
             if ( ! is_valid_date( $(this).val() ) ) {$(this).val("");}
         });
+        [% IF services_json.length > 0 %]
+        var services = [% services_json | $raw %];
+        [% ELSE %]
+        var services = [];
+        [% END %]
+        [% IF metadata.length > 0 %]
+        var metadata = "[% metadata | $raw %]";
+        [% END %]
     </script>
     <script>
         $('#ill_checkout_inhouse_select').on('change', function() {
     </script>
     [% 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') %]
+        <script>
+            $(document).ready(function() {
+                window.doSearch();
+            });
+        </script>
+    [% END %]
+    [% IF query_type == 'generic_confirm' && Koha.Preference('ILLCheckAvailability') %]
+        [% Asset.js("js/ill-availability-partner.js") | $raw %]
+    [% END %]
 [% END %]
 
 [% TRY %]
index a47cd7e..1c25a22 100644 (file)
@@ -37,6 +37,7 @@
                                     <li><a href="/cgi-bin/koha/plugins/plugins-home.pl?method=to_marc">View MARC conversion plugins</a></li>
                                     <li><a href="/cgi-bin/koha/plugins/plugins-home.pl?method=opac_online_payment">View online payment plugins</a></li>
                                     <li><a href="/cgi-bin/koha/plugins/plugins-home.pl?method=intranet_catalog_biblio_enhancements">View intranet catalog biblio enhancement plugins</a></li>
+                                    <li><a href="/cgi-bin/koha/plugins/plugins-home.pl?method=ill_availability_services">View ILL availability plugins</a></li>
                                 </ul>
                             </div>
 
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 (file)
index 0000000..07f3664
--- /dev/null
@@ -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 (file)
index 0000000..289b934
--- /dev/null
@@ -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 ?
+                    '<a href="'+row.url+'" target="_blank">'+row.title+'</a>' :
+                    row.title;
+            },
+            source: function(data, type, row) {
+                return row.opac_url ?
+                    '<a href="'+row.opac_url+'" target="_blank">'+row.source+'</a>' :
+                    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);
+        });
+    }
+
+
+});
index 7b0dbfa..1ebc3c8 100644 (file)
@@ -2920,6 +2920,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 (file)
index 0000000..a2597bc
--- /dev/null
@@ -0,0 +1,17 @@
+<div>
+    <div>[% service.name %]</div>
+    <table class="ill-availability table table-bordered table-striped" id="[% service.id %]">
+        <thead id="[% service.id %]-header">
+            <tr>
+                <th>Source</th>
+                <th>Title</th>
+                <th>Author</th>
+                <th>ISBN</th>
+                <th>ISSN</th>
+                <th>Date</th>
+            </tr>
+        </thead>
+        <tbody id="[% service.id %]-body">
+        </tbody>
+    </table>
+</div>
index 218a6e2..a2b0571 100644 (file)
                                 <span class="cancel"><a href="/cgi-bin/koha/opac-illrequests.pl">Cancel</a></span>
                             </fieldset>
                         </form>
+                    [% ELSIF method == 'availability' %]
+                        <h2>Interlibrary loan item availability</h2>
+                        <div id="results">
+                            <h3>Displaying availability results</h3>
+                            <form method="POST" action="/cgi-bin/koha/opac-illrequests.pl">
+                                [% FOREACH key IN whole.keys %]
+                                    [% value = whole.$key %]
+                                    [% IF key != 'custom_key' && key != 'custom_value' %]
+                                    <input type="hidden" name="[% key | html %]" value="[% value | html %]">
+                                    [% END %]
+                                [% END %]
+                                [% custom_keys = whole.custom_key.split('\0') %]
+                                [% custom_values = whole.custom_value.split('\0') %]
+                                [% i = 0 %]
+                                [% FOREACH custom_key IN custom_keys %]
+                                    <input type="hidden" name="custom_key" value="[% custom_key %]">
+                                    <input type="hidden" name="custom_value" value="[% custom_values.$i %]">
+                                [% i = i + 1 %]
+                                [% END %]
+                                <input type="hidden" name="checked_availability" value="1">
+                                <div id="continue-request-row" class="alert">
+                                    If you can't find what you are looking for, you can
+                                    <button class="button" type="submit">continue creating your request</button> or
+                                    <a href="/cgi-bin/koha/opac-illrequests.pl">cancel your request</a>
+                                </div>
+                            </form>
+                            [% FOR service IN services %]
+                                <h4 class="ill_availability_sourcename">[% service.plugin %]</h4>
+                                [% INCLUDE 'ill-availability-table.inc' service=service %]
+                            [% END %]
+                        </div>
                     [% END %]
                 </div> <!-- / .maincontent -->
           [% END %]
             "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 %]
     //]]>
 </script>
+[% IF method == 'availability' %]
+    [% Asset.js("js/ill-availability.js") | $raw %]
+    <script>
+        $(document).ready(function() {
+            window.doSearch();
+        })
+    </script>
+[% 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 (file)
index 0000000..9c0be4c
--- /dev/null
@@ -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 ?
+                    '<a href="'+row.url+'" target="_blank">'+row.title+'</a>' :
+                    row.title;
+            },
+            source: function(data, type, row) {
+                return row.opac_url ?
+                    '<a href="'+row.opac_url+'" target="_blank">'+row.source+'</a>' :
+                    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)
+            );
+        });
+    }
+
+
+});
index f35b19a..f81edf4 100755 (executable)
@@ -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;