Bug 7317: Interlibrary loans framework for Koha.

This Commit is at the heart of adding an interlibrary loans framework
for Koha.  The framework does not prescribe a particular workflow.
Instead it provides a general framework that can be extended &
implemented by individual backends whose responsibility it is to
implement a specific workflow.

The module is largely self-sufficient: it adds new tables to the Koha
database and touches only a few files in the Koha source tree.

Primarily, we add our files to the Makefile and the koha-conf.xml,
define ill paths for the REST API, and introduce links from the main
intranet, opac pages & user permissions.

Outside of this we simply add new files & functionality.

Signed-off-by: Magnus Enger <magnus@libriotech.no>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Benjamin Rokseth <benjamin.rokseth@kul.oslo.kommune.no>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
This commit is contained in:
Alex Sassmannshausen 2017-02-03 16:58:35 +01:00 committed by Jonathan Druart
parent 4a1ff8534a
commit 8e86b5e093
27 changed files with 4712 additions and 17 deletions

935
Koha/Illrequest.pm Normal file
View File

@ -0,0 +1,935 @@
package Koha::Illrequest;
# Copyright PTFS Europe 2016
#
# 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 Clone 'clone';
use File::Basename qw/basename/;
use Koha::Database;
use Koha::Email;
use Koha::Illrequest;
use Koha::Illrequestattributes;
use Koha::Patron;
use Mail::Sendmail;
use Try::Tiny;
use base qw(Koha::Object);
=head1 NAME
Koha::Illrequest - Koha Illrequest Object class
=head1 (Re)Design
An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
of related Illrequestattributes.
The former encapsulates the basic necessary information that any ILL requires
to be usable in Koha. The latter is a set of additional properties used by
one of the backends.
The former subsumes the legacy "Status" object. The latter remains
encapsulated in the "Record" object.
TODO:
- Anything invoking the ->status method; annotated with:
+ # Old use of ->status !
=head1 API
=head2 Backend API Response Principles
All methods should return a hashref in the following format:
=item * error
This should be set to 1 if an error was encountered.
=item * status
The status should be a string from the list of statuses detailed below.
=item * message
The message is a free text field that can be passed on to the end user.
=item * value
The value returned by the method.
=over
=head2 Interface Status Messages
=over
=item * branch_address_incomplete
An interface request has determined branch address details are incomplete.
=item * cancel_success
The interface's cancel_request method was successful in cancelling the
Illrequest using the API.
=item * cancel_fail
The interface's cancel_request method failed to cancel the Illrequest using
the API.
=item * unavailable
The interface's request method returned saying that the desired item is not
available for request.
=head2 Class Methods
=cut
=head3 type
=cut
sub _type {
return 'Illrequest';
}
sub illrequestattributes {
my ( $self ) = @_;
return Koha::Illrequestattributes->_new_from_dbic(
scalar $self->_result->illrequestattributes
);
}
sub patron {
my ( $self ) = @_;
return Koha::Patron->_new_from_dbic(
scalar $self->_result->borrowernumber
);
}
sub load_backend {
my ( $self, $backend_id ) = @_;
my @raw = qw/Koha Illbackends/; # Base Path
my $backend_name = $backend_id || $self->backend;
$location = join "/", @raw, $backend_name, "Base.pm"; # File to load
$backend_class = join "::", @raw, $backend_name, "Base"; # Package name
require $location;
$self->{_my_backend} = $backend_class->new({ config => $self->_config });
return $self;
}
=head3 _backend
my $backend = $abstract->_backend($new_backend);
my $backend = $abstract->_backend;
Getter/Setter for our API object.
=cut
sub _backend {
my ( $self, $backend ) = @_;
$self->{_my_backend} = $backend if ( $backend );
# Dynamically load our backend object, as late as possible.
$self->load_backend unless ( $self->{_my_backend} );
return $self->{_my_backend};
}
=head3 _backend_capability
my $backend_capability_result = $self->_backend_capability($name, $args);
This is a helper method to invoke optional capabilities in the backend. If
the capability named by $name is not supported, return 0, else invoke it,
passing $args along with the invocation, and return its return value.
NOTE: this module suffers from a confusion in termninology:
in _backend_capability, the notion of capability refers to an optional feature
that is implemented in core, but might not be supported by a given backend.
in capabilities & custom_capability, capability refers to entries in the
status_graph (after union between backend and core).
The easiest way to fix this would be to fix the terminology in
capabilities & custom_capability and their callers.
=cut
sub _backend_capability {
my ( $self, $name, $args ) = @_;
my $capability = 0;
try {
$capability = $self->_backend->capabilities($name);
} catch {
return 0;
};
if ( $capability ) {
return &{$capability}($args);
} else {
return 0;
}
}
=head3 _config
my $config = $abstract->_config($config);
my $config = $abstract->_config;
Getter/Setter for our config object.
=cut
sub _config {
my ( $self, $config ) = @_;
$self->{_my_config} = $config if ( $config );
# Load our config object, as late as possible.
unless ( $self->{_my_config} ) {
$self->{_my_config} = Koha::Illrequest::Config->new;
}
return $self->{_my_config};
}
=head3 metadata
=cut
sub metadata {
my ( $self ) = @_;
return $self->_backend->metadata($self);
}
=head3 _core_status_graph
my $core_status_graph = $illrequest->_core_status_graph;
Returns ILL module's default status graph. A status graph defines the list of
available actions at any stage in the ILL workflow. This is for instance used
by the perl script & template to generate the correct buttons to display to
the end user at any given point.
=cut
sub _core_status_graph {
my ( $self ) = @_;
return {
NEW => {
prev_actions => [ ], # Actions containing buttons
# leading to this status
id => 'NEW', # ID of this status
name => 'New request', # UI name of this status
ui_method_name => 'New request', # UI name of method leading
# to this status
method => 'create', # method to this status
next_actions => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
# requests with this status
ui_method_icon => 'fa-plus', # UI Style class
},
REQ => {
prev_actions => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
id => 'REQ',
name => 'Requested',
ui_method_name => 'Confirm request',
method => 'confirm',
next_actions => [ 'REQREV', 'COMP' ],
ui_method_icon => 'fa-check',
},
GENREQ => {
prev_actions => [ 'NEW', 'REQREV' ],
id => 'GENREQ',
name => 'Requested from partners',
ui_method_name => 'Place request with partners',
method => 'generic_confirm',
next_actions => [ 'COMP' ],
ui_method_icon => 'fa-send-o',
},
REQREV => {
prev_actions => [ 'REQ' ],
id => 'REQREV',
name => 'Request reverted',
ui_method_name => 'Revert Request',
method => 'cancel',
next_actions => [ 'REQ', 'GENREQ', 'KILL' ],
ui_method_icon => 'fa-times',
},
QUEUED => {
prev_actions => [ ],
id => 'QUEUED',
name => 'Queued request',
ui_method_name => 0,
method => 0,
next_actions => [ 'REQ', 'KILL' ],
ui_method_icon => 0,
},
CANCREQ => {
prev_actions => [ 'NEW' ],
id => 'CANCREQ',
name => 'Cancellation requested',
ui_method_name => 0,
method => 0,
next_actions => [ 'KILL', 'REQ' ],
ui_method_icon => 0,
},
COMP => {
prev_actions => [ 'REQ' ],
id => 'COMP',
name => 'Completed',
ui_method_name => 'Mark completed',
method => 'mark_completed',
next_actions => [ ],
ui_method_icon => 'fa-check',
},
KILL => {
prev_actions => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
id => 'KILL',
name => 0,
ui_method_name => 'Delete request',
method => 'delete',
next_actions => [ ],
ui_method_icon => 'fa-trash',
},
};
}
=head3 _core_status_graph
my $status_graph = $illrequest->_core_status_graph($origin, $new_graph);
Return a new status_graph, the result of merging $origin & new_graph. This is
operation is a union over the sets defied by the two graphs.
Each entry in $new_graph is added to $origin. We do not provide a syntax for
'subtraction' of entries from $origin.
Whilst it is not intended that this works, you can override entries in $origin
with entries with the same key in $new_graph. This can lead to problematic
behaviour when $new_graph adds an entry, which modifies a dependent entry in
$origin, only for the entry in $origin to be replaced later with a new entry
from $new_graph.
NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
i.e. each of the graphs need to be correct at the outset of the operation.
=cut
sub _status_graph_union {
my ( $self, $core_status_graph, $backend_status_graph ) = @_;
# Create new status graph with:
# - all core_status_graph
# - for-each each backend_status_graph
# + add to new status graph
# + for each core prev_action:
# * locate core_status
# * update next_actions with additional next action.
# + for each core next_action:
# * locate core_status
# * update prev_actions with additional prev action
my @core_status_ids = keys %{$core_status_graph};
my $status_graph = clone($core_status_graph);
foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
$backend_status = $backend_status_graph->{$backend_status_key};
# Add to new status graph
$status_graph->{$backend_status_key} = $backend_status;
# Update all core methods' next_actions.
foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
if ( grep $prev_action, @core_status_ids ) {
my @next_actions =
@{$status_graph->{$prev_action}->{next_actions}};
push @next_actions, $backend_status_key;
$status_graph->{$prev_action}->{next_actions}
= \@next_actions;
}
}
# Update all core methods' prev_actions
foreach my $next_action ( @{$backend_status->{next_actions}} ) {
if ( grep $next_action, @core_status_ids ) {
my @prev_actions =
@{$status_graph->{$next_action}->{prev_actions}};
push @prev_actions, $backend_status_key;
$status_graph->{$next_action}->{prev_actions}
= \@prev_actions;
}
}
}
return $status_graph;
}
### Core API methods
=head3 capabilities
my $capabilities = $illrequest->capabilities;
Return a hashref mapping methods to operation names supported by the queried
backend.
Example return value:
{ create => "Create Request", confirm => "Progress Request" }
NOTE: this module suffers from a confusion in termninology:
in _backend_capability, the notion of capability refers to an optional feature
that is implemented in core, but might not be supported by a given backend.
in capabilities & custom_capability, capability refers to entries in the
status_graph (after union between backend and core).
The easiest way to fix this would be to fix the terminology in
capabilities & custom_capability and their callers.
=cut
sub capabilities {
my ( $self, $status ) = @_;
# Generate up to date status_graph
my $status_graph = $self->_status_graph_union(
$self->_core_status_graph,
$self->_backend->status_graph({
request => $self,
other => {}
})
);
# Extract available actions from graph.
return $status_graph->{$status} if $status;
# Or return entire graph.
return $status_graph;
}
=head3 custom_capability
Return the result of invoking $CANDIDATE on this request's backend with
$PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
NOTE: this module suffers from a confusion in termninology:
in _backend_capability, the notion of capability refers to an optional feature
that is implemented in core, but might not be supported by a given backend.
in capabilities & custom_capability, capability refers to entries in the
status_graph (after union between backend and core).
The easiest way to fix this would be to fix the terminology in
capabilities & custom_capability and their callers.
=cut
sub custom_capability {
my ( $self, $candidate, $params ) = @_;
foreach my $capability ( values %{$self->capabilities} ) {
if ( $candidate eq $capability->{method} ) {
my $response =
$self->_backend->$candidate({
request => $self,
other => $params,
});
return $self->expandTemplate($response);
}
}
return 0;
}
sub available_backends {
my ( $self ) = @_;
my $backend_dir = $self->_config->backend_dir;
my @backends = ();
@backends = <$backend_dir/*> if ( $backend_dir );
@backends = map { basename($_) } @backends;
return \@backends;
}
sub available_actions {
my ( $self ) = @_;
my $current_action = $self->capabilities($self->status);
my @available_actions = map { $self->capabilities($_) }
@{$current_action->{next_actions}};
return \@available_actions;
}
sub mark_completed {
my ( $self ) = @_;
$self->status('COMP')->store;
return {
error => 0,
status => '',
message => '',
method => 'mark_completed',
stage => 'commit',
next => 'illview',
};
}
sub backend_confirm {
my ( $self, $params ) = @_;
# The backend handles setting of mandatory fields in the commit stage:
# - orderid
# - accessurl, cost (if available).
my $response = $self->_backend->confirm({
request => $self,
other => $params,
});
return $self->expandTemplate($response);
}
sub backend_update_status {
my ( $self, $params ) = @_;
return $self->expandTemplate($self->_backend->update_status($params));
}
=head3 backend_cancel
my $ILLResponse = $illRequest->backend_cancel;
The standard interface method allowing for request cancellation.
=cut
sub backend_cancel {
my ( $self, $params ) = @_;
my $result = $self->_backend->cancel({
request => $self,
other => $params
});
return $self->expandTemplate($result);
}
=head3 backend_renew
my $renew_response = $illRequest->backend_renew;
The standard interface method allowing for request renewal queries.
=cut
sub backend_renew {
my ( $self ) = @_;
return $self->expandTemplate(
$self->_backend->renew({
request => $self,
})
);
}
=head3 backend_create
my $create_response = $abstractILL->backend_create($params);
Return an array of Record objects created by querying our backend with
a Search query.
In the context of the other ILL methods, this is a special method: we only
pass it $params, as it does not yet have any other data associated with it.
=cut
sub backend_create {
my ( $self, $params ) = @_;
# Establish whether we need to do a generic copyright clearance.
if ( ( !$params->{stage} || $params->{stage} eq 'init' )
&& C4::Context->preference("ILLModuleCopyrightClearance") ) {
return {
error => 0,
status => '',
message => '',
method => 'create',
stage => 'copyrightclearance',
value => {
backend => $self->_backend->name
}
};
} elsif ( $params->{stage} eq 'copyrightclearance' ) {
$params->{stage} = 'init';
}
# First perform API action, then...
my $args = {
request => $self,
other => $params,
};
my $result = $self->_backend->create($args);
# ... simple case: we're not at 'commit' stage.
my $stage = $result->{stage};
return $self->expandTemplate($result)
unless ( 'commit' eq $stage );
# ... complex case: commit!
# Do we still have space for an ILL or should we queue?
my $permitted = $self->check_limits(
{ patron => $self->patron }, { librarycode => $self->branchcode }
);
# Now augment our committed request.
$result->{permitted} = $permitted; # Queue request?
# This involves...
# ...Updating status!
$self->status('QUEUED')->store unless ( $permitted );
return $self->expandTemplate($result);
}
=head3 expandTemplate
my $params = $abstract->expandTemplate($params);
Return a version of $PARAMS augmented with our required template path.
=cut
sub expandTemplate {
my ( $self, $params ) = @_;
my $backend = $self->_backend->name;
# Generate path to file to load
my $backend_dir = $self->_config->backend_dir;
my $backend_tmpl = join "/", $backend_dir, $backend;
my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
$params->{method} . ".inc";
my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
$params->{method} . ".inc";
# Set files to load
$params->{template} = $intra_tmpl;
$params->{opac_template} = $opac_tmpl;
return $params;
}
#### Abstract Imports
=head3 getLimits
my $limit_rules = $abstract->getLimits( {
type => 'brw_cat' | 'branch',
value => $value
} );
Return the ILL limit rules for the supplied combination of type / value.
As the config may have no rules for this particular type / value combination,
or for the default, we must define fall-back values here.
=cut
sub getLimits {
my ( $self, $params ) = @_;
my $limits = $self->_config->getLimitRules($params->{type});
return $limits->{$params->{value}}
|| $limits->{default}
|| { count => -1, method => 'active' };
}
=head3 getPrefix
my $prefix = $abstract->getPrefix( {
brw_cat => $brw_cat,
branch => $branch_code,
} );
Return the ILL prefix as defined by our $params: either per borrower category,
per branch or the default.
=cut
sub getPrefix {
my ( $self, $params ) = @_;
my $brn_prefixes = $self->_config->getPrefixes('branch');
my $brw_prefixes = $self->_config->getPrefixes('brw_cat');
return $brw_prefixes->{$params->{brw_cat}}
|| $brn_prefixes->{$params->{branch}}
|| $brw_prefixes->{default}
|| ""; # "the empty prefix"
}
#### Illrequests Imports
=head3 check_limits
my $ok = $illRequests->check_limits( {
borrower => $borrower,
branchcode => 'branchcode' | undef,
} );
Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
see whether we are still able to place ILLs.
LimitRules are derived from koha-conf.xml:
+ default limit counts, and counting method
+ branch specific limit counts & counting method
+ borrower category specific limit counts & counting method
+ err on the side of caution: a counting fail will cause fail, even if
the other counts passes.
=cut
sub check_limits {
my ( $self, $params ) = @_;
my $patron = $params->{patron};
my $branchcode = $params->{librarycode} || $patron->branchcode;
# Establish maximum number of allowed requests
my ( $branch_rules, $brw_rules ) = (
$self->getLimits( {
type => 'branch',
value => $branchcode
} ),
$self->getLimits( {
type => 'brw_cat',
value => $patron->categorycode,
} ),
);
my ( $branch_limit, $brw_limit )
= ( $branch_rules->{count}, $brw_rules->{count} );
# Establish currently existing requests
my ( $branch_count, $brw_count ) = (
$self->_limit_counter(
$branch_rules->{method}, { branchcode => $branchcode }
),
$self->_limit_counter(
$brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
),
);
# Compare and return
# A limit of -1 means no limit exists.
# We return blocked if either branch limit or brw limit is reached.
if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
|| ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
return 0;
} else {
return 1;
}
}
sub _limit_counter {
my ( $self, $method, $target ) = @_;
# Establish parameters of counts
my $resultset;
if ($method && $method eq 'annual') {
$resultset = Koha::Illrequests->search({
-and => [
%{$target},
\"YEAR(placed) = YEAR(NOW())"
]
});
} else { # assume 'active'
# XXX: This status list is ugly. There should be a method in config
# to return these.
$where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
$resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
}
# Fetch counts
return $resultset->count;
}
=head3 requires_moderation
my $status = $illRequest->requires_moderation;
Return the name of the status if moderation by staff is required; or 0
otherwise.
=cut
sub requires_moderation {
my ( $self ) = @_;
my $require_moderation = {
'CANCREQ' => 'CANCREQ',
};
return $require_moderation->{$self->status};
}
=head3 generic_confirm
my $stage_summary = $illRequest->generic_confirm;
Handle the generic_confirm extended method. The first stage involves creating
a template email for the end user to edit in the browser. The second stage
attempts to submit the email.
=cut
sub generic_confirm {
my ( $self, $params ) = @_;
my $branch = Koha::Libraries->find($params->{current_branchcode})
|| die "Invalid current branchcode. Are you logged in as the database user?";
if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
my $draft->{subject} = "ILL Request";
$draft->{body} = <<EOF;
Dear Sir/Madam,
We would like to request an interlibrary loan for a title matching the
following description:
EOF
my $details = $self->metadata;
while (my ($title, $value) = each %{$details}) {
$draft->{body} .= " - " . $title . ": " . $value . "\n"
if $value;
}
$draft->{body} .= <<EOF;
Please let us know if you are able to supply this to us.
Kind Regards
EOF
my @address = map { $branch->$_ }
qw/ branchname branchaddress1 branchaddress2 branchaddress3
branchzip branchcity branchstate branchcountry branchphone
branchemail /;
my $address = "";
foreach my $line ( @address ) {
$address .= $line . "\n" if $line;
}
$draft->{body} .= $address;
my $partners = Koha::Patrons->search({
categorycode => $self->_config->partner_code
});
return {
error => 0,
status => '',
message => '',
method => 'generic_confirm',
stage => 'draft',
value => {
draft => $draft,
partners => $partners,
}
};
} elsif ( 'draft' eq $params->{stage} ) {
# Create the to header
my $to = $params->{partners};
$to =~ s/^\x00//; # Strip leading NULLs
$to =~ s/\x00/; /; # Replace others with '; '
die "No target email addresses found. Either select at least one partner or check your ILL partner library records." if ( !$to );
# Create the from, replyto and sender headers
my $from = $branch->branchemail;
my $replyto = $branch->branchreplyto || $from;
die "Your branch has no email address. Please set it."
if ( !$from );
# Create the email
my $message = Koha::Email->new;
my %mail = $message->create_message_headers(
{
to => $to,
from => $from,
replyto => $replyto,
subject => Encode::encode( "utf8", $params->{subject} ),
message => Encode::encode( "utf8", $params->{body} ),
contenttype => 'text/plain',
}
);
# Send it
my $result = sendmail(%mail);
if ( $result ) {
$self->status("GENREQ")->store;
return {
error => 0,
status => '',
message => '',
method => 'generic_confirm',
stage => 'commit',
next => 'illview',
};
} else {
return {
error => 1,
status => 'email_failed',
message => $Mail::Sendmail::error,
method => 'generic_confirm',
stage => 'draft',
};
}
} else {
die "Unknown stage, should not have happened."
}
}
=head3 id_prefix
my $prefix = $record->id_prefix;
Return the prefix appropriate for the current Illrequest as derived from the
borrower and branch associated with this request's Status, and the config
file.
=cut
sub id_prefix {
my ( $self ) = @_;
my $brw = $self->patron;
my $brw_cat = "dummy";
$brw_cat = $brw->categorycode
unless ( 'HASH' eq ref($brw) && $brw->{deleted} );
my $prefix = $self->getPrefix( {
brw_cat => $brw_cat,
branch => $self->branchcode,
} );
$prefix .= "-" if ( $prefix );
return $prefix;
}
=head3 _censor
my $params = $illRequest->_censor($params);
Return $params, modified to reflect our censorship requirements.
=cut
sub _censor {
my ( $self, $params ) = @_;
my $censorship = $self->_config->censorship;
$params->{censor_notes_staff} = $censorship->{censor_notes_staff}
if ( $params->{opac} );
$params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
return $params;
}
=head1 AUTHOR
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
=cut
1;

384
Koha/Illrequest/Config.pm Normal file
View File

@ -0,0 +1,384 @@
package Koha::Illrequest::Config;
# Copyright 2013,2014 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 C4::Context;
=head1 NAME
Koha::Illrequest::Config - Koha ILL Configuration Object
=head1 SYNOPSIS
Object-oriented class that giving access to the illconfig data derived
from ill/config.yaml.
=head1 DESCRIPTION
Config object providing abstract representation of the expected XML
returned by ILL API.
In particular the config object uses a YAML file, whose path is
defined by <illconfig> in koha-conf.xml. That YAML file provides the
data structure exposed in this object.
By default the configured data structure complies with fields used by
the British Library Interlibrary Loan DSS API.
The config file also provides mappings for Record Object accessors.
=head1 API
=head2 Class Methods
=head3 new
my $config = Koha::Illrequest::Config->new();
Create a new Koha::Illrequest::Config object, with mapping data loaded from the
ILL configuration file.
=cut
sub new {
my ( $class ) = @_;
my $self = {};
$self->{configuration} = _load_configuration(
C4::Context->config("interlibrary_loans"),
C4::Context->preference("UnmediatedILL")
);
bless $self, $class;
return $self;
}
=head3 backend
$backend = $config->backend($name);
$backend = $config->backend;
Standard setter/accessor for our backend.
=cut
sub backend {
my ( $self, $new ) = @_;
$self->{configuration}->{backend} = $new if $new;
return $self->{configuration}->{backend};
}
=head3 backend_dir
$backend_dir = $config->backend_dir($new_path);
$backend_dir = $config->backend_dir;
Standard setter/accessor for our backend_directory.
=cut
sub backend_dir {
my ( $self, $new ) = @_;
$self->{configuration}->{backend_directory} = $new if $new;
return $self->{configuration}->{backend_directory};
}
=head3 partner_code
$partner_code = $config->partner_code($new_code);
$partner_code = $config->partner_code;
Standard setter/accessor for our partner_code.
=cut
sub partner_code {
my ( $self, $new ) = @_;
$self->{configuration}->{partner_code} = $new if $new;
return $self->{configuration}->{partner_code};
}
=head3 limits
$limits = $config->limits($limitshash);
$limits = $config->limits;
Standard setter/accessor for our limits. No parsing is performed on
$LIMITSHASH, so caution should be exercised when using this setter.
=cut
sub limits {
my ( $self, $new ) = @_;
$self->{configuration}->{limits} = $new if $new;
return $self->{configuration}->{limits};
}
=head3 getPrefixes
my $prefixes = $config->getPrefixes('brw_cat' | 'branch');
Return the prefix for ILLs defined by our config.
=cut
sub getPrefixes {
my ( $self, $type ) = @_;
die "Unexpected type." unless ( $type eq 'brw_cat' || $type eq 'branch' );
my $values = $self->{configuration}->{prefixes}->{$type};
$values->{default} = $self->{configuration}->{prefixes}->{default};
return $values;
}
=head3 getLimitRules
my $rules = $config->getLimitRules('brw_cat' | 'branch')
Return the hash of ILL limit rules defined by our config.
=cut
sub getLimitRules {
my ( $self, $type ) = @_;
die "Unexpected type." unless ( $type eq 'brw_cat' || $type eq 'branch' );
my $values = $self->{configuration}->{limits}->{$type};
$values->{default} = $self->{configuration}->{limits}->{default};
return $values;
}
=head3 getDigitalRecipients
my $recipient_rules= $config->getDigitalRecipients('brw_cat' | 'branch');
Return the hash of digital_recipient settings defined by our config.
=cut
sub getDigitalRecipients {
my ( $self, $type ) = @_;
die "Unexpected type." unless ( $type eq 'brw_cat' || $type eq 'branch' );
my $values = $self->{configuration}->{digital_recipients}->{$type};
$values->{default} =
$self->{configuration}->{digital_recipients}->{default};
return $values;
}
=head3 censorship
my $censoredValues = $config->censorship($hash);
my $censoredValues = $config->censorship;
Standard setter/accessor for our limits. No parsing is performed on $HASH, so
caution should be exercised when using this setter.
Return our censorship values for the OPAC as loaded from the koha-conf.xml, or
the fallback value (no censorship).
=cut
sub censorship {
my ( $self, $new ) = @_;
$self->{configuration}->{censorship} = $new if $new;
return $self->{configuration}->{censorship};
}
=head3 _load_configuration
my $configuration = $config->_load_configuration($config_from_xml);
Read the configuration values passed as the parameter, and populate a hashref
suitable for use with these.
A key task performed here is the parsing of the input in the configuration
file to ensure we have only valid input there.
=cut
sub _load_configuration {
my ( $xml_config, $unmediated ) = @_;
my $xml_backend_dir = $xml_config->{backend_directory};
# Default data structure to be returned
my $configuration = {
backend_directory => $xml_backend_dir,
censorship => {
censor_notes_staff => 0,
censor_reply_date => 0,
},
limits => {},
digital_recipients => {},
prefixes => {},
partner_code => 'ILLLIBS',
raw_config => $xml_config,
};
# Per Branch Configuration
my $branches = $xml_config->{branch};
if ( ref($branches) eq "ARRAY" ) {
# Multiple branch overrides defined
map {
_load_unit_config({
unit => $_,
id => $_->{code},
config => $configuration,
type => 'branch'
})
} @{$branches};
} elsif ( ref($branches) eq "HASH" ) {
# Single branch override defined
_load_unit_config({
unit => $branches,
id => $branches->{code},
config => $configuration,
type => 'branch'
});
}
# Per Borrower Category Configuration
my $brw_cats = $xml_config->{borrower_category};
if ( ref($brw_cats) eq "ARRAY" ) {
# Multiple borrower category overrides defined
map {
_load_unit_config({
unit => $_,
id => $_->{code},
config => $configuration,
type => 'brw_cat'
})
} @{$brw_cats};
} elsif ( ref($brw_cats) eq "HASH" ) {
# Single branch override defined
_load_unit_config({
unit => $brw_cats,
id => $brw_cats->{code},
config => $configuration,
type => 'brw_cat'
});
}
# Default Configuration
_load_unit_config({
unit => $xml_config,
id => 'default',
config => $configuration
});
# Censorship
my $staff_comments = $xml_config->{staff_request_comments} || 0;
$configuration->{censorship}->{censor_notes_staff} = 1
if ( $staff_comments && 'hide' eq $staff_comments );
my $reply_date = $xml_config->{reply_date} || 0;
$configuration->{censorship}->{censor_reply_date} = 1
if ( $reply_date && 'hide' eq $reply_date );
# ILL Partners
$configuration->{partner_code} = $xml_config->{partner_code} || 'ILLLIBS';
die "No DEFAULT_FORMATS has been defined in koha-conf.xml, but UNMEDIATEDILL is active."
if ( $unmediated && !$configuration->{default_formats}->{default} );
return $configuration;
}
=head3 _load_unit_config
my $configuration->{part} = _load_unit_config($params);
$PARAMS is a hashref with the following elements:
- unit: the part of the configuration we are parsing.
- id: the name within which we will store the parsed unit in config.
- config: the configuration we are augmenting.
- type: the type of config unit we are parsing. Assumed to be 'default'.
Read `unit', and augment `config' with these under `id'.
This is a helper for _load_configuration.
A key task performed here is the parsing of the input in the configuration
file to ensure we have only valid input there.
=cut
sub _load_unit_config {
my ( $params ) = @_;
my $unit = $params->{unit};
my $id = $params->{id};
my $config = $params->{config};
my $type = $params->{type};
die "TYPE should be either 'branch' or 'brw_cat' if ID is not 'default'."
if ( $id ne 'default' && ( $type ne 'branch' && $type ne 'brw_cat') );
return $config unless $id;
if ( $unit->{api_key} && $unit->{api_auth} ) {
$config->{credentials}->{api_keys}->{$id} = {
api_key => $unit->{api_key},
api_auth => $unit->{api_auth},
};
}
# Add request_limit rules.
# METHOD := 'annual' || 'active'
# COUNT := x >= -1
if ( ref $unit->{request_limit} eq 'HASH' ) {
my $method = $unit->{request_limit}->{method};
my $count = $unit->{request_limit}->{count};
if ( 'default' eq $id ) {
$config->{limits}->{$id}->{method} = $method
if ( $method && ( 'annual' eq $method || 'active' eq $method ) );
$config->{limits}->{$id}->{count} = $count
if ( $count && ( -1 <= $count ) );
} else {
$config->{limits}->{$type}->{$id}->{method} = $method
if ( $method && ( 'annual' eq $method || 'active' eq $method ) );
$config->{limits}->{$type}->{$id}->{count} = $count
if ( $count && ( -1 <= $count ) );
}
}
# Add prefix rules.
# PREFIX := string
if ( $unit->{prefix} ) {
if ( 'default' eq $id ) {
$config->{prefixes}->{$id} = $unit->{prefix};
} else {
$config->{prefixes}->{$type}->{$id} = $unit->{prefix};
}
}
# Add digital_recipient rules.
# DIGITAL_RECIPIENT := borrower || branch (defaults to borrower)
if ( $unit->{digital_recipient} ) {
if ( 'default' eq $id ) {
$config->{digital_recipients}->{$id} = $unit->{digital_recipient};
} else {
$config->{digital_recipients}->{$type}->{$id} =
$unit->{digital_recipient};
}
}
return $config;
}
=head1 AUTHOR
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
=cut
1;

View File

@ -0,0 +1,51 @@
package Koha::Illrequestattribute;
# Copyright PTFS Europe 2016
#
# 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 Koha::Database;
use base qw(Koha::Object);
=head1 NAME
Koha::Illrequestattribute - Koha Illrequestattribute Object class
=head1 API
=head2 Class Methods
=cut
=head3 type
=cut
sub _type {
return 'Illrequestattribute';
}
=head1 AUTHOR
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
=cut
1;

View File

@ -0,0 +1,55 @@
package Koha::Illrequestattributes;
# Copyright PTFS Europe 2016
#
# 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 Koha::Database;
use Koha::Illrequestattribute;
use base qw(Koha::Objects);
=head1 NAME
Koha::Illrequestattributes - Koha Illrequestattributes Object class
=head1 API
=head2 Class Methods
=cut
=head3 type
=cut
sub _type {
return 'Illrequestattribute';
}
sub object_class {
return 'Koha::Illrequestattribute';
}
=head1 AUTHOR
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
=cut
1;

97
Koha/Illrequests.pm Normal file
View File

@ -0,0 +1,97 @@
package Koha::Illrequests;
# Copyright PTFS Europe 2016
#
# 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 Koha::Database;
use Koha::Illrequest;
use Koha::Illrequest::Config;
use base qw(Koha::Objects);
=head1 NAME
Koha::Illrequests - Koha Illrequests Object class
=head1 API
=head2 Class Methods
=cut
=head3 type
=cut
sub _type {
return 'Illrequest';
}
sub object_class {
return 'Koha::Illrequest';
}
##### To be implemented Facade
=head3 new
my $illRequests = Koha::Illrequests->new();
Create an ILLREQUESTS object, a singleton through which we can interact with
ILLREQUEST objects stored in the database or search for ILL candidates at API
backends.
=cut
sub new {
my ( $class, $attributes ) = @_;
my $self = $class->SUPER::new($class, $attributes);
my $config = Koha::Illrequest::Config->new; # <- Necessary
$self->{_config} = $config; # <- Necessary
return $self;
}
=head3 search_incomplete
my $requests = $illRequests->search_incomplete;
A specialised version of `search`, returning all requests currently
not considered completed.
=cut
sub search_incomplete {
my ( $self ) = @_;
$self->search( {
status => [
-and => { '!=', 'COMP' }, { '!=', 'GENCOMP' }
]
} );
}
=head1 AUTHOR
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
=cut
1;

View File

@ -0,0 +1,85 @@
package Koha::REST::V1::Illrequests;
# 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 Mojo::Base 'Mojolicious::Controller';
use Koha::Illrequests;
use Koha::Library;
sub list {
my ($c, $args, $cb) = @_;
my $filter;
$args //= {};
my $output = [];
# Create a hash where all keys are embedded values
# Enables easy checking
my %embed;
if (defined $args->{embed}) {
%embed = map { $_ => 1 } @{$args->{embed}};
delete $args->{embed};
}
for my $filter_param ( keys %$args ) {
my @values = split(/,/, $args->{$filter_param});
$filter->{$filter_param} = \@values;
}
my $requests = Koha::Illrequests->search($filter);
while (my $request = $requests->next) {
my $unblessed = $request->unblessed;
# Add the request's id_prefix
$unblessed->{id_prefix} = $request->id_prefix;
# Augment the request response with patron details
# if appropriate
if (defined $embed{patron}) {
my $patron = $request->patron;
$unblessed->{patron} = {
firstname => $patron->firstname,
surname => $patron->surname,
cardnumber => $patron->cardnumber
};
}
# Augment the request response with metadata details
# if appropriate
if (defined $embed{metadata}) {
$unblessed->{metadata} = $request->metadata;
}
# Augment the request response with status details
# if appropriate
if (defined $embed{capabilities}) {
$unblessed->{capabilities} = $request->capabilities;
}
# Augment the request response with branch details
# if appropriate
if (defined $embed{branch}) {
$unblessed->{branch} = Koha::Libraries->find(
$request->branchcode
)->unblessed;
}
push @{$output}, $unblessed
}
return $c->$cb( $output, 200 );
}
1;

View File

@ -312,6 +312,7 @@ my $target_map = {
'./etc/zebradb' => { target => 'ZEBRA_CONF_DIR', trimdir => -1 },
'./etc/pazpar2' => { target => 'PAZPAR2_CONF_DIR', trimdir => -1 },
'./help.pl' => 'INTRANET_CGI_DIR',
'./ill' => 'INTRANET_CGI_DIR',
'./installer-CPAN.pl' => 'NONE',
'./installer' => 'INTRANET_CGI_DIR',
'./errors' => {target => 'INTRANET_CGI_DIR'},

View File

@ -22,5 +22,8 @@
},
"/patrons/{borrowernumber}": {
"$ref": "paths/patrons.json#/~1patrons~1{borrowernumber}"
},
"/illrequests": {
"$ref": "paths/illrequests.json#/~1illrequests"
}
}

View File

@ -0,0 +1,98 @@
{
"/illrequests": {
"get": {
"x-mojo-controller": "Koha::REST::V1::Illrequests",
"operationId": "list",
"tags": ["illrequests"],
"parameters": [{
"name": "embed",
"in": "query",
"description": "Additional objects that should be embedded in the response",
"required": false,
"type": "array",
"collectionFormat": "csv",
"items": {
"type": "string",
"enum": [
"patron",
"branch",
"capabilities"
]
}
}, {
"name": "backend",
"in": "query",
"description": "The name of a ILL backend",
"required": false,
"type": "string"
}, {
"name": "orderid",
"in": "query",
"description": "The order ID of a request",
"required": false,
"type": "string"
}, {
"name": "biblio_id",
"in": "query",
"description": "The biblio ID associated with a request",
"required": false,
"type": "integer"
}, {
"name": "borrower_id",
"in": "query",
"description": "The borrower ID associated with a request",
"required": false,
"type": "integer"
}, {
"name": "completed",
"in": "query",
"description": "The date the request was considered completed",
"required": false,
"type": "string"
}, {
"name": "status",
"in": "query",
"description": "A full status string e.g. REQREV",
"required": false,
"type": "string"
}, {
"name": "medium",
"in": "query",
"description": "The medium of the requested item",
"required": false,
"type": "string"
}, {
"name": "updated",
"in": "query",
"description": "The last updated date of the request",
"required": false,
"type": "string"
}, {
"name": "placed",
"in": "query",
"description": "The date the request was placed",
"required": false,
"type": "string"
}, {
"name": "branch_id",
"in": "query",
"description": "The ID of the pickup branch",
"required": false,
"type": "string"
}],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "OK"
}
},
"x-koha-authorization": {
"permissions": {
"borrowers": "1"
}
}
}
}
}

View File

@ -153,5 +153,26 @@ __PAZPAR2_TOGGLE_XML_POST__
<plack_max_requests>50</plack_max_requests>
<plack_workers>2</plack_workers>
<interlibrary_loans>
<!-- Path to where Illbackends are located on the system
- This setting should normally not be touched -->
<backend_directory>__PERL_MODULE_DIR__/Koha/Illbackends</backend_directory>
<!-- How should we treat staff comments?
- hide: don't show in OPAC
- show: show in OPAC -->
<staff_request_comments>hide</staff_request_comments>
<!-- How should we treat the reply_date field?
- hide: don't show this field in the UI
- any other string: show, with this label -->
<reply_date>hide</reply_date>
<!-- Where should digital ILLs be sent?
- borrower: send it straight to the borrower email
- branch: send the ILL to the branch email -->
<digital_recipient>branch</digital_recipient>
<!-- What patron category should we use for p2p ILL requests?
- By default this is set to 'ILLLIBS' -->
<partner_code>ILLLIBS</partner_code>
</interlibrary_loans>
</config>
</yazgfs>

252
ill/ill-requests.pl Executable file
View File

@ -0,0 +1,252 @@
#!/usr/bin/perl
# Copyright 2013 PTFS-Europe Ltd and Mark Gavillet
# Copyright 2014 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 CGI;
use C4::Auth;
use C4::Output;
use Koha::AuthorisedValues;
use Koha::Illrequests;
use Koha::Libraries;
my $cgi = CGI->new;
my $illRequests = Koha::Illrequests->new;
# Grab all passed data
# 'our' since Plack changes the scoping
# of 'my'
our $params = $cgi->Vars();
my $op = $params->{method} || 'illlist';
my ( $template, $patronnumber, $cookie ) = get_template_and_user( {
template_name => 'ill/ill-requests.tt',
query => $cgi,
type => 'intranet',
flagsrequired => { ill => '*' },
} );
if ( $op eq 'illview' ) {
# View the details of an ILL
my $request = Koha::Illrequests->find($params->{illrequest_id});
$template->param(
request => $request
);
} 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);
} elsif ( $op eq 'confirm' ) {
# Backend 'confirm' method
# confirm requires a specific request, so first, find it.
my $request = Koha::Illrequests->find($params->{illrequest_id});
my $backend_result = $request->backend_confirm($params);
$template->param(
whole => $backend_result,
request => $request,
);
# handle special commit rules & update type
handle_commit_maybe($backend_result, $request);
} elsif ( $op eq 'cancel' ) {
# Backend 'cancel' method
# cancel requires a specific request, so first, find it.
my $request = Koha::Illrequests->find($params->{illrequest_id});
my $backend_result = $request->backend_cancel($params);
$template->param(
whole => $backend_result,
request => $request,
);
# handle special commit rules & update type
handle_commit_maybe($backend_result, $request);
} elsif ( $op eq 'edit_action' ) {
# Handle edits to the Illrequest object.
# (not the Illrequestattributes)
# We simulate the API for backend requests for uniformity.
# So, init:
my $request = Koha::Illrequests->find($params->{illrequest_id});
if ( !$params->{stage} ) {
my $backend_result = {
error => 0,
status => '',
message => '',
method => 'edit_action',
stage => 'init',
next => '',
value => {}
};
$template->param(
whole => $backend_result,
request => $request
);
} else {
# Commit:
# Save the changes
$request->borrowernumber($params->{borrowernumber});
$request->biblio_id($params->{biblio_id});
$request->branchcode($params->{branchcode});
$request->notesopac($params->{notesopac});
$request->notesstaff($params->{notesstaff});
$request->store;
my $backend_result = {
error => 0,
status => '',
message => '',
method => 'edit_action',
stage => 'commit',
next => 'illlist',
value => {}
};
handle_commit_maybe($backend_result, $request);
}
} elsif ( $op eq 'moderate_action' ) {
# Moderate action is required for an ILL submodule / syspref.
# Currently still needs to be implemented.
redirect_to_list();
} elsif ( $op eq 'delete_confirm') {
my $request = Koha::Illrequests->find($params->{illrequest_id});
$template->param(
request => $request
);
} elsif ( $op eq 'delete' ) {
# Check if the request is confirmed, if not, redirect
# to the confirmation view
if ($params->{confirmed} == 1) {
# We simply delete the request...
my $request = Koha::Illrequests->find(
$params->{illrequest_id}
)->delete;
# ... then return to list view.
redirect_to_list();
} else {
print $cgi->redirect(
"/cgi-bin/koha/ill/ill-requests.pl?" .
"method=delete_confirm&illrequest_id=" .
$params->{illrequest_id});
}
} elsif ( $op eq 'mark_completed' ) {
my $request = Koha::Illrequests->find($params->{illrequest_id});
my $backend_result = $request->mark_completed($params);
$template->param(
whole => $backend_result,
request => $request,
);
# handle special commit rules & update type
handle_commit_maybe($backend_result, $request);
} elsif ( $op eq 'generic_confirm' ) {
my $request = Koha::Illrequests->find($params->{illrequest_id});
$params->{current_branchcode} = C4::Context->mybranch;
my $backend_result = $request->generic_confirm($params);
$template->param(
whole => $backend_result,
request => $request,
);
# handle special commit rules & update type
handle_commit_maybe($backend_result, $request);
} elsif ( $op eq 'illlist') {
# Display all current ILLs
my $requests = $illRequests->search();
$template->param(
requests => $requests
);
# If we receive a pre-filter, make it available to the template
my $possible_filters = ['borrowernumber'];
my $active_filters = [];
foreach my $filter(@{$possible_filters}) {
if ($params->{$filter}) {
push @{$active_filters},
{ name => $filter, value => $params->{$filter}};
}
}
if (scalar @{$active_filters} > 0) {
$template->param(
prefilters => $active_filters
);
}
} else {
my $request = Koha::Illrequests->find($params->{illrequest_id});
my $backend_result = $request->custom_capability($op, $params);
$template->param(
whole => $backend_result,
request => $request,
);
# handle special commit rules & update type
handle_commit_maybe($backend_result, $request);
}
# Get a list of backends
my $ir = Koha::Illrequest->new;
$template->param(
backends => $ir->available_backends,
media => [ "Book", "Article", "Journal" ],
query_type => $op,
branches => Koha::Libraries->search->unblessed,
here_link => "/cgi-bin/koha/ill/ill-requests.pl"
);
output_html_with_http_headers( $cgi, $cookie, $template->output );
sub handle_commit_maybe {
my ( $backend_result, $request ) = @_;
# We need to special case 'commit'
if ( $backend_result->{stage} eq 'commit' ) {
if ( $backend_result->{next} eq 'illview' ) {
# Redirect to a view of the newly created request
print $cgi->redirect(
'/cgi-bin/koha/ill/ill-requests.pl?method=illview&illrequest_id='.
$request->id
);
} else {
# Redirect to a requests list view
redirect_to_list();
}
}
}
sub redirect_to_list {
print $cgi->redirect('/cgi-bin/koha/ill/ill-requests.pl');
}

View File

@ -3049,3 +3049,95 @@ fieldset.rows + fieldset.action {
#patron_search #filters {
display: none;
}
#interlibraryloans h1 {
margin: 1em 0;
}
#interlibraryloans h2 {
margin-bottom: 20px;
}
#interlibraryloans h3 {
margin-top: 20px;
}
#interlibraryloans .bg-info {
overflow: auto;
position: relative;
}
#interlibraryloans #search-summary {
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
-o-transform: translateY(-50%);
transform: translateY(-50%);
position: absolute;
top: 50%;
}
#interlibraryloans .format h5 {
margin-top: 20px;
}
#interlibraryloans .format li {
list-style: none;
}
#interlibraryloans .format h4 {
margin-bottom: 20px;
}
#interlibraryloans .format input {
margin: 10px 0;
}
#interlibraryloans #freeform-fields .custom-name {
width: 9em;
margin-right: 1em;
text-align: right;
}
#interlibraryloans #freeform-fields .delete-new-field {
margin-left: 1em;
}
#interlibraryloans #add-new-fields {
margin: 1em;
}
#interlibraryloans #column-toggle,
#interlibraryloans #reset-toggle {
margin: 15px 0;
line-height: 1.5em;
font-weight: 700;
}
#ill-view-panel {
margin-top: 15px;
}
#ill-view-panel h3 {
margin-bottom: 10px;
}
#ill-view-panel h4 {
margin-bottom: 20px;
}
#ill-view-panel .rows div {
height: 1em;
margin-bottom: 1em;
}
#ill-view-panel #requestattributes .label {
width: auto;
}
table#ill-requests {
width: 100% !important;
}
table#ill-requests th {
text-transform: capitalize;
}

View File

@ -117,5 +117,8 @@
[% IF Koha.Preference('HouseboundModule') %]
[% IF houseboundview %]<li class="active">[% ELSE %]<li>[% END %]<a href="/cgi-bin/koha/members/housebound.pl?borrowernumber=[% borrowernumber %]">Housebound</a></li>
[% END %]
[% IF Koha.Preference('ILLModule') %]
<li><a href="/cgi-bin/koha/ill/ill-requests.pl?borrowernumber=[% borrowernumber %]">Interlibrary loans</a></li>
[% END %]
</ul></div>
[% END %]

View File

@ -0,0 +1,24 @@
[% USE Koha %]
[% IF Koha.Preference('ILLModule ') %]
<div id="toolbar" class="btn-toolbar">
[% IF backends.size > 1 %]
<div class="dropdown btn-group">
<button class="btn btn-sm btn-default dropdown-toggle" type="button" id="ill-backend-dropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fa fa-plus"></i> New ILL request <span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="ill-backend-dropdown">
[% FOREACH backend IN backends %]
<li><a href="/cgi-bin/koha/ill/ill-requests.pl?method=create&amp;backend=[% backend %]">[% backend %]</a></li>
[% END %]
</ul>
</div>
[% ELSE %]
<a id="ill-new" class="btn btn-sm btn-default" href="/cgi-bin/koha/ill/ill-requests.pl?method=create&amp;backend=[% backends.0 %]">
<i class="fa fa-plus"></i> New ILL request
</a>
[% END %]
<a id="ill-list" class="btn btn-sm btn-default btn-group" href="/cgi-bin/koha/ill/ill-requests.pl">
<i class="fa fa-list"></i> List requests
</a>
</div>
[% END %]

View File

@ -20,6 +20,7 @@
[%- CASE 'plugins' -%]<span>Koha plugins</span>
[%- CASE 'lists' -%]<span>Lists</span>
[%- CASE 'clubs' -%]<span>Patron clubs</span>
[%- CASE 'ill' -%]<span>Create and modify Interlibrary loan requests</span>
[%- END -%]
[%- END -%]

View File

@ -0,0 +1,698 @@
[% USE Branches %]
[% USE Koha %]
[% INCLUDE 'doc-head-open.inc' %]
<title>Koha &rsaquo; ILL requests &rsaquo;</title>
[% INCLUDE 'doc-head-close.inc' %]
<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.tablesorter.min.js"></script>
<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.checkboxes.min.js"></script>
<link rel="stylesheet" type="text/css" href="[% interface %]/[% theme %]/css/datatables.css">
[% INCLUDE 'datatables.inc' %]
<script type="text/javascript">
//<![CDATA[
$(document).ready(function() {
// Illview Datatable setup
// Fields we don't want to display
var ignore = [
'accessurl',
'backend',
'completed',
'branch',
'capabilities',
'cost',
'medium',
'notesopac',
'notesstaff',
'placed',
'replied'
];
// Fields we need to expand (flatten)
var expand = [
'metadata',
'patron'
];
// Expanded fields
// This is auto populated
var expanded = {};
// The core fields that should be displayed first
var core = [
'metadata_Author',
'metadata_Title',
'borrowername',
'biblio_id',
'branchcode',
'status',
'updated',
'illrequest_id',
'action'
];
// Extra fields that we need to tack on to the end
var extra = [ 'action' ];
// Remove any fields we're ignoring
var removeIgnore = function(dataObj) {
dataObj.forEach(function(thisRow) {
ignore.forEach(function(thisIgnore) {
if (thisRow.hasOwnProperty(thisIgnore)) {
delete thisRow[thisIgnore];
}
});
});
};
// Expand any fields we're expanding
var expandExpand = function(row) {
expand.forEach(function(thisExpand) {
if (row.hasOwnProperty(thisExpand)) {
if (!expanded.hasOwnProperty(thisExpand)) {
expanded[thisExpand] = [];
}
var expandObj = row[thisExpand];
Object.keys(expandObj).forEach(
function(thisExpandCol) {
var expColName = thisExpand + '_' + thisExpandCol;
// Keep a list of fields that have been expanded
// so we can create toggle links for them
if (expanded[thisExpand].indexOf(expColName) == -1) {
expanded[thisExpand].push(expColName);
}
expandObj[expColName] =
expandObj[thisExpandCol];
delete expandObj[thisExpandCol];
}
);
$.extend(true, row, expandObj);
delete row[thisExpand];
}
});
};
// Build a de-duped list of all column names
var allCols = {};
core.map(function(thisCore) {
allCols[thisCore] = 1;
});
var unionColumns = function(row) {
Object.keys(row).forEach(function(col) {
if (ignore.indexOf(col) == -1) {
allCols[col] = 1;
}
});
};
// Some rows may not have fields that other rows have,
// so make sure all rows have the same fields
var fillMissing = function(row) {
Object.keys(allCols).forEach(function(thisCol) {
row[thisCol] = (!row.hasOwnProperty(thisCol)) ?
null :
row[thisCol];
});
}
// Strip the expand prefix if it exists, we do this for display
var stripPrefix = function(value) {
expand.forEach(function(thisExpand) {
var regex = new RegExp(thisExpand + '_', 'g');
value = value.replace(regex, '');
});
return value;
};
// Our 'render' function for borrowerlink
var createBorrowerLink = function(data, type, row) {
return '<a title="View borrower details" ' +
'href="/cgi-bin/koha/members/moremember.pl?' +
'borrowernumber='+row.borrowernumber+'">' +
row.patron_firstname + ' ' + row.patron_surname +
'</a>';
};
// Render function for request ID
var createRequestId = function(data, type, row) {
return row.id_prefix + row.illrequest_id;
};
// Render function for request status
var createStatus = function(data, type, row, meta) {
var origData = meta.settings.oInit.originalData;
if (origData.length > 0) {
return meta.settings.oInit.originalData[0].capabilities[
row.status
].name;
} else {
return '';
}
};
// Render function for creating a row's action link
var createActionLink = function(data, type, row) {
return '<a class="btn btn-default btn-sm" ' +
'href="/cgi-bin/koha/ill/ill-requests.pl?' +
'method=illview&amp;illrequest_id=' +
row.illrequest_id +
'">Manage request</a>' +
'</div>'
};
// Columns that require special treatment
var specialCols = {
action: {
name: '',
func: createActionLink
},
borrowername: {
name: 'Borrower',
func: createBorrowerLink
},
illrequest_id: {
name: 'Request number',
func: createRequestId
},
status: {
name: 'Status',
func: createStatus
},
biblio_id: {
name: 'Biblio number'
},
branchcode: {
name: 'Branch code'
}
};
// Helper for handling prefilter column names
function toColumnName(myVal) {
return myVal
.replace(/^filter/, '')
.replace(/([A-Z])/g, "_$1")
.replace(/^_/,'').toLowerCase();
};
// Toggle request attributes in Illview
$('#toggle_requestattributes').click(function() {
$('#requestattributes').toggleClass('content_hidden');
});
// Filter partner list
$('#partner_filter').keyup(function() {
var needle = $('#partner_filter').val();
$('#partners > option').each(function() {
var regex = new RegExp(needle, 'i');
if (
needle.length == 0 ||
$(this).is(':selected') ||
$(this).text().match(regex)
) {
$(this).show();
} else {
$(this).hide();
}
});
});
// Get our data from the API and process it prior to passing
// it to datatables
var ajax = $.ajax(
'/api/v1/illrequests?embed=metadata,patron,capabilities,branch'
).done(function() {
var data = JSON.parse(ajax.responseText);
// Make a copy, we'll be removing columns next and need
// to be able to refer to data that has been removed
var dataCopy = $.extend(true, [], data);
// Remove all columns we're not interested in
removeIgnore(dataCopy);
// Expand columns that need it and create an array
// of all column names
$.each(dataCopy, function(k, row) {
expandExpand(row);
unionColumns(row);
});
// Append any extra columns we need to tag on
if (extra.length > 0) {
extra.forEach(function(thisExtra) {
allCols[thisExtra] = 1;
});
};
// Different requests will have different columns,
// make sure they all have the same
$.each(dataCopy, function(k, row) {
fillMissing(row);
});
// Assemble an array of column definitions for passing
// to datatables
var colData = [];
Object.keys(allCols).forEach(function(thisCol) {
// We may have defined a pretty name for this column
var colName = (
specialCols.hasOwnProperty(thisCol) &&
specialCols[thisCol].hasOwnProperty('name')
) ?
specialCols[thisCol].name :
thisCol;
// Create the table header for this column
var str = '<th>' + stripPrefix(colName) + '</th>';
$(str).appendTo('#illview-header');
// Create the base column object
var colObj = {
name: thisCol,
className: thisCol
};
// We may need to process the data going in this
// column, so do it if necessary
if (
specialCols.hasOwnProperty(thisCol) &&
specialCols[thisCol].hasOwnProperty('func')
) {
colObj.render = specialCols[thisCol].func;
} else {
colObj.data = thisCol
}
colData.push(colObj);
});
// Create the toggle links for all metadata fields
var links = [];
expanded.metadata.forEach(function(thisExpanded) {
if (core.indexOf(thisExpanded) == -1) {
links.push(
'<a href="#" class="toggle-vis" data-column="' +
thisExpanded + '">' + stripPrefix(thisExpanded) +
'</a>'
);
}
});
$('#column-toggle').append(links.join(' | '));
// Initialise the datatable
var myTable = $('#ill-requests').DataTable($.extend(true, {}, dataTablesDefaults, {
aoColumnDefs: [ // Last column shouldn't be sortable or searchable
{
aTargets: [ 'action' ],
bSortable: false,
bSearchable: false
},
],
aaSorting: [[ 6, 'desc' ]], // Default sort, updated descending
processing: true, // Display a message when manipulating
language: {
loadingRecords: "Please wait - loading requests...",
zeroRecords: "No requests were found"
},
iDisplayLength: 10, // 10 results per page
sPaginationType: "full_numbers", // Pagination display
deferRender: true, // Improve performance on big datasets
data: dataCopy,
columns: colData,
originalData: data // Enable render functions to access
// our original data
}));
// Reset columns to default
var resetColumns = function() {
Object.keys(allCols).forEach(function(thisCol) {
myTable.column(thisCol + ':name').visible(core.indexOf(thisCol) != -1);
});
myTable.columns.adjust().draw(false);
};
// Handle the click event on a toggle link
$('a.toggle-vis').on('click', function(e) {
e.preventDefault();
var column = myTable.column(
$(this).data('column') + ':name'
);
column.visible(!column.visible());
});
// Reset column toggling
$('#reset-toggle').click(function() {
resetColumns();
});
// Handle a prefilter request and do the prefiltering
var filters = $('#ill-requests').data();
if (typeof filters !== 'undefined') {
var filterNames = Object.keys(filters).filter(
function(thisData) {
return thisData.match(/^filter/);
}
);
filterNames.forEach(function(thisFilter) {
var filterName = toColumnName(thisFilter) + ':name';
var regex = '^'+filters[thisFilter]+'$';
console.log(regex);
myTable.columns(filterName).search(regex, true, false);
});
myTable.draw();
}
// Initialise column hiding
resetColumns();
}
);
});
//]]>
</script>
</head>
<body id="acq_suggestion" class="acq">
[% INCLUDE 'header.inc' %]
[% INCLUDE 'cat-search.inc' %]
<div id="breadcrumbs">
<a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo;
[% IF query_type == 'create' %]
<a href=[% parent %]>ILL requests</a> &rsaquo; New request
[% ELSIF query_type == 'status' %]
<a href=[% parent %]>ILL requests</a> &rsaquo; Status
[% ELSE %]
ILL requests
[% END %]
</div>
<div id="doc3" class="yui-t2">
<div id="bd">
<div id="yui-main">
<div id="interlibraryloans" class="yui-b">
[% INCLUDE 'ill-toolbar.inc' %]
[% IF whole.error %]
<h1>Error performing operation</h1>
<!-- Dispatch on Status -->
<p>We encountered an error:</p>
<p>
<pre>[% whole.message %] ([% whole.status %])</pre>
</p>
[% END %]
[% IF query_type == 'create' %]
<h1>New ILL request</h1>
[% IF whole.stage == 'copyrightclearance' %]
<div>
<p>
[% Koha.Preference('ILLModuleCopyrightClearance') %]
</p>
<a href="?method=create&stage=copyrightclearance&backend=[% whole.value.backend %]"
class="btn btn-sm btn-default btn-group"><i class="fa fa-check">Yes</i></a>
<a href="/cgi-bin/koha/ill/ill-requests.pl"
class="btn btn-sm btn-default btn-group"><i class="fa fa-times">No</i></a>
</div>
[% ELSE %]
[% PROCESS $whole.template %]
[% END %]
[% ELSIF query_type == 'confirm' %]
<h1>Confirm ILL request</h1>
[% PROCESS $whole.template %]
[% ELSIF query_type == 'cancel' and !whole.error %]
<h1>Cancel a confirmed request</h1>
[% PROCESS $whole.template %]
[% ELSIF query_type == 'generic_confirm' %]
<h1>Place request with partner libraries</h1>
<!-- Start of GENERIC_EMAIL case -->
[% IF whole.value.partners %]
[% ill_url = here_link _ "?method=illview&illrequest_id=" _ request.illrequest_id %]
<form method="POST" action=[% here_link %]>
<fieldset class="rows">
<legend>Interlibrary loan request details</legend>
<ol>
<li>
<label for="partner_filter">Filter partner libraries:</label>
<input type="text" id="partner_filter">
</li>
<li>
<label for="partners">Select partner libraries:</label>
<select size="5" multiple="true" id="partners"
name="partners">
[% FOREACH partner IN whole.value.partners %]
<option value=[% partner.email %]>
[% partner.branchcode _ " - " _ partner.surname %]
</option>
[% END %]
</select>
</li>
<li>
<label for="subject">Subject Line</label>
<input type="text" name="subject"
id="subject" type="text"
value="[% whole.value.draft.subject %]"/>
</li>
<li>
<label for="body">Email text:</label>
<textarea name="body" id="body" rows="20" cols="80">[% whole.value.draft.body %]</textarea>
</li>
</ol>
<input type="hidden" value="generic_confirm" name="method">
<input type="hidden" value="draft" name="stage">
<input type="hidden" value="[% request.illrequest_id %]" name="illrequest_id">
</fieldset>
<fieldset class="action">
<input type="submit" class="btn btn-default" value="Send email"/>
<span><a href="[% ill_url %]" title="Return to request details">Cancel</a></span>
</fieldset>
</form>
[% ELSE %]
<fieldset class="rows">
<legend>Interlibrary loan request details</legend>
<p>No partners have been defined yet. Please create appropriate patron records (by default ILLLIBS category).</p>
<p>Be sure to provide email addresses for these patrons.</p>
<p><span><a href="[% ill_url %]" title="Return to request details">Cancel</a></span></p>
</fieldset>
[% END %]
<!-- generic_confirm ends here -->
[% ELSIF query_type == 'edit_action' %]
<form method="POST" action=[% here_link %]>
<fieldset class="rows">
<legend>Request details</legend>
<ol>
<li class="borrowernumber">
<label for="borrowernumber">Borrower number:</label>
<input name="borrowernumber" id="borrowernumber" type="text" value="[% request.borrowernumber %]">
</li>
<li class="biblio_id">
<label for="biblio_id" class="biblio_id">Biblio number:</label>
<input name="biblio_id" id="biblio_id" type="text" value="[% request.biblio_id %]">
</li>
<li class="branchcode">
<label for="branchcode" class="branchcode">Branch:</label>
<select name="branchcode" id="branch">
[% FOREACH branch IN branches %]
[% IF ( branch.branchcode == request.branchcode ) %]
<option value="[% branch.branchcode %]" selected="selected">
[% branch.branchname %]
</option>
[% ELSE %]
<option value="[% branch.branchcode %]">
[% branch.branchname %]
</option>
[% END %]
[% END %]
</select>
</li>
<li class="status">
<label class="status">Status:</label>
[% stat = request.status %]
[% request.capabilities.$stat.name %]
</li>
<li class="updated">
<label class="updated">Last updated:</label>
[% request.updated %]
</li>
<li class="medium">
<label class="medium">Request type:</label>
[% request.medium %]
</li>
<li class="cost">
<label class="cost">Cost:</label>
[% request.cost %]
</li>
<li class="req_id">
<label class="req_id">Request number:</label>
[% request.id_prefix _ request.illrequest_id %]
</li>
<li class="notesstaff">
<label for="notesstaff" class="notesstaff">Staff notes:</label>
<textarea name="notesstaff" id="notesstaff" rows="5">[% request.notesstaff %]</textarea>
</li>
<li class="notesopac">
<label for="notesopac" class="notesopac">Opac notes:</label>
<textarea name="notesopac" id="notesopac" rows="5">[% request.notesopac %]</textarea>
</li>
</ol>
</fieldset>
<fieldset class="action">
<input type="hidden" value="edit_action" name="method">
<input type="hidden" value="form" name="stage">
<input type="hidden" value="[% request.illrequest_id %]" name="illrequest_id">
<input type="submit" value="Submit">
<a class="cancel" href="/cgi-bin/koha/ill/ill-requests.pl?method=illview&amp;illrequest_id=[% request.id %]">Cancel</a>
</fieldset>
</form>
[% ELSIF query_type == 'delete_confirm' %]
<div class="dialog alert">
<h3>Are you sure you wish to delete this request?</h3>
<p>
<a class="btn btn-default btn-sm approve" href="?method=delete&amp;illrequest_id=[% request.id %]&amp;confirmed=1"><i class="fa fa-fw fa-check"></i>Yes</a>
<a class="btn btn-default btn-sm deny" href="?method=illview&amp;illrequest_id=[% request.id %]"><i class="fa fa-fw fa-remove"></i>No</a>
</p>
</div>
[% ELSIF query_type == 'illview' %]
[% actions = request.available_actions %]
[% capabilities = request.capabilities %]
[% req_status = request.status %]
<h1>Manage ILL request</h1>
<div id="toolbar" class="btn-toolbar">
<a title="Edit request" id="ill-toolbar-btn-edit-action" class="btn btn-sm btn-default" href="/cgi-bin/koha/ill/ill-requests.pl?method=edit_action&amp;illrequest_id=[% request.illrequest_id %]">
<span class="fa fa-pencil"></span>
Edit request
</a>
[% FOREACH action IN actions %]
[% IF action.method != 0 %]
<a title="[% action.ui_method_name %]" id="ill-toolbar-btn-[% action.id | lower %]" class="btn btn-sm btn-default" href="/cgi-bin/koha/ill/ill-requests.pl?method=[% action.method %]&amp;illrequest_id=[% request.illrequest_id %]">
<span class="fa [% action.ui_method_icon %]"></span>
[% action.ui_method_name %]
</a>
[% END %]
[% END %]
</div>
<div id="ill-view-panel" class="panel panel-default">
<div class="panel-heading">
<h3>Request details</h3>
</div>
<div class="panel-body">
<h4>Details from library</h4>
<div class="rows">
<div class="orderid">
<span class="label orderid">Order ID:</span>
[% request.orderid || "N/A" %]
</div>
<div class="borrowernumber">
<span class="label borrowernumber">Borrower:</span>
[% borrowerlink = "/cgi-bin/koha/members/moremember.pl"
_ "?borrowernumber=" _ request.patron.borrowernumber %]
<a href="[% borrowerlink %]" title="View borrower details">
[% request.patron.firstname _ " "
_ request.patron.surname _ " ["
_ request.patron.cardnumber
_ "]" %]
</a>
</div>
<div class="biblio_id">
<span class="label biblio_id">Biblio number:</span>
[% request.biblio_id || "N/A" %]
</div>
<div class="branchcode">
<span class="label branchcode">Branch:</span>
[% Branches.GetName(request.branchcode) %]
</div>
<div class="status">
<span class="label status">Status:</span>
[% capabilities.$req_status.name %]
</div>
<div class="updated">
<span class="label updated">Last updated:</span>
[% request.updated %]
</div>
<div class="medium">
<span class="label medium">Request type:</span>
[% request.medium %]
</div>
<div class="cost">
<span class="label cost">Cost:</span>
[% request.cost || "N/A" %]
</div>
<div class="req_id">
<span class="label req_id">Request number:</span>
[% request.id_prefix _ request.illrequest_id %]
</div>
<div class="notesstaff">
<span class="label notes_staff">Staff notes:</span>
<pre>[% request.notesstaff %]</pre>
</div>
<div class="notesopac">
<span class="label notes_opac">Notes:</span>
<pre>[% request.notesopac %]</pre>
</div>
</div>
<div class="rows">
<h4>Details from supplier ([% request.backend %])</h4>
[% FOREACH meta IN request.metadata %]
<div class="requestmeta-[% meta.key %]">
<span class="label">[% meta.key %]:</span>
[% meta.value %]
</div>
[% END %]
</div>
<div class="rows">
<h3><a id="toggle_requestattributes" href="#">Toggle full supplier metadata</a></h3>
<div id="requestattributes" class="content_hidden">
[% FOREACH attr IN request.illrequestattributes %]
<div class="requestattr-[% attr.type %]">
<span class="label">[% attr.type %]:</span>
[% attr.value %]
</div>
[% END %]
</div>
</div>
</div>
</div>
[% ELSIF query_type == 'illlist' %]
<!-- illlist -->
<h1>View ILL requests</h1>
<div id="results">
<h3>Details for all requests</h3>
<div id="column-toggle">
Toggle additional columns:
</div>
<div id="reset-toggle"><a href="#">Reset toggled columns</a></div>
<table
[% FOREACH filter IN prefilters %]
data-filter-[% filter.name %]="[% filter.value %]"
[% END %]
id="ill-requests">
<thead>
<tr id="illview-header"></tr>
</thead>
<tbody id="illview-body">
</tbody>
</table>
</div>
[% ELSE %]
<!-- Custom Backend Action -->
[% INCLUDE $whole.template %]
[% END %]
</div>
</div>
</div>
</div>
[% TRY %]
[% PROCESS backend_jsinclude %]
[% CATCH %]
[% END %]
[% INCLUDE 'intranet-bottom.inc' %]

View File

@ -63,6 +63,11 @@
<li>
<a class="icon_general icon_authorities" href="/cgi-bin/koha/authorities/authorities-home.pl">Authorities</a>
</li>
[% IF Koha.Preference('ILLModule') %]
<li>
<a class="icon_general icon_ill" href="/cgi-bin/koha/ill/ill-requests.pl">ILL requests</a>
</li>
[% END %]
</ul>
</div><!-- /area-list-left -->
</div><!-- /yui-u first -->

View File

@ -104,6 +104,15 @@
[% END %]
<a href="/cgi-bin/koha/opac-discharge.pl">ask for a discharge</a></li>
[% END %]
[% IF Koha.Preference( 'ILLModule' ) == 1 %]
[% IF ( illrequestsview ) %]
<li class="active">
[% ELSE %]
<li>
[% END %]
<a href="/cgi-bin/koha/opac-illrequests.pl">your interlibrary loan requests</a></li>
[% END %]
</ul>
</div>
[% END %]

View File

@ -0,0 +1,221 @@
[% USE Koha %]
[% USE Branches %]
[% INCLUDE 'doc-head-open.inc' %]
<title>[% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha online[% END %] catalog &rsaquo; Your Interlibrary loan requests</title>[% INCLUDE 'doc-head-close.inc' %]
[% BLOCK cssinclude %][% END %]
</head>
[% INCLUDE 'bodytag.inc' bodyid='opac-illrequests' bodyclass='scrollto' %]
[% BLOCK messages %]
[% IF message == "1" %]
<div class="alert alert-success" role="alert">Request updated</div>
[% ELSIF message == "2" %]
<div class="alert alert-success" role="alert">Request placed</div>
[% END %]
[% END %]
[% INCLUDE 'masthead.inc' %]
<div class="main">
<ul class="breadcrumb noprint">
<li><a href="/cgi-bin/koha/opac-main.pl">Home</a> <span class="divider">&rsaquo;</span></li>
[% IF ( loggedinusername ) %]
<li><a href="/cgi-bin/koha/opac-user.pl">[% USER_INFO.title %] [% USER_INFO.firstname %] [% USER_INFO.surname %]</a> <span class="divider">&rsaquo;</span></li>
[% END %]
[% IF method != 'list' %]
<li><a href="/cgi-bin/koha/opac-illrequests.pl">Interlibrary loan requests</a> <span class="divider">&rsaquo;</span></li>
[% IF method == 'create' %]
<li>New Interlibrary loan request</li>
[% ELSIF method == 'view' %]
<li>View Interlibrary loan request</li>
[% END %]
[% ELSE %]
<li>Interlibrary loan requests</li>
[% END %]
</ul> <!-- / .breadcrumb -->
<div class="container-fluid">
<div class="row-fluid">
[% IF ( OpacNav||loggedinusername ) && !print %]
<div class="span2">
<div id="navigation">
[% INCLUDE 'navigation.inc' IsPatronPage=1 %]
</div>
</div>
[% END %]
[% IF ( OpacNav||loggedinusername ) %]
<div class="span10">
[% ELSE %]
<div class="span12">
[% END %]
<div id="illrequests" class="maincontent">
[% IF method == 'create' %]
<h2>New Interlibrary loan request</h2>
[% INCLUDE messages %]
[% IF backends %]
<form method="post" id="illrequestcreate-form" novalidate="novalidate">
<fieldset class="rows">
<label for="backend">Provider:</label>
<select name="backend">
[% FOREACH backend IN backends %]
<option value="[% backend %]">[% backend %]</option>
[% END %]
</select>
</fieldset>
<fieldset class="action">
<input type="hidden" name="method" value="create">
<input type="submit" name="create_select_backend" value="Next &raquo;">
</fieldset>
</form>
[% ELSE %]
[% PROCESS $whole.opac_template %]
[% END %]
[% ELSIF method == 'list' %]
<h2>Interlibrary loan requests</h2>
[% INCLUDE messages %]
<div id="illrequests-create-button" class="dropdown btn-group">
[% IF backends.size > 1 %]
<button class="btn btn-default dropdown-toggle" type="button" id="ill-backend-dropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fa fa-plus"></i> Create a new request <span class="caret"></span>
</button>
<ul id="backend-dropdown-options" class="dropdown-menu nojs" aria-labelledby="ill-backend-dropdown">
[% FOREACH backend IN backends %]
<li><a href="/cgi-bin/koha/opac-illrequests.pl?method=create&amp;backend=[% backend %]">[% backend %]</a></li>
[% END %]
</ul>
[% ELSE %]
<a id="ill-new" class="btn btn-default" href="/cgi-bin/koha/opac-illrequests.pl?method=create&amp;backend=[% backends.0 %]">
<i class="fa fa-plus"></i> Create a new request
</a>
[% END %]
</div>
<table id="illrequestlist" class="table table-bordered table-striped">
<thead>
<tr>
<th>Author</th>
<th>Title</th>
<th>Requested from</th>
<th>Request type</th>
<th>Status</th>
<th>Request placed</th>
<th>Last updated</th>
<th></th>
</tr>
</thead>
<tbody>
[% FOREACH request IN requests %]
[% status = request.status %]
<tr>
<td>[% request.metadata.Author || 'N/A' %]</td>
<td>[% request.metadata.Title || 'N/A' %]</td>
<td>[% request.backend %]</td>
<td>[% request.medium %]</td>
<td>[% request.capabilities.$status.name %]</td>
<td>[% request.placed %]</td>
<td>[% request.updated %]</td>
<td>
<a href="/cgi-bin/koha/opac-illrequests.pl?method=view&amp;illrequest_id=[% request.id %]" class="btn btn-default btn-small pull-right">View</a>
</td>
</tr>
[% END %]
</tbody>
</table>
[% ELSIF method == 'view' %]
<h2>View Interlibrary loan request</h2>
[% INCLUDE messages %]
[% status = request.status %]
<form method="post" action="?method=update" id="illrequestupdate-form" novalidate="novalidate">
<fieldset class="rows">
<legend id="library_legend">Details from library</legend>
<ol>
<li>
<label for="backend">Requested from:</label>
[% request.backend %]
</li>
[% IF request.biblio_id %]
<li>
<label for="biblio">Requested item:</label>
<a href="/cgi-bin/koha/opac-detail.pl?biblionumber=[% request.biblio_id %]">Click here to view</a>
</li>
[% END %]
<li>
<label for="branchcode">Collection library:</label>
[% Branches.GetName(request.branchcode) %]
</li>
<li>
<label for="status">Status:</label>
[% request.capabilities.$status.name %]
</li>
<li>
<label for="medium">Request type:</label>
[% request.medium %]
</li>
<li>
<label for="placed">Request placed:</label>
[% request.placed %]
</li>
<li>
<label for="updated">Last updated:</label>
[% request.updated %]
</li>
<li>
<label for="notesopac">Notes:</label>
[% IF !request.completed %]
<textarea name="notesopac" rows="5" cols="50">[% request.notesopac %]</textarea>
[% ELSE %]
[% request.notesopac %]
[% END %]
</li>
</ol>
</fieldset>
<div class="rows">
<legend id="backend_legend">Details from [% request.backend %]</legend>
[% FOREACH meta IN request.metadata %]
<div class="requestattr-[% meta.key %]">
<span class="label">[% meta.key %]:</span>
[% meta.value || 'N/A' %]
</div>
[% END %]
</div>
<fieldset class="action illrequest-actions">
<input type="hidden" name="illrequest_id" value="[% request.illrequest_id %]">
<input type="hidden" name="method" value="update">
[% IF !request.completed %]
[% IF request.status == "NEW" %]
<a class="cancel-illrequest btn btn-danger" href="/cgi-bin/koha/opac-illrequests.pl?method=cancreq&amp;illrequest_id=[% request.illrequest_id %]">Request cancellation</a>
[% END %]
<input type="submit" class="update-illrequest btn btn-default" value="Submit modifications">
[% END %]
<span class="cancel"><a href="/cgi-bin/koha/opac-illrequests.pl">Cancel</a></span>
</fieldset>
</form>
[% END %]
</div> <!-- / .maincontent -->
</div> <!-- / .span10/12 -->
</div> <!-- / .row-fluid -->
</div> <!-- / .container-fluid -->
</div> <!-- / .main -->
[% INCLUDE 'opac-bottom.inc' %]
[% BLOCK jsinclude %]
[% INCLUDE 'datatables.inc' %]
<script type="text/javascript">
//<![CDATA[
$("#illrequestlist").dataTable($.extend(true, {}, dataTablesDefaults, {
"aoColumnDefs": [
{ "aTargets": [ -1 ], "bSortable": false, "bSearchable": false }
],
"aaSorting": [[ 3, "desc" ]],
"deferRender": true
}));
$("#backend-dropdown-options").removeClass("nojs");
//]]>
</script>
[% TRY %]
[% PROCESS backend_jsinclude %]
[% CATCH %]
[% END %]
[% END %]

View File

@ -253,14 +253,30 @@ href="/cgi-bin/koha/opac-rss.pl?[% query_cgi %][% limit_cgi |html %]" />
[% INCLUDE 'page-numbers.inc' %]
[% END # / IF total %]
[% IF Koha.Preference( 'suggestion' ) == 1 %]
[% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
<div class="suggestion">Not finding what you're looking for?<br /> Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></div>
[% ELSE %]
[% IF ( loggedinusername ) %]<div class="suggestion">Not finding what you're looking for?<br /> Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></div>[% END %]
[% END %]
[% IF
Koha.Preference( 'suggestion' ) == 1 &&
(
Koha.Preference( 'AnonSuggestions' ) == 1 ||
loggedinusername ||
Koha.Preference( 'ILLModule' ) == 1
)
%]
<div class="suggestion">
Not finding what you're looking for?
<ul>
[% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
<li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></li>
[% ELSE %]
[% IF ( loggedinusername ) %]
<li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></li>
[% END %]
[% END %]
[% IF Koha.Preference( 'ILLModule' ) == 1 && loggedinusername %]
<li>Make an <a href="/cgi-bin/koha/opac-illrequests.pl?op=create">Interlibrary loan request</a></li>
[% END %]
</ul>
</div>
[% END %]
</div> <!-- / #grouped-results -->
</div> <!-- /.span10/12 -->
</div> <!-- / .row-fluid -->

View File

@ -562,16 +562,29 @@
[% END # / IF total %]
[% IF Koha.Preference( 'suggestion' ) == 1 %]
[% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
<div class="suggestion">Not finding what you're looking for?<br /> Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></div>
[% ELSE %]
[% IF ( loggedinusername ) %]
<div class="suggestion">
Not finding what you're looking for?<br /> Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a>
</div>
[% END %]
[% END %]
[% IF
Koha.Preference( 'suggestion' ) == 1 &&
(
Koha.Preference( 'AnonSuggestions' ) == 1 ||
loggedinusername ||
Koha.Preference( 'ILLModule' ) == 1
)
%]
<div class="suggestion">
Not finding what you're looking for?
<ul>
[% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
<li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></li>
[% ELSE %]
[% IF ( loggedinusername ) %]
<li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></li>
[% END %]
[% END %]
[% IF Koha.Preference( 'ILLModule' ) == 1 && loggedinusername %]
<li>Make an <a href="/cgi-bin/koha/opac-illrequests.pl?op=create">Interlibrary loan request</a></li>
[% END %]
</ul>
</div>
[% END %]
</div> <!-- / #userresults -->
</div> <!-- /.span10/12 -->

View File

@ -2508,6 +2508,44 @@ a.reviewlink:visited {
font-size: 90%;
}
#illrequests {
.illrequest-actions {
.btn,
.cancel {
margin-right: 5px;
}
padding-top: 20px;
margin-bottom: 20px;
}
#illrequests-create-button {
margin-bottom: 20px;
}
.bg-info {
overflow: auto;
position: relative;
}
.bg-info {
#search-summary {
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
-o-transform: translateY(-50%);
transform: translateY(-50%);
position: absolute;
top: 50%;
}
}
#freeform-fields .custom-name {
float: left;
width: 8em;
margin-right: 1em;
text-align: right;
}
.dropdown:hover .dropdown-menu.nojs {
display: block;
}
}
#dc_fieldset {
border: 1px solid #dddddd;
border-width: 1px;

129
opac/opac-illrequests.pl Executable file
View File

@ -0,0 +1,129 @@
#!/usr/bin/perl
# Copyright 2017 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 <http://www.gnu.org/licenses>.
use Modern::Perl;
use CGI qw ( -utf8 );
use C4::Auth;
use C4::Koha;
use C4::Output;
use Koha::Illrequests;
use Koha::Libraries;
use Koha::Patrons;
my $query = new CGI;
# Grab all passed data
# 'our' since Plack changes the scoping
# of 'my'
our $params = $query->Vars();
# if illrequests is disabled, leave immediately
if ( ! C4::Context->preference('ILLModule') ) {
print $query->redirect("/cgi-bin/koha/errors/404.pl");
exit;
}
my ( $template, $loggedinuser, $cookie ) = get_template_and_user({
template_name => "opac-illrequests.tt",
query => $query,
type => "opac",
authnotrequired => ( C4::Context->preference("OpacPublic") ? 1 : 0 ),
});
my $op = $params->{'method'} || 'list';
if ( $op eq 'list' ) {
my $requests = Koha::Illrequests->search(
{ borrowernumber => $loggedinuser }
);
my $req = Koha::Illrequest->new;
$template->param(
requests => $requests,
backends => $req->available_backends
);
} elsif ( $op eq 'view') {
my $request = Koha::Illrequests->find({
borrowernumber => $loggedinuser,
illrequest_id => $params->{illrequest_id}
});
$template->param(
request => $request
);
} elsif ( $op eq 'update') {
my $request = Koha::Illrequests->find({
borrowernumber => $loggedinuser,
illrequest_id => $params->{illrequest_id}
});
$request->notesopac($params->{notesopac})->store;
print $query->redirect(
'/cgi-bin/koha/opac-illrequests.pl?method=view&illrequest_id=' .
$params->{illrequest_id} .
'&message=1'
);
} elsif ( $op eq 'cancreq') {
my $request = Koha::Illrequests->find({
borrowernumber => $loggedinuser,
illrequest_id => $params->{illrequest_id}
});
$request->status('CANCREQ')->store;
print $query->redirect(
'/cgi-bin/koha/opac-illrequests.pl?method=view&illrequest_id=' .
$params->{illrequest_id} .
'&message=1'
);
} elsif ( $op eq 'create' ) {
if (!$params->{backend}) {
my $req = Koha::Illrequest->new;
$template->param(
backends => $req->available_backends
);
} else {
my $request = Koha::Illrequest->new
->load_backend($params->{backend});
$params->{cardnumber} = Koha::Patrons->find({
borrowernumber => $loggedinuser
})->cardnumber;
my $backend_result = $request->backend_create($params);
$template->param(
media => [ "Book", "Article", "Journal" ],
branches => Koha::Libraries->search->unblessed,
whole => $backend_result,
request => $request
);
if ($backend_result->{stage} eq 'commit') {
print $query->redirect('/cgi-bin/koha/opac-illrequests.pl?message=2');
}
}
}
$template->param(
message => $params->{message},
illrequestsview => 1,
method => $op
);
output_html_with_http_headers $query, $cookie, $template->output;

View File

@ -0,0 +1,473 @@
#!/usr/bin/perl
# 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 <http://www.gnu.org/licenses>.
use Modern::Perl;
use Koha::Database;
use t::lib::Mocks;
use t::lib::TestBuilder;
use Test::MockObject;
use Test::Exception;
use Test::More tests => 5;
my $schema = Koha::Database->new->schema;
my $builder = t::lib::TestBuilder->new;
use_ok('Koha::Illrequest::Config');
my $base_limits = {
branch => { CPL => { count => 1, method => 'annual' } },
brw_cat => { A => { count => -1, method => 'active' } },
default => { count => 10, method => 'annual' },
};
my $base_censorship = { censor_notes_staff => 1, censor_reply_date => 1 };
subtest 'Basics' => sub {
plan tests => 19;
$schema->storage->txn_begin;
t::lib::Mocks::mock_preference("UnmediatedILL", 0);
t::lib::Mocks::mock_config("interlibrary_loans", {});
my $config = Koha::Illrequest::Config->new;
isa_ok($config, "Koha::Illrequest::Config",
"Correctly create and load a config object.");
# backend:
is($config->backend, undef, "backend: Undefined backend is undefined.");
is($config->backend("Mock"), "Mock", "backend: setter works.");
is($config->backend, "Mock", "backend: setter is persistent.");
# backend_dir:
is($config->backend_dir, undef, "backend_dir: Undefined backend_dir is undefined.");
is($config->backend_dir("/tmp/"), "/tmp/", "backend_dir: setter works.");
is($config->backend_dir, "/tmp/", "backend_dir: setter is persistent.");
# partner_code:
is($config->partner_code, "ILLLIBS", "partner_code: Undefined partner_code is undefined.");
is($config->partner_code("ILLLIBSTST"), "ILLLIBSTST", "partner_code: setter works.");
is($config->partner_code, "ILLLIBSTST", "partner_code: setter is persistent.");
# limits:
is_deeply($config->limits, {}, "limits: Undefined limits is empty hash.");
is_deeply($config->limits($base_limits), $base_limits, "limits: setter works.");
is_deeply($config->limits, $base_limits, "limits: setter is persistent.");
# censorship:
is_deeply($config->censorship, { censor_notes_staff => 0, censor_reply_date => 0 },
"censorship: Undefined censorship is default values.");
is_deeply($config->censorship($base_censorship), $base_censorship, "censorship: setter works.");
is_deeply($config->censorship, $base_censorship, "censorship: setter is persistent.");
# getLimitRules
dies_ok( sub { $config->getLimitRules("FOO") }, "getLimitRules: die if not correct type.");
is_deeply($config->getLimitRules("brw_cat"), {
A => { count => -1, method => 'active' },
default => { count => 10, method => 'annual' },
}, "getLimitRules: fetch brw_cat limits.");
is_deeply($config->getLimitRules("branch"), {
CPL => { count => 1, method => 'annual' },
default => { count => 10, method => 'annual' },
}, "getLimitRules: fetch brw_cat limits.");
$schema->storage->txn_rollback;
};
# _load_unit_config:
subtest '_load_unit_config' => sub {
plan tests => 10;
$schema->storage->txn_begin;
my $config = Koha::Illrequest::Config->new;
dies_ok(
sub { Koha::Illrequest::Config::_load_unit_config({
id => 'durineadu', type => 'baz'
}) },
"_load_unit_config: die if ID is not default, and type is not branch or brw_cat."
);
is_deeply(
Koha::Illrequest::Config::_load_unit_config({
unit => {}, id => 'default', config => {}, test => 1
}), {}, "_load_unit_config: invocation without id returns unmodified config."
);
is_deeply(
Koha::Illrequest::Config::_load_unit_config({
unit => { api_key => 'foo', api_auth => 'bar' },
id => "CPL", type => 'branch', config => {}
}),
{ credentials => { api_keys => { CPL => { api_key => 'foo', api_auth => 'bar' } } } },
"_load_unit_config: add auth values."
);
# Populate request_limits
is_deeply(
Koha::Illrequest::Config::_load_unit_config({
unit => { request_limit => [ 'heelo', 1234 ] },
id => "CPL", type => 'branch', config => {}
}), {}, "_load_unit_config: invalid request_limit structure."
);
is_deeply(
Koha::Illrequest::Config::_load_unit_config({
unit => { request_limit => { method => 'eudiren', count => '-5465' } },
id => "CPL", type => 'branch', config => {}
}), {}, "_load_unit_config: invalid method & count."
);
is_deeply(
Koha::Illrequest::Config::_load_unit_config({
unit => { request_limit => { method => 'annual', count => 6 } },
id => "default", config => {}
}),
{ limits => { default => { method => 'annual', count => 6 } } },
"_load_unit_config: correct default request_limits."
);
# Populate prefix
is_deeply(
Koha::Illrequest::Config::_load_unit_config({
unit => { prefix => 'Foo-ill' },
id => "default", config => {}
}),
{ prefixes => { default => 'Foo-ill' } },
"_load_unit_config: correct default prefix."
);
is_deeply(
Koha::Illrequest::Config::_load_unit_config({
unit => { prefix => 'Foo-ill' },
id => "A", config => {}, type => 'brw_cat'
}),
{ prefixes => { brw_cat => { A => 'Foo-ill' } } },
"_load_unit_config: correct brw_cat prefix."
);
# Populate digital_recipient
is_deeply(
Koha::Illrequest::Config::_load_unit_config({
unit => { digital_recipient => 'borrower' },
id => "default", config => {}
}),
{ digital_recipients => { default => 'borrower' } },
"_load_unit_config: correct default digital_recipient."
);
is_deeply(
Koha::Illrequest::Config::_load_unit_config({
unit => { digital_recipient => 'branch' },
id => "A", config => {}, type => 'brw_cat'
}),
{ digital_recipients => { brw_cat => { A => 'branch' } } },
"_load_unit_config: correct brw_cat digital_recipient."
);
$schema->storage->txn_rollback;
};
# _load_configuration:
# We have already tested _load_unit_config, so we are reasonably confident
# that the per-branch, per-borrower_category & default sections parsing is
# good.
#
# Now we need to ensure that Arrays & Hashes are handled correctly, that
# censorship & ill partners are loaded correctly and that the backend
# directory is set correctly.
subtest '_load_configuration' => sub {
plan tests => 9;
$schema->storage->txn_begin;
my $config = Koha::Illrequest::Config->new;
# Return basic configuration
is_deeply(
Koha::Illrequest::Config::_load_configuration({}, 0),
{
backend_directory => undef,
censorship => {
censor_notes_staff => 0,
censor_reply_date => 0,
},
limits => {},
digital_recipients => {},
prefixes => {},
partner_code => 'ILLLIBS',
raw_config => {},
},
"load_configuration: return the base configuration."
);
# Return correct backend_dir
is_deeply(
Koha::Illrequest::Config::_load_configuration({ backend_directory => '/tmp/' }, 0),
{
backend_directory => '/tmp/',
censorship => {
censor_notes_staff => 0,
censor_reply_date => 0,
},
limits => {},
digital_recipients => {},
prefixes => {},
partner_code => 'ILLLIBS',
raw_config => { backend_directory => '/tmp/' },
},
"load_configuration: return the correct backend_dir."
);
# Map over branch configs
my $xml_config = {
backend_directory => '/tmp/',
branch => [
{ code => '1', request_limit => { method => 'annual', count => 1 } },
{ code => '2', prefix => '2-prefix' },
{ code => '3', digital_recipient => 'branch' }
]
};
is_deeply(
Koha::Illrequest::Config::_load_configuration($xml_config, 0),
{
backend_directory => '/tmp/',
censorship => {
censor_notes_staff => 0,
censor_reply_date => 0,
},
limits => { branch => { 1 => { method => 'annual', count => 1 } } },
digital_recipients => { branch => { 3 => 'branch' } },
prefixes => { branch => { 2 => '2-prefix' } },
partner_code => 'ILLLIBS',
raw_config => $xml_config,
},
"load_configuration: multi branch config parsed correctly."
);
# Single branch config
$xml_config = {
backend_directory => '/tmp/',
branch => {
code => '1',
request_limit => { method => 'annual', count => 1 },
prefix => '2-prefix',
digital_recipient => 'branch',
}
};
is_deeply(
Koha::Illrequest::Config::_load_configuration($xml_config, 0),
{
backend_directory => '/tmp/',
censorship => {
censor_notes_staff => 0,
censor_reply_date => 0,
},
limits => { branch => { 1 => { method => 'annual', count => 1 } } },
digital_recipients => { branch => { 1 => 'branch' } },
prefixes => { branch => { 1 => '2-prefix' } },
partner_code => 'ILLLIBS',
raw_config => $xml_config,
},
"load_configuration: single branch config parsed correctly."
);
# Map over borrower_category settings
$xml_config = {
backend_directory => '/tmp/',
borrower_category => [
{ code => 'A', request_limit => { method => 'annual', count => 1 } },
{ code => 'B', prefix => '2-prefix' },
{ code => 'C', digital_recipient => 'branch' }
]
};
is_deeply(
Koha::Illrequest::Config::_load_configuration($xml_config, 0),
{
backend_directory => '/tmp/',
censorship => {
censor_notes_staff => 0,
censor_reply_date => 0,
},
limits => { brw_cat => { A => { method => 'annual', count => 1 } } },
digital_recipients => { brw_cat => { C => 'branch' } },
prefixes => { brw_cat => { B => '2-prefix' } },
partner_code => 'ILLLIBS',
raw_config => $xml_config,
},
"load_configuration: multi borrower_category config parsed correctly."
);
# Single borrower_category config
$xml_config = {
backend_directory => '/tmp/',
borrower_category => {
code => '1',
request_limit => { method => 'annual', count => 1 },
prefix => '2-prefix',
digital_recipient => 'branch',
}
};
is_deeply(
Koha::Illrequest::Config::_load_configuration($xml_config, 0),
{
backend_directory => '/tmp/',
censorship => {
censor_notes_staff => 0,
censor_reply_date => 0,
},
limits => { brw_cat => { 1 => { method => 'annual', count => 1 } } },
digital_recipients => { brw_cat => { 1 => 'branch' } },
prefixes => { brw_cat => { 1 => '2-prefix' } },
partner_code => 'ILLLIBS',
raw_config => $xml_config,
},
"load_configuration: single borrower_category config parsed correctly."
);
# Default Configuration
$xml_config = {
backend_directory => '/tmp/',
request_limit => { method => 'annual', count => 1 },
prefix => '2-prefix',
digital_recipient => 'branch',
};
is_deeply(
Koha::Illrequest::Config::_load_configuration($xml_config, 0),
{
backend_directory => '/tmp/',
censorship => {
censor_notes_staff => 0,
censor_reply_date => 0,
},
limits => { default => { method => 'annual', count => 1 } },
digital_recipients => { default => 'branch' },
prefixes => { default => '2-prefix' },
partner_code => 'ILLLIBS',
raw_config => $xml_config,
},
"load_configuration: parse the default configuration."
);
# Censorship
$xml_config = {
backend_directory => '/tmp/',
staff_request_comments => 'hide',
reply_date => 'hide'
};
is_deeply(
Koha::Illrequest::Config::_load_configuration($xml_config, 0),
{
backend_directory => '/tmp/',
censorship => {
censor_notes_staff => 1,
censor_reply_date => 1,
},
limits => {},
digital_recipients => {},
prefixes => {},
partner_code => 'ILLLIBS',
raw_config => $xml_config,
},
"load_configuration: parse censorship settings configuration."
);
# Partner library category
is_deeply(
Koha::Illrequest::Config::_load_configuration({ partner_code => 'FOOBAR' }),
{
backend_directory => undef,
censorship => {
censor_notes_staff => 0,
censor_reply_date => 0,
},
limits => {},
digital_recipients => {},
prefixes => {},
partner_code => 'FOOBAR',
raw_config => { partner_code => 'FOOBAR' },
},
"load_configuration: Set partner code."
);
$schema->storage->txn_rollback;
};
subtest 'Final tests' => sub {
plan tests => 10;
$schema->storage->txn_begin;
t::lib::Mocks::mock_preference("UnmediatedILL", 0);
t::lib::Mocks::mock_config("interlibrary_loans", {});
my $config = Koha::Illrequest::Config->new;
# getPrefixes (error & undef):
dies_ok( sub { $config->getPrefixes("FOO") }, "getPrefixes: die if not correct type.");
is_deeply($config->getPrefixes("brw_cat"), { default => undef},
"getPrefixes: Undefined brw_cat prefix is undefined.");
is_deeply($config->getPrefixes("branch"), { default => undef},
"getPrefixes: Undefined branch prefix is undefined.");
# getDigitalRecipients (error & undef):
dies_ok( sub { $config->getDigitalRecipients("FOO") },
"getDigitalRecipients: die if not correct type.");
is_deeply($config->getDigitalRecipients("brw_cat"), { default => undef},
"getDigitalRecipients: Undefined brw_cat dig rec is undefined.");
is_deeply($config->getDigitalRecipients("branch"), { default => undef},
"getDigitalRecipients: Undefined branch dig rec is undefined.");
$config->{configuration} = Koha::Illrequest::Config::_load_configuration({
backend_directory => '/tmp/',
prefix => 'DEFAULT-prefix',
digital_recipient => 'branch',
borrower_category => [
{ code => 'B', prefix => '2-prefix' },
{ code => 'C', digital_recipient => 'branch' }
],
branch => [
{ code => '1', prefix => 'T-prefix' },
{ code => '2', digital_recipient => 'borrower' }
]
}, 0
);
# getPrefixes (values):
is_deeply($config->getPrefixes("brw_cat"),
{ B => '2-prefix', default => 'DEFAULT-prefix' },
"getPrefixes: return configuration brw_cat prefixes.");
is_deeply($config->getPrefixes("branch"),
{ 1 => 'T-prefix', default => 'DEFAULT-prefix' },
"getPrefixes: return configuration branch prefixes.");
# getDigitalRecipients (values):
is_deeply($config->getDigitalRecipients("brw_cat"),
{ C => 'branch', default => 'branch' },
"getDigitalRecipients: return brw_cat digital_recipients.");
is_deeply($config->getDigitalRecipients("branch"),
{ 2 => 'borrower', default => 'branch' },
"getDigitalRecipients: return branch digital_recipients.");
$schema->storage->txn_rollback;
};
1;

View File

@ -0,0 +1,63 @@
#!/usr/bin/perl
#
# 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 2 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 File::Basename qw/basename/;
use Koha::Database;
use Koha::Patrons;
use t::lib::TestBuilder;
use Test::More tests => 3;
my $schema = Koha::Database->new->schema;
use_ok('Koha::Illrequestattribute');
use_ok('Koha::Illrequestattributes');
subtest 'Basic object tests' => sub {
plan tests => 5;
$schema->storage->txn_begin;
my $builder = t::lib::TestBuilder->new;
my $illrqattr = $builder->build({ source => 'Illrequestattribute' });
my $illrqattr_obj = Koha::Illrequestattributes->find(
$illrqattr->{illrequest_id},
$illrqattr->{type}
);
isa_ok($illrqattr_obj, 'Koha::Illrequestattribute',
"Correctly create and load an illrequestattribute object.");
is($illrqattr_obj->illrequest_id, $illrqattr->{illrequest_id},
"Illrequest_id getter works.");
is($illrqattr_obj->type, $illrqattr->{type},
"Type getter works.");
is($illrqattr_obj->value, $illrqattr->{value},
"Value getter works.");
$illrqattr_obj->delete;
is(Koha::Illrequestattributes->search->count, 0,
"No attributes found after delete.");
$schema->storage->txn_rollback;
};
1;

View File

@ -0,0 +1,792 @@
#!/usr/bin/perl
#
# 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 2 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 File::Basename qw/basename/;
use Koha::Database;
use Koha::Illrequestattributes;
use Koha::Illrequest::Config;
use Koha::Patrons;
use t::lib::Mocks;
use t::lib::TestBuilder;
use Test::MockObject;
use Test::Exception;
use Test::More tests => 10;
my $schema = Koha::Database->new->schema;
my $builder = t::lib::TestBuilder->new;
use_ok('Koha::Illrequest');
use_ok('Koha::Illrequests');
subtest 'Basic object tests' => sub {
plan tests => 21;
$schema->storage->txn_begin;
my $illrq = $builder->build({ source => 'Illrequest' });
my $illrq_obj = Koha::Illrequests->find($illrq->{illrequest_id});
isa_ok($illrq_obj, 'Koha::Illrequest',
"Correctly create and load an illrequest object.");
isa_ok($illrq_obj->_config, 'Koha::Illrequest::Config',
"Created a config object as part of Illrequest creation.");
is($illrq_obj->illrequest_id, $illrq->{illrequest_id},
"Illrequest_id getter works.");
is($illrq_obj->borrowernumber, $illrq->{borrowernumber},
"Borrowernumber getter works.");
is($illrq_obj->biblio_id, $illrq->{biblio_id},
"Biblio_Id getter works.");
is($illrq_obj->branchcode, $illrq->{branchcode},
"Branchcode getter works.");
is($illrq_obj->status, $illrq->{status},
"Status getter works.");
is($illrq_obj->placed, $illrq->{placed},
"Placed getter works.");
is($illrq_obj->replied, $illrq->{replied},
"Replied getter works.");
is($illrq_obj->updated, $illrq->{updated},
"Updated getter works.");
is($illrq_obj->completed, $illrq->{completed},
"Completed getter works.");
is($illrq_obj->medium, $illrq->{medium},
"Medium getter works.");
is($illrq_obj->accessurl, $illrq->{accessurl},
"Accessurl getter works.");
is($illrq_obj->cost, $illrq->{cost},
"Cost getter works.");
is($illrq_obj->notesopac, $illrq->{notesopac},
"Notesopac getter works.");
is($illrq_obj->notesstaff, $illrq->{notesstaff},
"Notesstaff getter works.");
is($illrq_obj->orderid, $illrq->{orderid},
"Orderid getter works.");
is($illrq_obj->backend, $illrq->{backend},
"Backend getter works.");
isnt($illrq_obj->status, 'COMP',
"ILL is not currently marked complete.");
$illrq_obj->mark_completed;
is($illrq_obj->status, 'COMP',
"ILL is now marked complete.");
$illrq_obj->delete;
is(Koha::Illrequests->search->count, 0,
"No illrequest found after delete.");
$schema->storage->txn_rollback;
};
subtest 'Working with related objects' => sub {
plan tests => 5;
$schema->storage->txn_begin;
my $patron = $builder->build({ source => 'Borrower' });
my $illrq = $builder->build({
source => 'Illrequest',
value => { borrowernumber => $patron->{borrowernumber} }
});
my $illrq_obj = Koha::Illrequests->find($illrq->{illrequest_id});
isa_ok($illrq_obj->patron, 'Koha::Patron',
"OK accessing related patron.");
$builder->build({
source => 'Illrequestattribute',
value => { illrequest_id => $illrq_obj->illrequest_id, type => 'X' }
});
$builder->build({
source => 'Illrequestattribute',
value => { illrequest_id => $illrq_obj->illrequest_id, type => 'Y' }
});
$builder->build({
source => 'Illrequestattribute',
value => { illrequest_id => $illrq_obj->illrequest_id, type => 'Z' }
});
is($illrq_obj->illrequestattributes->count, Koha::Illrequestattributes->search->count,
"Fetching expected number of Illrequestattributes for our request.");
my $illrq1 = $builder->build({ source => 'Illrequest' });
$builder->build({
source => 'Illrequestattribute',
value => { illrequest_id => $illrq1->{illrequest_id}, type => 'X' }
});
is($illrq_obj->illrequestattributes->count + 1, Koha::Illrequestattributes->search->count,
"Fetching expected number of Illrequestattributes for our request.");
$illrq_obj->delete;
is(Koha::Illrequestattributes->search->count, 1,
"Correct number of illrequestattributes after delete.");
isa_ok(Koha::Patrons->find($patron->{borrowernumber}), 'Koha::Patron',
"Borrower was not deleted after illrq delete.");
$schema->storage->txn_rollback;
};
subtest 'Status Graph tests' => sub {
plan tests => 4;
$schema->storage->txn_begin;
my $illrq = $builder->build({source => 'Illrequest'});
my $illrq_obj = Koha::Illrequests->find($illrq->{illrequest_id});
# _core_status_graph tests: it's just a constant, so here we just make
# sure it returns a hashref.
is(ref $illrq_obj->_core_status_graph, "HASH",
"_core_status_graph returns a hash.");
# _status_graph_union: let's try different merge operations.
# Identity operation
is_deeply(
$illrq_obj->_status_graph_union($illrq_obj->_core_status_graph, {}),
$illrq_obj->_core_status_graph,
"core_status_graph + null = core_status_graph"
);
# Simple addition
is_deeply(
$illrq_obj->_status_graph_union({}, $illrq_obj->_core_status_graph),
$illrq_obj->_core_status_graph,
"null + core_status_graph = core_status_graph"
);
# Correct merge behaviour
is_deeply(
$illrq_obj->_status_graph_union({
REQ => {
prev_actions => [ ],
id => 'REQ',
next_actions => [ ],
},
}, {
QER => {
prev_actions => [ 'REQ' ],
id => 'QER',
next_actions => [ 'REQ' ],
},
}),
{
REQ => {
prev_actions => [ 'QER' ],
id => 'REQ',
next_actions => [ 'QER' ],
},
QER => {
prev_actions => [ 'REQ' ],
id => 'QER',
next_actions => [ 'REQ' ],
},
},
"REQ atom + linking QER = cyclical status graph"
);
$schema->storage->txn_rollback;
};
subtest 'Backend testing (mocks)' => sub {
plan tests => 13;
$schema->storage->txn_begin;
# testing load_backend & available_backends requires that we have at least
# the Dummy plugin installed. load_backend & available_backends don't
# currently have tests as a result.
my $backend = Test::MockObject->new;
$backend->set_isa('Koha::Illbackends::Mock');
$backend->set_always('name', 'Mock');
my $patron = $builder->build({ source => 'Borrower' });
my $illrq = $builder->build({
source => 'Illrequest',
value => { borrowernumber => $patron->{borrowernumber} }
});
my $illrq_obj = Koha::Illrequests->find($illrq->{illrequest_id});
$illrq_obj->_backend($backend);
isa_ok($illrq_obj->_backend, 'Koha::Illbackends::Mock',
"OK accessing mocked backend.");
# _backend_capability tests:
# We need to test whether this optional feature of a mocked backend
# behaves as expected.
# 3 scenarios: feature not implemented, feature implemented, but requested
# capability is not provided by backend, & feature is implemented &
# capability exists. This method can be used to implement custom backend
# functionality, such as unmediated in the BLDSS backend (also see
# bugzilla 18837).
$backend->set_always('capabilities', undef);
is($illrq_obj->_backend_capability('Test'), 0,
"0 returned on Mock not implementing capabilities.");
$backend->set_always('capabilities', 0);
is($illrq_obj->_backend_capability('Test'), 0,
"0 returned on Mock not implementing Test capability.");
$backend->set_always('capabilities', sub { return 'bar'; } );
is($illrq_obj->_backend_capability('Test'), 'bar',
"'bar' returned on Mock implementing Test capability.");
# metadata test: we need to be sure that we return the arbitrary values
# from the backend.
$backend->mock(
'metadata',
sub {
my ( $self, $rq ) = @_;
return {
ID => $rq->illrequest_id,
Title => $rq->patron->borrowernumber
}
}
);
is_deeply(
$illrq_obj->metadata,
{
ID => $illrq_obj->illrequest_id,
Title => $illrq_obj->patron->borrowernumber
},
"Test metadata."
);
# capabilities:
# No backend graph extension
$backend->set_always('status_graph', {});
is_deeply($illrq_obj->capabilities('COMP'),
{
prev_actions => [ 'REQ' ],
id => 'COMP',
name => 'Completed',
ui_method_name => 'Mark completed',
method => 'mark_completed',
next_actions => [ ],
ui_method_icon => 'fa-check',
},
"Dummy status graph for COMP.");
is($illrq_obj->capabilities('UNKNOWN'), undef,
"Dummy status graph for UNKNOWN.");
is_deeply($illrq_obj->capabilities(),
$illrq_obj->_core_status_graph,
"Dummy full status graph.");
# Simple backend graph extension
$backend->set_always('status_graph',
{
QER => {
prev_actions => [ 'REQ' ],
id => 'QER',
next_actions => [ 'REQ' ],
},
});
is_deeply($illrq_obj->capabilities('QER'),
{
prev_actions => [ 'REQ' ],
id => 'QER',
next_actions => [ 'REQ' ],
},
"Simple status graph for QER.");
is($illrq_obj->capabilities('UNKNOWN'), undef,
"Simple status graph for UNKNOWN.");
is_deeply($illrq_obj->capabilities(),
$illrq_obj->_status_graph_union(
$illrq_obj->_core_status_graph,
{
QER => {
prev_actions => [ 'REQ' ],
id => 'QER',
next_actions => [ 'REQ' ],
},
}
),
"Simple full status graph.");
# custom_capability:
# No backend graph extension
$backend->set_always('status_graph', {});
is($illrq_obj->custom_capability('unknown', {}), 0,
"Unknown candidate.");
# Simple backend graph extension
$backend->set_always('status_graph',
{
ID => {
prev_actions => [ 'REQ' ],
id => 'ID',
method => 'identity',
next_actions => [ 'REQ' ],
},
});
$backend->mock('identity',
sub { my ( $self, $params ) = @_; return $params->{other}; });
is($illrq_obj->custom_capability('identity', { test => 1 })->{test}, 1,
"Resolve identity custom_capability");
$schema->storage->txn_rollback;
};
subtest 'Backend core methods' => sub {
plan tests => 16;
$schema->storage->txn_begin;
# Build infrastructure
my $backend = Test::MockObject->new;
$backend->set_isa('Koha::Illbackends::Mock');
$backend->set_always('name', 'Mock');
my $config = Test::MockObject->new;
$config->set_always('backend_dir', "/tmp");
$config->set_always('getLimitRules',
{ default => { count => 0, method => 'active' } });
my $illrq = $builder->build({source => 'Illrequest'});
my $illrq_obj = Koha::Illrequests->find($illrq->{illrequest_id});
$illrq_obj->_config($config);
$illrq_obj->_backend($backend);
# expandTemplate:
is_deeply($illrq_obj->expandTemplate({ test => 1, method => "bar" }),
{
test => 1,
method => "bar",
template => "/tmp/Mock/intra-includes/bar.inc",
opac_template => "/tmp/Mock/opac-includes/bar.inc",
},
"ExpandTemplate");
# backend_create
# we are testing simple cases.
$backend->set_series('create',
{ stage => 'bar', method => 'create' },
{ stage => 'commit', method => 'create' },
{ stage => 'commit', method => 'create' });
# Test Copyright Clearance
t::lib::Mocks::mock_preference("ILLModuleCopyrightClearance", "Test Copyright Clearance.");
is_deeply($illrq_obj->backend_create({test => 1}),
{
error => 0,
status => '',
message => '',
method => 'create',
stage => 'copyrightclearance',
value => {
backend => "Mock"
}
},
"Backend create: copyright clearance.");
t::lib::Mocks::mock_preference("ILLModuleCopyrightClearance", "");
# Test non-commit
is_deeply($illrq_obj->backend_create({test => 1}),
{
stage => 'bar', method => 'create',
template => "/tmp/Mock/intra-includes/create.inc",
opac_template => "/tmp/Mock/opac-includes/create.inc",
},
"Backend create: arbitrary stage.");
# Test commit
is_deeply($illrq_obj->backend_create({test => 1}),
{
stage => 'commit', method => 'create', permitted => 0,
template => "/tmp/Mock/intra-includes/create.inc",
opac_template => "/tmp/Mock/opac-includes/create.inc",
},
"Backend create: arbitrary stage, not permitted.");
is($illrq_obj->status, "QUEUED", "Backend create: queued if restricted.");
$config->set_always('getLimitRules', {});
$illrq_obj->status('NEW');
is_deeply($illrq_obj->backend_create({test => 1}),
{
stage => 'commit', method => 'create', permitted => 1,
template => "/tmp/Mock/intra-includes/create.inc",
opac_template => "/tmp/Mock/opac-includes/create.inc",
},
"Backend create: arbitrary stage, permitted.");
is($illrq_obj->status, "NEW", "Backend create: not-queued.");
# backend_renew
$backend->set_series('renew', { stage => 'bar', method => 'renew' });
is_deeply($illrq_obj->backend_renew({test => 1}),
{
stage => 'bar', method => 'renew',
template => "/tmp/Mock/intra-includes/renew.inc",
opac_template => "/tmp/Mock/opac-includes/renew.inc",
},
"Backend renew: arbitrary stage.");
# backend_cancel
$backend->set_series('cancel', { stage => 'bar', method => 'cancel' });
is_deeply($illrq_obj->backend_cancel({test => 1}),
{
stage => 'bar', method => 'cancel',
template => "/tmp/Mock/intra-includes/cancel.inc",
opac_template => "/tmp/Mock/opac-includes/cancel.inc",
},
"Backend cancel: arbitrary stage.");
# backend_update_status
$backend->set_series('update_status', { stage => 'bar', method => 'update_status' });
is_deeply($illrq_obj->backend_update_status({test => 1}),
{
stage => 'bar', method => 'update_status',
template => "/tmp/Mock/intra-includes/update_status.inc",
opac_template => "/tmp/Mock/opac-includes/update_status.inc",
},
"Backend update_status: arbitrary stage.");
# backend_confirm
$backend->set_series('confirm', { stage => 'bar', method => 'confirm' });
is_deeply($illrq_obj->backend_confirm({test => 1}),
{
stage => 'bar', method => 'confirm',
template => "/tmp/Mock/intra-includes/confirm.inc",
opac_template => "/tmp/Mock/opac-includes/confirm.inc",
},
"Backend confirm: arbitrary stage.");
$config->set_always('partner_code', "ILLTSTLIB");
$backend->set_always('metadata', { Test => "Foobar" });
my $illbrn = $builder->build({
source => 'Branch',
value => { branchemail => "", branchreplyto => "" }
});
my $partner1 = $builder->build({
source => 'Borrower',
value => { categorycode => "ILLTSTLIB" },
});
my $partner2 = $builder->build({
source => 'Borrower',
value => { categorycode => "ILLTSTLIB" },
});
my $gen_conf = $illrq_obj->generic_confirm({
current_branchcode => $illbrn->{branchcode}
});
isnt(index($gen_conf->{value}->{draft}->{body}, $backend->metadata->{Test}), -1,
"Generic confirm: draft contains metadata."
);
is($gen_conf->{value}->{partners}->next->borrowernumber, $partner1->{borrowernumber},
"Generic cofnirm: partner 1 is correct."
);
is($gen_conf->{value}->{partners}->next->borrowernumber, $partner2->{borrowernumber},
"Generic confirm: partner 2 is correct."
);
dies_ok { $illrq_obj->generic_confirm({
current_branchcode => $illbrn->{branchcode},
stage => 'draft'
}) }
"Generic confirm: missing to dies OK.";
dies_ok { $illrq_obj->generic_confirm({
current_branchcode => $illbrn->{branchcode},
partners => $partner1->{email},
stage => 'draft'
}) }
"Generic confirm: missing from dies OK.";
$schema->storage->txn_rollback;
};
subtest 'Helpers' => sub {
plan tests => 9;
$schema->storage->txn_begin;
# Build infrastructure
my $backend = Test::MockObject->new;
$backend->set_isa('Koha::Illbackends::Mock');
$backend->set_always('name', 'Mock');
my $config = Test::MockObject->new;
$config->set_always('backend_dir', "/tmp");
my $patron = $builder->build({
source => 'Borrower',
value => { categorycode => "A" }
});
my $illrq = $builder->build({
source => 'Illrequest',
value => { branchcode => "CPL", borrowernumber => $patron->{borrowernumber} }
});
my $illrq_obj = Koha::Illrequests->find($illrq->{illrequest_id});
$illrq_obj->_config($config);
$illrq_obj->_backend($backend);
# getPrefix
$config->set_series('getPrefixes',
{ CPL => "TEST", TSL => "BAR", default => "DEFAULT" },
{ A => "ATEST", C => "CBAR", default => "DEFAULT" });
is($illrq_obj->getPrefix({ brw_cat => "C", branch => "CPL" }), "CBAR",
"getPrefix: brw_cat");
$config->set_series('getPrefixes',
{ CPL => "TEST", TSL => "BAR", default => "DEFAULT" },
{ A => "ATEST", C => "CBAR", default => "DEFAULT" });
is($illrq_obj->getPrefix({ brw_cat => "UNKNOWN", branch => "CPL" }), "TEST",
"getPrefix: branch");
$config->set_series('getPrefixes',
{ CPL => "TEST", TSL => "BAR", default => "DEFAULT" },
{ A => "ATEST", C => "CBAR", default => "DEFAULT" });
is($illrq_obj->getPrefix({ brw_cat => "UNKNOWN", branch => "UNKNOWN" }), "DEFAULT",
"getPrefix: default");
$config->set_always('getPrefixes', {});
is($illrq_obj->getPrefix({ brw_cat => "UNKNOWN", branch => "UNKNOWN" }), "",
"getPrefix: the empty prefix");
# id_prefix
$config->set_series('getPrefixes',
{ CPL => "TEST", TSL => "BAR", default => "DEFAULT" },
{ A => "ATEST", C => "CBAR", default => "DEFAULT" });
is($illrq_obj->id_prefix, "ATEST-", "id_prefix: brw_cat");
$config->set_series('getPrefixes',
{ CPL => "TEST", TSL => "BAR", default => "DEFAULT" },
{ AB => "ATEST", CD => "CBAR", default => "DEFAULT" });
is($illrq_obj->id_prefix, "TEST-", "id_prefix: branch");
$config->set_series('getPrefixes',
{ CPLT => "TEST", TSLT => "BAR", default => "DEFAULT" },
{ AB => "ATEST", CD => "CBAR", default => "DEFAULT" });
is($illrq_obj->id_prefix, "DEFAULT-", "id_prefix: default");
# requires_moderation
$illrq_obj->status('NEW')->store;
is($illrq_obj->requires_moderation, undef, "requires_moderation: No.");
$illrq_obj->status('CANCREQ')->store;
is($illrq_obj->requires_moderation, 'CANCREQ', "requires_moderation: Yes.");
$schema->storage->txn_rollback;
};
subtest 'Censorship' => sub {
plan tests => 2;
$schema->storage->txn_begin;
# Build infrastructure
my $backend = Test::MockObject->new;
$backend->set_isa('Koha::Illbackends::Mock');
$backend->set_always('name', 'Mock');
my $config = Test::MockObject->new;
$config->set_always('backend_dir', "/tmp");
my $illrq = $builder->build({source => 'Illrequest'});
my $illrq_obj = Koha::Illrequests->find($illrq->{illrequest_id});
$illrq_obj->_config($config);
$illrq_obj->_backend($backend);
$config->set_always('censorship', { censor_notes_staff => 1, censor_reply_date => 0 });
my $censor_out = $illrq_obj->_censor({ foo => 'bar', baz => 564 });
is_deeply($censor_out, { foo => 'bar', baz => 564, display_reply_date => 1 },
"_censor: not OPAC, reply_date = 1");
$censor_out = $illrq_obj->_censor({ foo => 'bar', baz => 564, opac => 1 });
is_deeply($censor_out, {
foo => 'bar', baz => 564, censor_notes_staff => 1,
display_reply_date => 1, opac => 1
}, "_censor: notes_staff = 0, reply_date = 0");
$schema->storage->txn_rollback;
};
subtest 'Checking Limits' => sub {
plan tests => 30;
$schema->storage->txn_begin;
# Build infrastructure
my $backend = Test::MockObject->new;
$backend->set_isa('Koha::Illbackends::Mock');
$backend->set_always('name', 'Mock');
my $config = Test::MockObject->new;
$config->set_always('backend_dir', "/tmp");
my $illrq = $builder->build({source => 'Illrequest'});
my $illrq_obj = Koha::Illrequests->find($illrq->{illrequest_id});
$illrq_obj->_config($config);
$illrq_obj->_backend($backend);
# getLimits
$config->set_series('getLimitRules',
{ CPL => { count => 1, method => 'test' } },
{ default => { count => 0, method => 'active' } });
is_deeply($illrq_obj->getLimits({ type => 'branch', value => "CPL" }),
{ count => 1, method => 'test' },
"getLimits: by value.");
is_deeply($illrq_obj->getLimits({ type => 'branch' }),
{ count => 0, method => 'active' },
"getLimits: by default.");
is_deeply($illrq_obj->getLimits({ type => 'branch', value => "CPL" }),
{ count => -1, method => 'active' },
"getLimits: by hard-coded.");
#_limit_counter
is($illrq_obj->_limit_counter('annual', { branchcode => $illrq_obj->branchcode }),
1, "_limit_counter: Initial branch annual count.");
is($illrq_obj->_limit_counter('active', { branchcode => $illrq_obj->branchcode }),
1, "_limit_counter: Initial branch active count.");
is($illrq_obj->_limit_counter('annual', { borrowernumber => $illrq_obj->borrowernumber }),
1, "_limit_counter: Initial patron annual count.");
is($illrq_obj->_limit_counter('active', { borrowernumber => $illrq_obj->borrowernumber }),
1, "_limit_counter: Initial patron active count.");
$builder->build({
source => 'Illrequest',
value => {
branchcode => $illrq_obj->branchcode,
borrowernumber => $illrq_obj->borrowernumber,
}
});
is($illrq_obj->_limit_counter('annual', { branchcode => $illrq_obj->branchcode }),
2, "_limit_counter: Add a qualifying request for branch annual count.");
is($illrq_obj->_limit_counter('active', { branchcode => $illrq_obj->branchcode }),
2, "_limit_counter: Add a qualifying request for branch active count.");
is($illrq_obj->_limit_counter('annual', { borrowernumber => $illrq_obj->borrowernumber }),
2, "_limit_counter: Add a qualifying request for patron annual count.");
is($illrq_obj->_limit_counter('active', { borrowernumber => $illrq_obj->borrowernumber }),
2, "_limit_counter: Add a qualifying request for patron active count.");
$builder->build({
source => 'Illrequest',
value => {
branchcode => $illrq_obj->branchcode,
borrowernumber => $illrq_obj->borrowernumber,
placed => "2005-05-31",
}
});
is($illrq_obj->_limit_counter('annual', { branchcode => $illrq_obj->branchcode }),
2, "_limit_counter: Add an out-of-date branch request.");
is($illrq_obj->_limit_counter('active', { branchcode => $illrq_obj->branchcode }),
3, "_limit_counter: Add a qualifying request for branch active count.");
is($illrq_obj->_limit_counter('annual', { borrowernumber => $illrq_obj->borrowernumber }),
2, "_limit_counter: Add an out-of-date patron request.");
is($illrq_obj->_limit_counter('active', { borrowernumber => $illrq_obj->borrowernumber }),
3, "_limit_counter: Add a qualifying request for patron active count.");
$builder->build({
source => 'Illrequest',
value => {
branchcode => $illrq_obj->branchcode,
borrowernumber => $illrq_obj->borrowernumber,
status => "COMP",
}
});
is($illrq_obj->_limit_counter('annual', { branchcode => $illrq_obj->branchcode }),
3, "_limit_counter: Add a qualifying request for branch annual count.");
is($illrq_obj->_limit_counter('active', { branchcode => $illrq_obj->branchcode }),
3, "_limit_counter: Add a completed request for branch active count.");
is($illrq_obj->_limit_counter('annual', { borrowernumber => $illrq_obj->borrowernumber }),
3, "_limit_counter: Add a qualifying request for patron annual count.");
is($illrq_obj->_limit_counter('active', { borrowernumber => $illrq_obj->borrowernumber }),
3, "_limit_counter: Add a completed request for patron active count.");
# check_limits:
# We've tested _limit_counter, so all we need to test here is whether the
# current counts of 3 for each work as they should against different
# configuration declarations.
# No limits
$config->set_always('getLimitRules', undef);
is($illrq_obj->check_limits({patron => $illrq_obj->patron,
librarycode => $illrq_obj->branchcode}),
1, "check_limits: no configuration => no limits.");
# Branch tests
$config->set_always('getLimitRules',
{ $illrq_obj->branchcode => { count => 1, method => 'active' } });
is($illrq_obj->check_limits({patron => $illrq_obj->patron,
librarycode => $illrq_obj->branchcode}),
0, "check_limits: branch active limit exceeded.");
$config->set_always('getLimitRules',
{ $illrq_obj->branchcode => { count => 1, method => 'annual' } });
is($illrq_obj->check_limits({patron => $illrq_obj->patron,
librarycode => $illrq_obj->branchcode}),
0, "check_limits: branch annual limit exceeded.");
$config->set_always('getLimitRules',
{ $illrq_obj->branchcode => { count => 4, method => 'active' } });
is($illrq_obj->check_limits({patron => $illrq_obj->patron,
librarycode => $illrq_obj->branchcode}),
1, "check_limits: branch active limit OK.");
$config->set_always('getLimitRules',
{ $illrq_obj->branchcode => { count => 4, method => 'annual' } });
is($illrq_obj->check_limits({patron => $illrq_obj->patron,
librarycode => $illrq_obj->branchcode}),
1, "check_limits: branch annual limit OK.");
# Patron tests
$config->set_always('getLimitRules',
{ $illrq_obj->patron->categorycode => { count => 1, method => 'active' } });
is($illrq_obj->check_limits({patron => $illrq_obj->patron,
librarycode => $illrq_obj->branchcode}),
0, "check_limits: patron category active limit exceeded.");
$config->set_always('getLimitRules',
{ $illrq_obj->patron->categorycode => { count => 1, method => 'annual' } });
is($illrq_obj->check_limits({patron => $illrq_obj->patron,
librarycode => $illrq_obj->branchcode}),
0, "check_limits: patron category annual limit exceeded.");
$config->set_always('getLimitRules',
{ $illrq_obj->patron->categorycode => { count => 4, method => 'active' } });
is($illrq_obj->check_limits({patron => $illrq_obj->patron,
librarycode => $illrq_obj->branchcode}),
1, "check_limits: patron category active limit OK.");
$config->set_always('getLimitRules',
{ $illrq_obj->patron->categorycode => { count => 4, method => 'annual' } });
is($illrq_obj->check_limits({patron => $illrq_obj->patron,
librarycode => $illrq_obj->branchcode}),
1, "check_limits: patron category annual limit OK.");
# One rule cancels the other
$config->set_series('getLimitRules',
# Branch rules allow request
{ $illrq_obj->branchcode => { count => 4, method => 'active' } },
# Patron rule forbids it
{ $illrq_obj->patron->categorycode => { count => 1, method => 'annual' } });
is($illrq_obj->check_limits({patron => $illrq_obj->patron,
librarycode => $illrq_obj->branchcode}),
0, "check_limits: patron category veto overrides branch OK.");
$config->set_series('getLimitRules',
# Branch rules allow request
{ $illrq_obj->branchcode => { count => 1, method => 'active' } },
# Patron rule forbids it
{ $illrq_obj->patron->categorycode => { count => 4, method => 'annual' } });
is($illrq_obj->check_limits({patron => $illrq_obj->patron,
librarycode => $illrq_obj->branchcode}),
0, "check_limits: branch veto overrides patron category OK.");
$schema->storage->txn_rollback;
};
1;

View File

@ -0,0 +1,136 @@
#!/usr/bin/env perl
# 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 Test::More tests => 1;
use Test::Mojo;
use Test::Warn;
use t::lib::TestBuilder;
use t::lib::Mocks;
use C4::Auth;
use Koha::Illrequests;
my $schema = Koha::Database->new->schema;
my $builder = t::lib::TestBuilder->new;
# FIXME: sessionStorage defaults to mysql, but it seems to break transaction handling
# this affects the other REST api tests
t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' );
my $remote_address = '127.0.0.1';
my $t = Test::Mojo->new('Koha::REST::V1');
subtest 'list() tests' => sub {
plan tests => 6;
$schema->storage->txn_begin;
Koha::Illrequests->search->delete;
my ( $borrowernumber, $session_id ) =
create_user_and_session( { authorized => 1 } );
## Authorized user tests
# No requests, so empty array should be returned
my $tx = $t->ua->build_tx( GET => '/api/v1/illrequests' );
$tx->req->cookies( { name => 'CGISESSID', value => $session_id } );
$tx->req->env( { REMOTE_ADDR => $remote_address } );
$t->request_ok($tx)->status_is(200)->json_is( [] );
# my $city_country = 'France';
# my $city = $builder->build(
# { source => 'City', value => { city_country => $city_country } } );
#
# # One city created, should get returned
# $tx = $t->ua->build_tx( GET => '/api/v1/cities' );
# $tx->req->cookies( { name => 'CGISESSID', value => $session_id } );
# $tx->req->env( { REMOTE_ADDR => $remote_address } );
# $t->request_ok($tx)->status_is(200)->json_is( [$city] );
#
# my $another_city = $builder->build(
# { source => 'City', value => { city_country => $city_country } } );
# my $city_with_another_country = $builder->build( { source => 'City' } );
#
# # Two cities created, they should both be returned
# $tx = $t->ua->build_tx( GET => '/api/v1/cities' );
# $tx->req->cookies( { name => 'CGISESSID', value => $session_id } );
# $tx->req->env( { REMOTE_ADDR => $remote_address } );
# $t->request_ok($tx)->status_is(200)
# ->json_is( [ $city, $another_city, $city_with_another_country ] );
#
# # Filtering works, two cities sharing city_country
# $tx =
# $t->ua->build_tx( GET => "/api/v1/cities?city_country=" . $city_country );
# $tx->req->cookies( { name => 'CGISESSID', value => $session_id } );
# $tx->req->env( { REMOTE_ADDR => $remote_address } );
# my $result =
# $t->request_ok($tx)->status_is(200)->json_is( [ $city, $another_city ] );
#
# $tx = $t->ua->build_tx(
# GET => "/api/v1/cities?city_name=" . $city->{city_name} );
# $tx->req->cookies( { name => 'CGISESSID', value => $session_id } );
# $tx->req->env( { REMOTE_ADDR => $remote_address } );
# $t->request_ok($tx)->status_is(200)->json_is( [$city] );
# Warn on unsupported query parameter
$tx = $t->ua->build_tx( GET => '/api/v1/illrequests?request_blah=blah' );
$tx->req->cookies( { name => 'CGISESSID', value => $session_id } );
$tx->req->env( { REMOTE_ADDR => $remote_address } );
$t->request_ok($tx)->status_is(400)->json_is(
[{ path => '/query/request_blah', message => 'Malformed query string'}]
);
$schema->storage->txn_rollback;
};
sub create_user_and_session {
my $args = shift;
my $flags = ( $args->{authorized} ) ? $args->{authorized} : 0;
my $dbh = C4::Context->dbh;
my $user = $builder->build(
{
source => 'Borrower',
value => {
flags => $flags
}
}
);
# Create a session for the authorized user
my $session = C4::Auth::get_session('');
$session->param( 'number', $user->{borrowernumber} );
$session->param( 'id', $user->{userid} );
$session->param( 'ip', '127.0.0.1' );
$session->param( 'lasttime', time() );
$session->flush;
if ( $args->{authorized} ) {
$dbh->do( "
INSERT INTO user_permissions (borrowernumber,module_bit,code)
VALUES (?,3,'parameters_remaining_permissions')", undef,
$user->{borrowernumber} );
}
return ( $user->{borrowernumber}, $session->id );
}
1;