From dc43d9985e021f9757bb249f79a3a8d6dbf8e89b Mon Sep 17 00:00:00 2001 From: Alex Sassmannshausen Date: Fri, 3 Feb 2017 16:58:35 +0100 Subject: [PATCH] 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 Signed-off-by: Tomas Cohen Arazi --- Koha/Illrequest.pm | 935 +++++++++++++++++++++ Koha/Illrequest/Config.pm | 384 +++++++++ Koha/Illrequestattribute.pm | 51 ++ Koha/Illrequestattributes.pm | 55 ++ Koha/Illrequests.pm | 97 +++ Koha/REST/V1/Illrequests.pm | 85 ++ Makefile.PL | 1 + api/v1/swagger/paths.json | 3 + api/v1/swagger/paths/illrequests.json | 98 +++ etc/koha-conf.xml | 21 + ill/ill-requests.pl | 252 ++++++ koha-tmpl/intranet-tmpl/prog/css/staff-global.css | 92 ++ .../intranet-tmpl/prog/en/includes/circ-menu.inc | 3 + .../intranet-tmpl/prog/en/includes/ill-toolbar.inc | 24 + .../intranet-tmpl/prog/en/includes/permissions.inc | 1 + .../prog/en/modules/ill/ill-requests.tt | 698 +++++++++++++++ .../intranet-tmpl/prog/en/modules/intranet-main.tt | 5 + .../opac-tmpl/bootstrap/en/includes/usermenu.inc | 9 + .../bootstrap/en/modules/opac-illrequests.tt | 221 +++++ .../bootstrap/en/modules/opac-results-grouped.tt | 30 +- .../opac-tmpl/bootstrap/en/modules/opac-results.tt | 33 +- koha-tmpl/opac-tmpl/bootstrap/less/opac.less | 38 + opac/opac-illrequests.pl | 129 +++ t/db_dependent/Illrequest/Config.t | 473 +++++++++++ t/db_dependent/Illrequestattributes.t | 63 ++ t/db_dependent/Illrequests.t | 792 +++++++++++++++++ t/db_dependent/api/v1/illrequests.t | 136 +++ 27 files changed, 4712 insertions(+), 17 deletions(-) create mode 100644 Koha/Illrequest.pm create mode 100644 Koha/Illrequest/Config.pm create mode 100644 Koha/Illrequestattribute.pm create mode 100644 Koha/Illrequestattributes.pm create mode 100644 Koha/Illrequests.pm create mode 100644 Koha/REST/V1/Illrequests.pm create mode 100644 api/v1/swagger/paths/illrequests.json create mode 100755 ill/ill-requests.pl create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt create mode 100755 opac/opac-illrequests.pl create mode 100644 t/db_dependent/Illrequest/Config.t create mode 100644 t/db_dependent/Illrequestattributes.t create mode 100644 t/db_dependent/Illrequests.t create mode 100644 t/db_dependent/api/v1/illrequests.t diff --git a/Koha/Illrequest.pm b/Koha/Illrequest.pm new file mode 100644 index 0000000..12ef9aa --- /dev/null +++ b/Koha/Illrequest.pm @@ -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} = <metadata; + while (my ($title, $value) = each %{$details}) { + $draft->{body} .= " - " . $title . ": " . $value . "\n" + if $value; + } + $draft->{body} .= <$_ } + 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 + +=cut + +1; diff --git a/Koha/Illrequest/Config.pm b/Koha/Illrequest/Config.pm new file mode 100644 index 0000000..3bc78d1 --- /dev/null +++ b/Koha/Illrequest/Config.pm @@ -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 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 + +=cut + +1; diff --git a/Koha/Illrequestattribute.pm b/Koha/Illrequestattribute.pm new file mode 100644 index 0000000..16bc086 --- /dev/null +++ b/Koha/Illrequestattribute.pm @@ -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 + +=cut + +1; diff --git a/Koha/Illrequestattributes.pm b/Koha/Illrequestattributes.pm new file mode 100644 index 0000000..e05fa6e --- /dev/null +++ b/Koha/Illrequestattributes.pm @@ -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 + +=cut + +1; diff --git a/Koha/Illrequests.pm b/Koha/Illrequests.pm new file mode 100644 index 0000000..85cc711 --- /dev/null +++ b/Koha/Illrequests.pm @@ -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 + +=cut + +1; diff --git a/Koha/REST/V1/Illrequests.pm b/Koha/REST/V1/Illrequests.pm new file mode 100644 index 0000000..0807eb1 --- /dev/null +++ b/Koha/REST/V1/Illrequests.pm @@ -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; diff --git a/Makefile.PL b/Makefile.PL index af6ea2d..ada4224 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -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'}, diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json index 147e985..9c3a6d2 100644 --- a/api/v1/swagger/paths.json +++ b/api/v1/swagger/paths.json @@ -22,5 +22,8 @@ }, "/patrons/{borrowernumber}": { "$ref": "paths/patrons.json#/~1patrons~1{borrowernumber}" + }, + "/illrequests": { + "$ref": "paths/illrequests.json#/~1illrequests" } } diff --git a/api/v1/swagger/paths/illrequests.json b/api/v1/swagger/paths/illrequests.json new file mode 100644 index 0000000..ddafd80 --- /dev/null +++ b/api/v1/swagger/paths/illrequests.json @@ -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" + } + } + } + } +} diff --git a/etc/koha-conf.xml b/etc/koha-conf.xml index 4ccee3a..067e978 100644 --- a/etc/koha-conf.xml +++ b/etc/koha-conf.xml @@ -153,5 +153,26 @@ __PAZPAR2_TOGGLE_XML_POST__ 50 2 + + + __PERL_MODULE_DIR__/Koha/Illbackends + + hide + + hide + + branch + + ILLLIBS + + diff --git a/ill/ill-requests.pl b/ill/ill-requests.pl new file mode 100755 index 0000000..26cb350 --- /dev/null +++ b/ill/ill-requests.pl @@ -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'); +} diff --git a/koha-tmpl/intranet-tmpl/prog/css/staff-global.css b/koha-tmpl/intranet-tmpl/prog/css/staff-global.css index 3a26b0d..0897f32 100644 --- a/koha-tmpl/intranet-tmpl/prog/css/staff-global.css +++ b/koha-tmpl/intranet-tmpl/prog/css/staff-global.css @@ -3031,3 +3031,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; +} diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/circ-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/circ-menu.inc index e621d64..4006f6c 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/circ-menu.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/circ-menu.inc @@ -112,5 +112,8 @@ [% IF Koha.Preference('HouseboundModule') %] [% IF houseboundview %]
  • [% ELSE %]
  • [% END %]Housebound
  • [% END %] + [% IF Koha.Preference('ILLModule') %] +
  • Interlibrary loans
  • + [% END %] [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc new file mode 100644 index 0000000..3f541d9 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc @@ -0,0 +1,24 @@ +[% USE Koha %] +[% IF Koha.Preference('ILLModule ') %] +
    + [% IF backends.size > 1 %] + + [% ELSE %] + + New ILL request + + [% END %] + + List requests + +
    +[% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc index 7c6275a..4a1ce81 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc @@ -20,6 +20,7 @@ [%- CASE 'plugins' -%]Koha plugins [%- CASE 'lists' -%]Lists [%- CASE 'clubs' -%]Patron clubs + [%- CASE 'ill' -%]Create and modify Interlibrary loan requests [%- END -%] [%- END -%] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt new file mode 100644 index 0000000..c013727 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt @@ -0,0 +1,698 @@ +[% USE Branches %] +[% USE Koha %] + +[% INCLUDE 'doc-head-open.inc' %] +Koha › ILL requests › +[% INCLUDE 'doc-head-close.inc' %] + + + +[% INCLUDE 'datatables.inc' %] + + + + +[% INCLUDE 'header.inc' %] +[% INCLUDE 'cat-search.inc' %] + + + +
    +
    +
    +
    + [% INCLUDE 'ill-toolbar.inc' %] + + [% IF whole.error %] +

    Error performing operation

    + +

    We encountered an error:

    +

    +

    [% whole.message %] ([% whole.status %])
    +

    + [% END %] + + [% IF query_type == 'create' %] +

    New ILL request

    + [% IF whole.stage == 'copyrightclearance' %] +
    +

    + [% Koha.Preference('ILLModuleCopyrightClearance') %] +

    + Yes + No +
    + [% ELSE %] + [% PROCESS $whole.template %] + [% END %] + + [% ELSIF query_type == 'confirm' %] +

    Confirm ILL request

    + [% PROCESS $whole.template %] + + [% ELSIF query_type == 'cancel' and !whole.error %] +

    Cancel a confirmed request

    + [% PROCESS $whole.template %] + + [% ELSIF query_type == 'generic_confirm' %] +

    Place request with partner libraries

    + + [% IF whole.value.partners %] + [% ill_url = here_link _ "?method=illview&illrequest_id=" _ request.illrequest_id %] +
    +
    + Interlibrary loan request details +
      +
    1. + + +
    2. +
    3. + + + +
    4. +
    5. + + +
    6. +
    7. + + +
    8. +
    + + + +
    +
    + + Cancel +
    +
    + [% ELSE %] +
    + Interlibrary loan request details +

    No partners have been defined yet. Please create appropriate patron records (by default ILLLIBS category).

    +

    Be sure to provide email addresses for these patrons.

    +

    Cancel

    +
    + [% END %] + + + [% ELSIF query_type == 'edit_action' %] +
    +
    + Request details +
      +
    1. + + +
    2. +
    3. + + +
    4. +
    5. + + +
    6. +
    7. + + [% stat = request.status %] + [% request.capabilities.$stat.name %] +
    8. +
    9. + + [% request.updated %] +
    10. +
    11. + + [% request.medium %] +
    12. +
    13. + + [% request.cost %] +
    14. +
    15. + + [% request.id_prefix _ request.illrequest_id %] +
    16. +
    17. + + +
    18. +
    19. + + +
    20. +
    +
    +
    + + + + + Cancel +
    +
    + + [% ELSIF query_type == 'delete_confirm' %] + +
    +

    Are you sure you wish to delete this request?

    +

    + Yes + No +

    +
    + + + [% ELSIF query_type == 'illview' %] + [% actions = request.available_actions %] + [% capabilities = request.capabilities %] + [% req_status = request.status %] +

    Manage ILL request

    +
    + + + Edit request + + [% FOREACH action IN actions %] + [% IF action.method != 0 %] + + + [% action.ui_method_name %] + + [% END %] + [% END %] +
    +
    +
    +

    Request details

    +
    +
    +

    Details from library

    +
    +
    + Order ID: + [% request.orderid || "N/A" %] +
    +
    + Borrower: + [% borrowerlink = "/cgi-bin/koha/members/moremember.pl" + _ "?borrowernumber=" _ request.patron.borrowernumber %] + + [% request.patron.firstname _ " " + _ request.patron.surname _ " [" + _ request.patron.cardnumber + _ "]" %] + +
    + +
    + Biblio number: + [% request.biblio_id || "N/A" %] +
    +
    + Branch: + [% Branches.GetName(request.branchcode) %] +
    +
    + Status: + [% capabilities.$req_status.name %] +
    +
    + Last updated: + [% request.updated %] +
    +
    + Request type: + [% request.medium %] +
    +
    + Cost: + [% request.cost || "N/A" %] +
    +
    + Request number: + [% request.id_prefix _ request.illrequest_id %] +
    +
    + Staff notes: +
    [% request.notesstaff %]
    +
    +
    + Notes: +
    [% request.notesopac %]
    +
    +
    +
    +

    Details from supplier ([% request.backend %])

    + [% FOREACH meta IN request.metadata %] +
    + [% meta.key %]: + [% meta.value %] +
    + [% END %] +
    +
    +

    Toggle full supplier metadata

    +
    + [% FOREACH attr IN request.illrequestattributes %] +
    + [% attr.type %]: + [% attr.value %] +
    + [% END %] +
    + +
    +
    +
    + + [% ELSIF query_type == 'illlist' %] + +

    View ILL requests

    +
    +

    Details for all requests

    + +
    + Toggle additional columns: +
    + + + + + + + + +
    +
    + [% ELSE %] + + [% INCLUDE $whole.template %] + + [% END %] +
    +
    +
    +
    + +[% TRY %] +[% PROCESS backend_jsinclude %] +[% CATCH %] +[% END %] + +[% INCLUDE 'intranet-bottom.inc' %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt index 2a69dd9..32a1409 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt @@ -63,6 +63,11 @@
  • Authorities
  • + [% IF Koha.Preference('ILLModule') %] +
  • + ILL requests +
  • + [% END %] diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc index 9afa331..7d27f22 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc +++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc @@ -104,6 +104,15 @@ [% END %] ask for a discharge [% END %] + + [% IF Koha.Preference( 'ILLModule' ) == 1 %] + [% IF ( illrequestsview ) %] +
  • + [% ELSE %] +
  • + [% END %] + your interlibrary loan requests
  • + [% END %] [% END %] diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt new file mode 100644 index 0000000..e0448c1 --- /dev/null +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt @@ -0,0 +1,221 @@ +[% USE Koha %] +[% USE Branches %] +[% INCLUDE 'doc-head-open.inc' %] +[% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha online[% END %] catalog › Your Interlibrary loan requests[% INCLUDE 'doc-head-close.inc' %] +[% BLOCK cssinclude %][% END %] + +[% INCLUDE 'bodytag.inc' bodyid='opac-illrequests' bodyclass='scrollto' %] +[% BLOCK messages %] + [% IF message == "1" %] + + [% ELSIF message == "2" %] + + [% END %] +[% END %] +[% INCLUDE 'masthead.inc' %] +
    + + +
    +
    + [% IF ( OpacNav||loggedinusername ) && !print %] +
    + +
    + [% END %] + + [% IF ( OpacNav||loggedinusername ) %] +
    + [% ELSE %] +
    + [% END %] +
    + [% IF method == 'create' %] +

    New Interlibrary loan request

    + [% INCLUDE messages %] + [% IF backends %] +
    +
    + + +
    +
    + + +
    +
    + [% ELSE %] + [% PROCESS $whole.opac_template %] + [% END %] + [% ELSIF method == 'list' %] +

    Interlibrary loan requests

    + [% INCLUDE messages %] + + + + + + + + + + + + + + + + + + [% FOREACH request IN requests %] + [% status = request.status %] + + + + + + + + + + + [% END %] + +
    AuthorTitleRequested fromRequest typeStatusRequest placedLast updated
    [% request.metadata.Author || 'N/A' %][% request.metadata.Title || 'N/A' %][% request.backend %][% request.medium %][% request.capabilities.$status.name %][% request.placed %][% request.updated %] + View +
    + [% ELSIF method == 'view' %] +

    View Interlibrary loan request

    + [% INCLUDE messages %] + [% status = request.status %] +
    +
    + Details from library +
      +
    1. + + [% request.backend %] +
    2. + [% IF request.biblio_id %] +
    3. + + Click here to view +
    4. + [% END %] +
    5. + + [% Branches.GetName(request.branchcode) %] +
    6. +
    7. + + [% request.capabilities.$status.name %] +
    8. +
    9. + + [% request.medium %] +
    10. +
    11. + + [% request.placed %] +
    12. +
    13. + + [% request.updated %] +
    14. +
    15. + + [% IF !request.completed %] + + [% ELSE %] + [% request.notesopac %] + [% END %] +
    16. +
    +
    +
    + Details from [% request.backend %] + [% FOREACH meta IN request.metadata %] +
    + [% meta.key %]: + [% meta.value || 'N/A' %] +
    + [% END %] +
    +
    + + + [% IF !request.completed %] + [% IF request.status == "NEW" %] + Request cancellation + [% END %] + + [% END %] + Cancel +
    +
    + [% END %] +
    +
    +
    +
    +
    + +[% INCLUDE 'opac-bottom.inc' %] + +[% BLOCK jsinclude %] +[% INCLUDE 'datatables.inc' %] + +[% TRY %] +[% PROCESS backend_jsinclude %] +[% CATCH %] +[% END %] +[% END %] diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results-grouped.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results-grouped.tt index 8595f9a..eaa20b3 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results-grouped.tt +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results-grouped.tt @@ -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 %] -
    Not finding what you're looking for?
    Make a purchase suggestion
    - [% ELSE %] - [% IF ( loggedinusername ) %]
    Not finding what you're looking for?
    Make a purchase suggestion
    [% END %] - [% END %] + [% IF + Koha.Preference( 'suggestion' ) == 1 && + ( + Koha.Preference( 'AnonSuggestions' ) == 1 || + loggedinusername || + Koha.Preference( 'ILLModule' ) == 1 + ) + %] +
    + Not finding what you're looking for? + +
    [% END %] -
    diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt index a5a7c70..11cf98e 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt @@ -562,16 +562,29 @@ [% END # / IF total %] - [% IF Koha.Preference( 'suggestion' ) == 1 %] - [% IF Koha.Preference( 'AnonSuggestions' ) == 1 %] -
    Not finding what you're looking for?
    Make a purchase suggestion
    - [% ELSE %] - [% IF ( loggedinusername ) %] -
    - Not finding what you're looking for?
    Make a purchase suggestion -
    - [% END %] - [% END %] + [% IF + Koha.Preference( 'suggestion' ) == 1 && + ( + Koha.Preference( 'AnonSuggestions' ) == 1 || + loggedinusername || + Koha.Preference( 'ILLModule' ) == 1 + ) + %] +
    + Not finding what you're looking for? + +
    [% END %] diff --git a/koha-tmpl/opac-tmpl/bootstrap/less/opac.less b/koha-tmpl/opac-tmpl/bootstrap/less/opac.less index 63961e8..c4e55c2 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/less/opac.less +++ b/koha-tmpl/opac-tmpl/bootstrap/less/opac.less @@ -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; diff --git a/opac/opac-illrequests.pl b/opac/opac-illrequests.pl new file mode 100755 index 0000000..9ff413a --- /dev/null +++ b/opac/opac-illrequests.pl @@ -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 . + +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; diff --git a/t/db_dependent/Illrequest/Config.t b/t/db_dependent/Illrequest/Config.t new file mode 100644 index 0000000..f44fa85 --- /dev/null +++ b/t/db_dependent/Illrequest/Config.t @@ -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 . + +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; diff --git a/t/db_dependent/Illrequestattributes.t b/t/db_dependent/Illrequestattributes.t new file mode 100644 index 0000000..ceb0474 --- /dev/null +++ b/t/db_dependent/Illrequestattributes.t @@ -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; diff --git a/t/db_dependent/Illrequests.t b/t/db_dependent/Illrequests.t new file mode 100644 index 0000000..34b9eda --- /dev/null +++ b/t/db_dependent/Illrequests.t @@ -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; diff --git a/t/db_dependent/api/v1/illrequests.t b/t/db_dependent/api/v1/illrequests.t new file mode 100644 index 0000000..fb306ba --- /dev/null +++ b/t/db_dependent/api/v1/illrequests.t @@ -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; -- 2.7.4