From 65a2fc2fa33ae68716ae4305ab33f6cf56be619e Mon Sep 17 00:00:00 2001 From: Andrew Isherwood Date: Fri, 8 Apr 2022 16:09:30 +0100 Subject: [PATCH] Bug 30484: Implement support for ILL request updates This commit adds support for the concept of ILL request update notices. - Adds a new Koha::Illrequest::SupplierUpdate class that is used to encapsulate an update to a request, this update may come from a supplier via a backend or from core ILL via, perhaps, a user action - Adds a new Koha::Illrequest::SupplierUpdateProcessor base class that can be subclassed in order to create a processor that can be passed an update and act accordingly. - Updates to Illrequest.pm to support the above classes and allow core Koha to offer update processors - A shell script to initiate a periodic process to check for updates meeting given criteria and run the appropriate processors Signed-off-by: Katrin Fischer https://bugs.koha-community.org/show_bug.cgi?id=28909 Signed-off-by: Tomas Cohen Arazi --- Koha/Illrequest.pm | 101 +++++++++- Koha/Illrequest/SupplierUpdate.pm | 111 +++++++++++ Koha/Illrequest/SupplierUpdateProcessor.pm | 85 ++++++++ misc/process_ill_updates.pl | 214 +++++++++++++++++++++ 4 files changed, 505 insertions(+), 6 deletions(-) create mode 100644 Koha/Illrequest/SupplierUpdate.pm create mode 100644 Koha/Illrequest/SupplierUpdateProcessor.pm create mode 100755 misc/process_ill_updates.pl diff --git a/Koha/Illrequest.pm b/Koha/Illrequest.pm index d667f9764a..2c38dddaad 100644 --- a/Koha/Illrequest.pm +++ b/Koha/Illrequest.pm @@ -116,6 +116,33 @@ available for request. =head2 Class methods +=head3 init_processors + + $request->init_processors() + +Initialises an empty processors arrayref + +=cut + +sub init_processors { + my ( $self ) = @_; + + $self->{processors} = []; +} + +=head3 push_processor + + $request->push_processors(sub { ...something... }); + +Pushes a passed processor function into our processors arrayref + +=cut + +sub push_processor { + my ( $self, $processor ) = @_; + push @{$self->{processors}}, $processor; +} + =head3 statusalias my $statusalias = $request->statusalias; @@ -371,6 +398,7 @@ sub _backend_capability { try { $capability = $self->_backend->capabilities($name); } catch { + warn $_; return 0; }; # Try to invoke it @@ -896,6 +924,28 @@ sub backend_create { return $self->expandTemplate($result); } +=head3 backend_get_update + + my $update = backend_get_update($request); + + Given a request, returns an update in a prescribed + format that can then be passed to update parsers + +=cut + +sub backend_get_update { + my ( $self, $options ) = @_; + + my $response = $self->_backend_capability( + 'get_supplier_update', + { + request => $self, + %{$options} + } + ); + return $response; +} + =head3 expandTemplate my $params = $abstract->expandTemplate($params); @@ -1437,7 +1487,7 @@ Send a specified notice regarding this request to a patron =cut sub send_patron_notice { - my ( $self, $notice_code ) = @_; + my ( $self, $notice_code, $additional_text ) = @_; # We need a notice code if (!$notice_code) { @@ -1448,8 +1498,9 @@ sub send_patron_notice { # Map from the notice code to the messaging preference my %message_name = ( - ILL_PICKUP_READY => 'Ill_ready', - ILL_REQUEST_UNAVAIL => 'Ill_unavailable' + ILL_PICKUP_READY => 'Ill_ready', + ILL_REQUEST_UNAVAIL => 'Ill_unavailable', + ILL_REQUEST_UPDATE => 'Ill_update' ); # Get the patron's messaging preferences @@ -1471,8 +1522,9 @@ sub send_patron_notice { my @fail = (); for my $transport (@transports) { my $letter = $self->get_notice({ - notice_code => $notice_code, - transport => $transport + notice_code => $notice_code, + transport => $transport, + additional_text => $additional_text }); if ($letter) { my $result = C4::Letters::EnqueueLetter({ @@ -1613,13 +1665,50 @@ sub get_notice { substitute => { ill_bib_title => $title ? $title->value : '', ill_bib_author => $author ? $author->value : '', - ill_full_metadata => $metastring + ill_full_metadata => $metastring, + additional_text => $params->{additional_text} } ); return $letter; } + +=head3 attach_processors + +Receive a Koha::Illrequest::SupplierUpdate and attach +any processors we have for it + +=cut + +sub attach_processors { + my ( $self, $update ) = @_; + + foreach my $processor(@{$self->{processors}}) { + if ( + $processor->{target_source_type} eq $update->{source_type} && + $processor->{target_source_name} eq $update->{source_name} + ) { + $update->attach_processor($processor); + } + } +} + +=head3 append_to_note + + append_to_note("Some text"); + +Append some text to the staff note + +=cut + +sub append_to_note { + my ($self, $text) = @_; + my $current = $self->notesstaff; + $text = ($current && length $current > 0) ? "$current\n\n$text" : $text; + $self->notesstaff($text)->store; +} + =head3 id_prefix my $prefix = $record->id_prefix; diff --git a/Koha/Illrequest/SupplierUpdate.pm b/Koha/Illrequest/SupplierUpdate.pm new file mode 100644 index 0000000000..3cc54298fc --- /dev/null +++ b/Koha/Illrequest/SupplierUpdate.pm @@ -0,0 +1,111 @@ +package Koha::Illrequest::SupplierUpdate; + +# Copyright 2022 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, see . + +use Modern::Perl; + +=head1 NAME + +Koha::Illrequest::SupplierUpdate - Represents a single request update from a supplier + +=head1 SYNOPSIS + +Object-oriented class that provides an object allowing us to interact with +an update from a supplier + +=head1 DESCRIPTION + +Object-oriented class that provides an object allowing us to interact with +an update from a supplier + +=head1 API + +=head2 Class Methods + +=head3 new + + my $update = Koha::Illrequest::SupplierUpdate->new( + $source_type, + $source_name, + $update + ); + +Create a new Koha::Illrequest::SupplierUpdate object. + +=cut + +sub new { + my ( $class, $source_type, $source_name, $update, $request ) = @_; + my $self = {}; + + $self->{source_type} = $source_type; + $self->{source_name} = $source_name; + $self->{update} = $update; + $self->{request} = $request; + $self->{processors} = []; + + bless $self, $class; + + return $self; +} + +=head3 attach_processor + + Koha::Illrequest::SupplierUpdate->attach_processor($processor); + +Pushes a processor function onto the 'processors' arrayref + +=cut + +sub attach_processor { + my ( $self, $processor ) = @_; + push(@{$self->{processors}}, $processor); +} + +=head3 run_processors + + Koha::Illrequest::SupplierUpdate->run_processors(); + +Iterates all processors on this object and runs each + +=cut + +sub run_processors { + my ( $self, $options ) = @_; + my $results = []; + foreach my $processor(@{$self->{processors}}) { + my $processor_result = { + name => $processor->{name}, + result => { + success => [], + error => [] + } + }; + $processor->run($self, $options, $processor_result->{result}); + push @{$results}, $processor_result; + } + return $results; +} + +=head1 AUTHOR + +Andrew Isherwood + +=cut + +1; diff --git a/Koha/Illrequest/SupplierUpdateProcessor.pm b/Koha/Illrequest/SupplierUpdateProcessor.pm new file mode 100644 index 0000000000..3136127c83 --- /dev/null +++ b/Koha/Illrequest/SupplierUpdateProcessor.pm @@ -0,0 +1,85 @@ +package Koha::Illrequest::SupplierUpdateProcessor; + +# Copyright 2022 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, see . + +use Modern::Perl; + +=head1 NAME + +Koha::Illrequest::SupplierUpdateProcessor - Represents a SupplerUpdate processor + +=head1 SYNOPSIS + +Object-oriented class that provides an object allowing us to perform processing on +a SupplierUpdate + +=head1 DESCRIPTION + +Object-oriented base class that provides an object allowing us to perform processing on +a SupplierUpdate +** This class should not be directly instantiated, it should only be sub-classed ** + +=head1 API + +=head2 Class Methods + +=head3 new + + my $processor = Koha::Illrequest::SupplierUpdateProcessor->new( + $target_source_type, + $target_source_name + ); + +Create a new Koha::Illrequest::SupplierUpdateProcessor object. + +=cut + +sub new { + my ( $class, $target_source_type, $target_source_name, $processor_name ) = @_; + my $self = {}; + + $self->{target_source_type} = $target_source_type; + $self->{target_source_name} = $target_source_name; + $self->{name} = $processor_name; + + bless $self, $class; + + return $self; +} + +=head3 run + + Koha::Illrequest::SupplierUpdateProcessor->run(); + +Runs the processor + +=cut + +sub run { + my ( $self ) = @_; + my ( $package, $filename ) = caller; + warn __PACKAGE__ . " run should only be invoked by a subclass\n"; +} + +=head1 AUTHOR + +Andrew Isherwood + +=cut + +1; diff --git a/misc/process_ill_updates.pl b/misc/process_ill_updates.pl new file mode 100755 index 0000000000..85b05d2a00 --- /dev/null +++ b/misc/process_ill_updates.pl @@ -0,0 +1,214 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Copyright (C) 2022 PTFS Europe +# +# 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, see . + +use Modern::Perl; +use Getopt::Long qw( GetOptions ); +use POSIX; + +use Koha::Script; +use Koha::Illrequests; + +# Command line option values +my $get_help = 0; +my $statuses = ""; +my $status_alias = ""; +my $status_to = ""; +my $status_alias_to = ""; +my $backend = ""; +my $dry_run = 0; +my $delay = 0; +my $debug = 0; + +my $options = GetOptions( + 'h|help' => \$get_help, + 'statuses:s' => \$statuses, + 'status-alias:s' => \$status_alias, + 'status-to:s' => \$status_to, + 'status-alias-to:s' => \$status_alias_to, + 'backend=s' => \$backend, + 'dry-run' => \$dry_run, + 'api-delay:i' => \$delay, + 'debug' => \$debug +); + +if ($get_help) { + get_help(); + exit 1; +} + +if (!$backend) { + print "No backend specified\n"; + exit 0; +} + +# First check we can proceed +my $cfg = Koha::Illrequest::Config->new; +my $backends = $cfg->available_backends; +my $has_branch = $cfg->has_branch; +my $backends_available = ( scalar @{$backends} > 0 ); +if (!$has_branch || $backends_available == 0) { + print "Unable to proceed:\n"; + print "Branch configured: $has_branch\n"; + print "Backends available: $backends_available\n"; + exit 0; +} + +# Get all required requests +my @statuses_arr = split(/:/, $statuses); +my @status_alias_arr = split(/:/, $status_alias); + +my $where = { + backend => $backend +}; + +if (scalar @statuses_arr > 0) { + my @or = grep(!/null/, @statuses_arr); + if (scalar @or < scalar @statuses_arr) { + push @or, undef; + } + $where->{status} = \@or; +} + +if (scalar @status_alias_arr > 0) { + my @or = grep(!/null/, @status_alias_arr); + if (scalar @or < scalar @status_alias_arr) { + push @or, undef; + } + $where->{status_alias} = \@or; +} + +debug_msg("DBIC WHERE:"); +debug_msg($where); + +my $requests = Koha::Illrequests->search($where); + +debug_msg("Processing " . $requests->count . " requests"); + +# Create an options hashref to pass to processors +my $options_to_pass = { + dry_run => $dry_run, + status_to => $status_to, + status_alias_to => $status_alias_to, + delay => $delay, + debug => \&debug_msg +}; + +# The progress log +my $output = []; + +while (my $request = $requests->next) { + debug_msg("- Request ID " . $request->illrequest_id); + my $update = $request->backend_get_update($options_to_pass); + # The log for this request + my $update_log = { + request_id => $request->illrequest_id, + processed_by => $request->_backend->name, + processors_run => [] + }; + if ($update) { + # Currently we make an assumption, this may need revisiting + # if we need to extend the functionality: + # + # Only the backend that originated the update will want to + # process it + # + # Since each backend's update format is different, it may + # be necessary for a backend to subclass Koha::Illrequest::SupplierUpdate + # so it can provide methods (corresponding to a generic interface) that + # return pertinent info to core ILL when it is processing updates + # + # Attach any request processors + $request->attach_processors($update); + # Attach any processors from this request's backend + $request->_backend->attach_processors($update); + my $processor_results = $update->run_processors($options_to_pass); + # Update our progress log + $update_log->{processors_run} = $processor_results; + } + push @{$output}, $update_log; +} + +print_summary($output); + +sub print_summary { + my ( $log ) = @_; + + my $timestamp = POSIX::strftime("%d/%m/%Y %H:%M:%S\n", localtime); + print "Run details:\n"; + foreach my $entry(@{$log}) { + my @processors_run = @{$entry->{processors_run}}; + print "Request ID: " . $entry->{request_id} . "\n"; + print " Processing by: " . $entry->{processed_by} . "\n"; + print " Number of processors run: " . scalar @processors_run . "\n"; + if (scalar @processors_run > 0) { + print " Processor details:\n"; + foreach my $processor(@processors_run) { + print " Processor name: " . $processor->{name} . "\n"; + print " Success messages: " . join(", ", @{$processor->{result}->{success}}) . "\n"; + print " Error messages: " . join(", ", @{$processor->{result}->{error}}) . "\n"; + } + } + } + print "Job completed at $timestamp\n====================================\n\n" +} + +sub debug_msg { + my ( $msg ) = @_; + + if (!$debug) { + return; + } + + if (ref $msg eq 'HASH') { + use Data::Dumper; + $msg = Dumper $msg; + } + print STDERR "$msg\n"; +} + +sub get_help { + print <<"HELP"; +$0: Fetch and process outstanding ILL updates + +This script will fetch all requests that have the specified +statuses and run any applicable processor scripts on them. +For example, the RapidILL backend provides a processor script +that emails users when their requested electronic resource +request has been fulfilled + +Parameters: + --statuses specify the statuses a request must have in order to be processed, + statuses should be separated by a : e.g. REQ:COMP:NEW. A null value + can be specified by passing null, e.g. --statuses null + + --status-aliases specify the statuses aliases a request must have in order to be processed, + statuses should be separated by a : e.g. STA:OLD:PRE. A null value + can be specified by passing null, e.g. --status-aliases null + --status-to specify the status a successfully processed request must be set to + after processing + --status-alias-to specify the status alias a successfully processed request must be set to + after processing + --dry-run only produce a run report, without actually doing anything permanent + --api-delay if a processing script needs to make an API call, how long a pause + should be inserted between each API call + --debug print additional debugging info during run + + --help or -h get help +HELP +} -- 2.39.5