Bug 7317: Interlibrary loans framework for Koha.
authorAlex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
Fri, 3 Feb 2017 15:58:35 +0000 (16:58 +0100)
committerJonathan Druart <jonathan.druart@bugs.koha-community.org>
Thu, 9 Nov 2017 14:42:12 +0000 (11:42 -0300)
This Commit is at the heart of adding an interlibrary loans framework
for Koha.  The framework does not prescribe a particular workflow.
Instead it provides a general framework that can be extended &
implemented by individual backends whose responsibility it is to
implement a specific workflow.

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

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

Outside of this we simply add new files & functionality.

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

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>

27 files changed:
Koha/Illrequest.pm [new file with mode: 0644]
Koha/Illrequest/Config.pm [new file with mode: 0644]
Koha/Illrequestattribute.pm [new file with mode: 0644]
Koha/Illrequestattributes.pm [new file with mode: 0644]
Koha/Illrequests.pm [new file with mode: 0644]
Koha/REST/V1/Illrequests.pm [new file with mode: 0644]
Makefile.PL
api/v1/swagger/paths.json
api/v1/swagger/paths/illrequests.json [new file with mode: 0644]
etc/koha-conf.xml
ill/ill-requests.pl [new file with mode: 0755]
koha-tmpl/intranet-tmpl/prog/css/staff-global.css
koha-tmpl/intranet-tmpl/prog/en/includes/circ-menu.inc
koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc
koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt
koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc
koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt [new file with mode: 0644]
koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results-grouped.tt
koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt
koha-tmpl/opac-tmpl/bootstrap/less/opac.less
opac/opac-illrequests.pl [new file with mode: 0755]
t/db_dependent/Illrequest/Config.t [new file with mode: 0644]
t/db_dependent/Illrequestattributes.t [new file with mode: 0644]
t/db_dependent/Illrequests.t [new file with mode: 0644]
t/db_dependent/api/v1/illrequests.t [new file with mode: 0644]

diff --git a/Koha/Illrequest.pm b/Koha/Illrequest.pm
new file mode 100644 (file)
index 0000000..12ef9aa
--- /dev/null
@@ -0,0 +1,935 @@
+package Koha::Illrequest;
+
+# Copyright PTFS Europe 2016
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# Koha; if not, write to the Free Software Foundation, Inc., 51 Franklin
+# Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+# use Modern::Perl;
+
+use Clone 'clone';
+use File::Basename qw/basename/;
+use Koha::Database;
+use Koha::Email;
+use Koha::Illrequest;
+use Koha::Illrequestattributes;
+use Koha::Patron;
+use Mail::Sendmail;
+use Try::Tiny;
+
+use base qw(Koha::Object);
+
+=head1 NAME
+
+Koha::Illrequest - Koha Illrequest Object class
+
+=head1 (Re)Design
+
+An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
+of related Illrequestattributes.
+
+The former encapsulates the basic necessary information that any ILL requires
+to be usable in Koha.  The latter is a set of additional properties used by
+one of the backends.
+
+The former subsumes the legacy "Status" object.  The latter remains
+encapsulated in the "Record" object.
+
+TODO:
+
+- Anything invoking the ->status method; annotated with:
+  + # Old use of ->status !
+
+=head1 API
+
+=head2 Backend API Response Principles
+
+All methods should return a hashref in the following format:
+
+=item * error
+
+This should be set to 1 if an error was encountered.
+
+=item * status
+
+The status should be a string from the list of statuses detailed below.
+
+=item * message
+
+The message is a free text field that can be passed on to the end user.
+
+=item * value
+
+The value returned by the method.
+
+=over
+
+=head2 Interface Status Messages
+
+=over
+
+=item * branch_address_incomplete
+
+An interface request has determined branch address details are incomplete.
+
+=item * cancel_success
+
+The interface's cancel_request method was successful in cancelling the
+Illrequest using the API.
+
+=item * cancel_fail
+
+The interface's cancel_request method failed to cancel the Illrequest using
+the API.
+
+=item * unavailable
+
+The interface's request method returned saying that the desired item is not
+available for request.
+
+=head2 Class Methods
+
+=cut
+
+=head3 type
+
+=cut
+
+sub _type {
+    return 'Illrequest';
+}
+
+sub illrequestattributes {
+    my ( $self ) = @_;
+    return Koha::Illrequestattributes->_new_from_dbic(
+        scalar $self->_result->illrequestattributes
+    );
+}
+
+sub patron {
+    my ( $self ) = @_;
+    return Koha::Patron->_new_from_dbic(
+        scalar $self->_result->borrowernumber
+    );
+}
+
+sub load_backend {
+    my ( $self, $backend_id ) = @_;
+
+    my @raw = qw/Koha Illbackends/; # Base Path
+
+    my $backend_name = $backend_id || $self->backend;
+    $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
+    $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
+    require $location;
+    $self->{_my_backend} = $backend_class->new({ config => $self->_config });
+    return $self;
+}
+
+=head3 _backend
+
+    my $backend = $abstract->_backend($new_backend);
+    my $backend = $abstract->_backend;
+
+Getter/Setter for our API object.
+
+=cut
+
+sub _backend {
+    my ( $self, $backend ) = @_;
+    $self->{_my_backend} = $backend if ( $backend );
+    # Dynamically load our backend object, as late as possible.
+    $self->load_backend unless ( $self->{_my_backend} );
+    return $self->{_my_backend};
+}
+
+=head3 _backend_capability
+
+    my $backend_capability_result = $self->_backend_capability($name, $args);
+
+This is a helper method to invoke optional capabilities in the backend.  If
+the capability named by $name is not supported, return 0, else invoke it,
+passing $args along with the invocation, and return its return value.
+
+NOTE: this module suffers from a confusion in termninology:
+
+in _backend_capability, the notion of capability refers to an optional feature
+that is implemented in core, but might not be supported by a given backend.
+
+in capabilities & custom_capability, capability refers to entries in the
+status_graph (after union between backend and core).
+
+The easiest way to fix this would be to fix the terminology in
+capabilities & custom_capability and their callers.
+
+=cut
+
+sub _backend_capability {
+    my ( $self, $name, $args ) = @_;
+    my $capability = 0;
+    try {
+        $capability = $self->_backend->capabilities($name);
+    } catch {
+        return 0;
+    };
+    if ( $capability ) {
+        return &{$capability}($args);
+    } else {
+        return 0;
+    }
+}
+
+=head3 _config
+
+    my $config = $abstract->_config($config);
+    my $config = $abstract->_config;
+
+Getter/Setter for our config object.
+
+=cut
+
+sub _config {
+    my ( $self, $config ) = @_;
+    $self->{_my_config} = $config if ( $config );
+    # Load our config object, as late as possible.
+    unless ( $self->{_my_config} ) {
+        $self->{_my_config} = Koha::Illrequest::Config->new;
+    }
+    return $self->{_my_config};
+}
+
+=head3 metadata
+
+=cut
+
+sub metadata {
+    my ( $self ) = @_;
+    return $self->_backend->metadata($self);
+}
+
+=head3 _core_status_graph
+
+    my $core_status_graph = $illrequest->_core_status_graph;
+
+Returns ILL module's default status graph.  A status graph defines the list of
+available actions at any stage in the ILL workflow.  This is for instance used
+by the perl script & template to generate the correct buttons to display to
+the end user at any given point.
+
+=cut
+
+sub _core_status_graph {
+    my ( $self ) = @_;
+    return {
+        NEW => {
+            prev_actions => [ ],                           # Actions containing buttons
+                                                           # leading to this status
+            id             => 'NEW',                       # ID of this status
+            name           => 'New request',               # UI name of this status
+            ui_method_name => 'New request',               # UI name of method leading
+                                                           # to this status
+            method         => 'create',                    # method to this status
+            next_actions   => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
+                                                           # requests with this status
+            ui_method_icon => 'fa-plus',                   # UI Style class
+        },
+        REQ => {
+            prev_actions   => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
+            id             => 'REQ',
+            name           => 'Requested',
+            ui_method_name => 'Confirm request',
+            method         => 'confirm',
+            next_actions   => [ 'REQREV', 'COMP' ],
+            ui_method_icon => 'fa-check',
+        },
+        GENREQ => {
+            prev_actions   => [ 'NEW', 'REQREV' ],
+            id             => 'GENREQ',
+            name           => 'Requested from partners',
+            ui_method_name => 'Place request with partners',
+            method         => 'generic_confirm',
+            next_actions   => [ 'COMP' ],
+            ui_method_icon => 'fa-send-o',
+        },
+        REQREV => {
+            prev_actions   => [ 'REQ' ],
+            id             => 'REQREV',
+            name           => 'Request reverted',
+            ui_method_name => 'Revert Request',
+            method         => 'cancel',
+            next_actions   => [ 'REQ', 'GENREQ', 'KILL' ],
+            ui_method_icon => 'fa-times',
+        },
+        QUEUED => {
+            prev_actions   => [ ],
+            id             => 'QUEUED',
+            name           => 'Queued request',
+            ui_method_name => 0,
+            method         => 0,
+            next_actions   => [ 'REQ', 'KILL' ],
+            ui_method_icon => 0,
+        },
+        CANCREQ => {
+            prev_actions   => [ 'NEW' ],
+            id             => 'CANCREQ',
+            name           => 'Cancellation requested',
+            ui_method_name => 0,
+            method         => 0,
+            next_actions   => [ 'KILL', 'REQ' ],
+            ui_method_icon => 0,
+        },
+        COMP => {
+            prev_actions   => [ 'REQ' ],
+            id             => 'COMP',
+            name           => 'Completed',
+            ui_method_name => 'Mark completed',
+            method         => 'mark_completed',
+            next_actions   => [ ],
+            ui_method_icon => 'fa-check',
+        },
+        KILL => {
+            prev_actions   => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
+            id             => 'KILL',
+            name           => 0,
+            ui_method_name => 'Delete request',
+            method         => 'delete',
+            next_actions   => [ ],
+            ui_method_icon => 'fa-trash',
+        },
+    };
+}
+
+=head3 _core_status_graph
+
+    my $status_graph = $illrequest->_core_status_graph($origin, $new_graph);
+
+Return a new status_graph, the result of merging $origin & new_graph.  This is
+operation is a union over the sets defied by the two graphs.
+
+Each entry in $new_graph is added to $origin.  We do not provide a syntax for
+'subtraction' of entries from $origin.
+
+Whilst it is not intended that this works, you can override entries in $origin
+with entries with the same key in $new_graph.  This can lead to problematic
+behaviour when $new_graph adds an entry, which modifies a dependent entry in
+$origin, only for the entry in $origin to be replaced later with a new entry
+from $new_graph.
+
+NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
+i.e. each of the graphs need to be correct at the outset of the operation.
+
+=cut
+
+sub _status_graph_union {
+    my ( $self, $core_status_graph, $backend_status_graph ) = @_;
+    # Create new status graph with:
+    # - all core_status_graph
+    # - for-each each backend_status_graph
+    #   + add to new status graph
+    #   + for each core prev_action:
+    #     * locate core_status
+    #     * update next_actions with additional next action.
+    #   + for each core next_action:
+    #     * locate core_status
+    #     * update prev_actions with additional prev action
+
+    my @core_status_ids = keys %{$core_status_graph};
+    my $status_graph = clone($core_status_graph);
+
+    foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
+        $backend_status = $backend_status_graph->{$backend_status_key};
+        # Add to new status graph
+        $status_graph->{$backend_status_key} = $backend_status;
+        # Update all core methods' next_actions.
+        foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
+            if ( grep $prev_action, @core_status_ids ) {
+                my @next_actions =
+                     @{$status_graph->{$prev_action}->{next_actions}};
+                push @next_actions, $backend_status_key;
+                $status_graph->{$prev_action}->{next_actions}
+                    = \@next_actions;
+            }
+        }
+        # Update all core methods' prev_actions
+        foreach my $next_action ( @{$backend_status->{next_actions}} ) {
+            if ( grep $next_action, @core_status_ids ) {
+                my @prev_actions =
+                     @{$status_graph->{$next_action}->{prev_actions}};
+                push @prev_actions, $backend_status_key;
+                $status_graph->{$next_action}->{prev_actions}
+                    = \@prev_actions;
+            }
+        }
+    }
+
+    return $status_graph;
+}
+
+### Core API methods
+
+=head3 capabilities
+
+    my $capabilities = $illrequest->capabilities;
+
+Return a hashref mapping methods to operation names supported by the queried
+backend.
+
+Example return value:
+
+    { create => "Create Request", confirm => "Progress Request" }
+
+NOTE: this module suffers from a confusion in termninology:
+
+in _backend_capability, the notion of capability refers to an optional feature
+that is implemented in core, but might not be supported by a given backend.
+
+in capabilities & custom_capability, capability refers to entries in the
+status_graph (after union between backend and core).
+
+The easiest way to fix this would be to fix the terminology in
+capabilities & custom_capability and their callers.
+
+=cut
+
+sub capabilities {
+    my ( $self, $status ) = @_;
+    # Generate up to date status_graph
+    my $status_graph = $self->_status_graph_union(
+        $self->_core_status_graph,
+        $self->_backend->status_graph({
+            request => $self,
+            other   => {}
+        })
+    );
+    # Extract available actions from graph.
+    return $status_graph->{$status} if $status;
+    # Or return entire graph.
+    return $status_graph;
+}
+
+=head3 custom_capability
+
+Return the result of invoking $CANDIDATE on this request's backend with
+$PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
+
+NOTE: this module suffers from a confusion in termninology:
+
+in _backend_capability, the notion of capability refers to an optional feature
+that is implemented in core, but might not be supported by a given backend.
+
+in capabilities & custom_capability, capability refers to entries in the
+status_graph (after union between backend and core).
+
+The easiest way to fix this would be to fix the terminology in
+capabilities & custom_capability and their callers.
+
+=cut
+
+sub custom_capability {
+    my ( $self, $candidate, $params ) = @_;
+    foreach my $capability ( values %{$self->capabilities} ) {
+        if ( $candidate eq $capability->{method} ) {
+            my $response =
+                $self->_backend->$candidate({
+                    request    => $self,
+                    other      => $params,
+                });
+            return $self->expandTemplate($response);
+        }
+    }
+    return 0;
+}
+
+sub available_backends {
+    my ( $self ) = @_;
+    my $backend_dir = $self->_config->backend_dir;
+    my @backends = ();
+    @backends = <$backend_dir/*> if ( $backend_dir );
+    @backends = map { basename($_) } @backends;
+    return \@backends;
+}
+
+sub available_actions {
+    my ( $self ) = @_;
+    my $current_action = $self->capabilities($self->status);
+    my @available_actions = map { $self->capabilities($_) }
+        @{$current_action->{next_actions}};
+    return \@available_actions;
+}
+
+sub mark_completed {
+    my ( $self ) = @_;
+    $self->status('COMP')->store;
+    return {
+        error   => 0,
+        status  => '',
+        message => '',
+        method  => 'mark_completed',
+        stage   => 'commit',
+        next    => 'illview',
+    };
+}
+
+sub backend_confirm {
+    my ( $self, $params ) = @_;
+
+    # The backend handles setting of mandatory fields in the commit stage:
+    # - orderid
+    # - accessurl, cost (if available).
+    my $response = $self->_backend->confirm({
+            request    => $self,
+            other      => $params,
+        });
+    return $self->expandTemplate($response);
+}
+
+sub backend_update_status {
+    my ( $self, $params ) = @_;
+    return $self->expandTemplate($self->_backend->update_status($params));
+}
+
+=head3 backend_cancel
+
+    my $ILLResponse = $illRequest->backend_cancel;
+
+The standard interface method allowing for request cancellation.
+
+=cut
+
+sub backend_cancel {
+    my ( $self, $params ) = @_;
+
+    my $result = $self->_backend->cancel({
+        request => $self,
+        other => $params
+    });
+
+    return $self->expandTemplate($result);
+}
+
+=head3 backend_renew
+
+    my $renew_response = $illRequest->backend_renew;
+
+The standard interface method allowing for request renewal queries.
+
+=cut
+
+sub backend_renew {
+    my ( $self ) = @_;
+    return $self->expandTemplate(
+        $self->_backend->renew({
+            request    => $self,
+        })
+    );
+}
+
+=head3 backend_create
+
+    my $create_response = $abstractILL->backend_create($params);
+
+Return an array of Record objects created by querying our backend with
+a Search query.
+
+In the context of the other ILL methods, this is a special method: we only
+pass it $params, as it does not yet have any other data associated with it.
+
+=cut
+
+sub backend_create {
+    my ( $self, $params ) = @_;
+
+    # Establish whether we need to do a generic copyright clearance.
+    if ( ( !$params->{stage} || $params->{stage} eq 'init' )
+             && C4::Context->preference("ILLModuleCopyrightClearance") ) {
+        return {
+            error   => 0,
+            status  => '',
+            message => '',
+            method  => 'create',
+            stage   => 'copyrightclearance',
+            value   => {
+                backend => $self->_backend->name
+            }
+        };
+    } elsif ( $params->{stage} eq 'copyrightclearance' ) {
+        $params->{stage} = 'init';
+    }
+
+    # First perform API action, then...
+    my $args = {
+        request => $self,
+        other   => $params,
+    };
+    my $result = $self->_backend->create($args);
+
+    # ... simple case: we're not at 'commit' stage.
+    my $stage = $result->{stage};
+    return $self->expandTemplate($result)
+        unless ( 'commit' eq $stage );
+
+    # ... complex case: commit!
+
+    # Do we still have space for an ILL or should we queue?
+    my $permitted = $self->check_limits(
+        { patron => $self->patron }, { librarycode => $self->branchcode }
+    );
+
+    # Now augment our committed request.
+
+    $result->{permitted} = $permitted;             # Queue request?
+
+    # This involves...
+
+    # ...Updating status!
+    $self->status('QUEUED')->store unless ( $permitted );
+
+    return $self->expandTemplate($result);
+}
+
+=head3 expandTemplate
+
+    my $params = $abstract->expandTemplate($params);
+
+Return a version of $PARAMS augmented with our required template path.
+
+=cut
+
+sub expandTemplate {
+    my ( $self, $params ) = @_;
+    my $backend = $self->_backend->name;
+    # Generate path to file to load
+    my $backend_dir = $self->_config->backend_dir;
+    my $backend_tmpl = join "/", $backend_dir, $backend;
+    my $intra_tmpl =  join "/", $backend_tmpl, "intra-includes",
+        $params->{method} . ".inc";
+    my $opac_tmpl =  join "/", $backend_tmpl, "opac-includes",
+        $params->{method} . ".inc";
+    # Set files to load
+    $params->{template} = $intra_tmpl;
+    $params->{opac_template} = $opac_tmpl;
+    return $params;
+}
+
+#### Abstract Imports
+
+=head3 getLimits
+
+    my $limit_rules = $abstract->getLimits( {
+        type  => 'brw_cat' | 'branch',
+        value => $value
+    } );
+
+Return the ILL limit rules for the supplied combination of type / value.
+
+As the config may have no rules for this particular type / value combination,
+or for the default, we must define fall-back values here.
+
+=cut
+
+sub getLimits {
+    my ( $self, $params ) = @_;
+    my $limits = $self->_config->getLimitRules($params->{type});
+
+    return $limits->{$params->{value}}
+        || $limits->{default}
+        || { count => -1, method => 'active' };
+}
+
+=head3 getPrefix
+
+    my $prefix = $abstract->getPrefix( {
+        brw_cat => $brw_cat,
+        branch  => $branch_code,
+    } );
+
+Return the ILL prefix as defined by our $params: either per borrower category,
+per branch or the default.
+
+=cut
+
+sub getPrefix {
+    my ( $self, $params ) = @_;
+    my $brn_prefixes = $self->_config->getPrefixes('branch');
+    my $brw_prefixes = $self->_config->getPrefixes('brw_cat');
+
+    return $brw_prefixes->{$params->{brw_cat}}
+        || $brn_prefixes->{$params->{branch}}
+        || $brw_prefixes->{default}
+        || "";                  # "the empty prefix"
+}
+
+#### Illrequests Imports
+
+=head3 check_limits
+
+    my $ok = $illRequests->check_limits( {
+        borrower   => $borrower,
+        branchcode => 'branchcode' | undef,
+    } );
+
+Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
+see whether we are still able to place ILLs.
+
+LimitRules are derived from koha-conf.xml:
+ + default limit counts, and counting method
+ + branch specific limit counts & counting method
+ + borrower category specific limit counts & counting method
+ + err on the side of caution: a counting fail will cause fail, even if
+   the other counts passes.
+
+=cut
+
+sub check_limits {
+    my ( $self, $params ) = @_;
+    my $patron     = $params->{patron};
+    my $branchcode = $params->{librarycode} || $patron->branchcode;
+
+    # Establish maximum number of allowed requests
+    my ( $branch_rules, $brw_rules ) = (
+        $self->getLimits( {
+            type => 'branch',
+            value => $branchcode
+        } ),
+        $self->getLimits( {
+            type => 'brw_cat',
+            value => $patron->categorycode,
+        } ),
+    );
+    my ( $branch_limit, $brw_limit )
+        = ( $branch_rules->{count}, $brw_rules->{count} );
+    # Establish currently existing requests
+    my ( $branch_count, $brw_count ) = (
+        $self->_limit_counter(
+            $branch_rules->{method}, { branchcode => $branchcode }
+        ),
+        $self->_limit_counter(
+            $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
+        ),
+    );
+
+    # Compare and return
+    # A limit of -1 means no limit exists.
+    # We return blocked if either branch limit or brw limit is reached.
+    if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
+             || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
+        return 0;
+    } else {
+        return 1;
+    }
+}
+
+sub _limit_counter {
+    my ( $self, $method, $target ) = @_;
+
+    # Establish parameters of counts
+    my $resultset;
+    if ($method && $method eq 'annual') {
+        $resultset = Koha::Illrequests->search({
+            -and => [
+                %{$target},
+                \"YEAR(placed) = YEAR(NOW())"
+            ]
+        });
+    } else {                    # assume 'active'
+        # XXX: This status list is ugly. There should be a method in config
+        # to return these.
+        $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
+        $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
+    }
+
+    # Fetch counts
+    return $resultset->count;
+}
+
+=head3 requires_moderation
+
+    my $status = $illRequest->requires_moderation;
+
+Return the name of the status if moderation by staff is required; or 0
+otherwise.
+
+=cut
+
+sub requires_moderation {
+    my ( $self ) = @_;
+    my $require_moderation = {
+        'CANCREQ' => 'CANCREQ',
+    };
+    return $require_moderation->{$self->status};
+}
+
+=head3 generic_confirm
+
+    my $stage_summary = $illRequest->generic_confirm;
+
+Handle the generic_confirm extended method.  The first stage involves creating
+a template email for the end user to edit in the browser.  The second stage
+attempts to submit the email.
+
+=cut
+
+sub generic_confirm {
+    my ( $self, $params ) = @_;
+    my $branch = Koha::Libraries->find($params->{current_branchcode})
+        || die "Invalid current branchcode. Are you logged in as the database user?";
+    if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
+        my $draft->{subject} = "ILL Request";
+        $draft->{body} = <<EOF;
+Dear Sir/Madam,
+
+    We would like to request an interlibrary loan for a title matching the
+following description:
+
+EOF
+
+        my $details = $self->metadata;
+        while (my ($title, $value) = each %{$details}) {
+            $draft->{body} .= "  - " . $title . ": " . $value . "\n"
+                if $value;
+        }
+        $draft->{body} .= <<EOF;
+
+Please let us know if you are able to supply this to us.
+
+Kind Regards
+
+EOF
+
+        my @address = map { $branch->$_ }
+            qw/ branchname branchaddress1 branchaddress2 branchaddress3
+                branchzip branchcity branchstate branchcountry branchphone
+                branchemail /;
+        my $address = "";
+        foreach my $line ( @address ) {
+            $address .= $line . "\n" if $line;
+        }
+
+        $draft->{body} .= $address;
+
+        my $partners = Koha::Patrons->search({
+            categorycode => $self->_config->partner_code
+        });
+        return {
+            error   => 0,
+            status  => '',
+            message => '',
+            method  => 'generic_confirm',
+            stage   => 'draft',
+            value   => {
+                draft    => $draft,
+                partners => $partners,
+            }
+        };
+
+    } elsif ( 'draft' eq $params->{stage} ) {
+        # Create the to header
+        my $to = $params->{partners};
+        $to =~ s/^\x00//;       # Strip leading NULLs
+        $to =~ s/\x00/; /;      # Replace others with '; '
+        die "No target email addresses found. Either select at least one partner or check your ILL partner library records." if ( !$to );
+        # Create the from, replyto and sender headers
+        my $from = $branch->branchemail;
+        my $replyto = $branch->branchreplyto || $from;
+        die "Your branch has no email address. Please set it."
+            if ( !$from );
+        # Create the email
+        my $message = Koha::Email->new;
+        my %mail = $message->create_message_headers(
+            {
+                to          => $to,
+                from        => $from,
+                replyto     => $replyto,
+                subject     => Encode::encode( "utf8", $params->{subject} ),
+                message     => Encode::encode( "utf8", $params->{body} ),
+                contenttype => 'text/plain',
+            }
+        );
+        # Send it
+        my $result = sendmail(%mail);
+        if ( $result ) {
+            $self->status("GENREQ")->store;
+            return {
+                error   => 0,
+                status  => '',
+                message => '',
+                method  => 'generic_confirm',
+                stage   => 'commit',
+                next    => 'illview',
+            };
+        } else {
+            return {
+                error   => 1,
+                status  => 'email_failed',
+                message => $Mail::Sendmail::error,
+                method  => 'generic_confirm',
+                stage   => 'draft',
+            };
+        }
+    } else {
+        die "Unknown stage, should not have happened."
+    }
+}
+
+=head3 id_prefix
+
+    my $prefix = $record->id_prefix;
+
+Return the prefix appropriate for the current Illrequest as derived from the
+borrower and branch associated with this request's Status, and the config
+file.
+
+=cut
+
+sub id_prefix {
+    my ( $self ) = @_;
+    my $brw = $self->patron;
+    my $brw_cat = "dummy";
+    $brw_cat = $brw->categorycode
+        unless ( 'HASH' eq ref($brw) && $brw->{deleted} );
+    my $prefix = $self->getPrefix( {
+        brw_cat => $brw_cat,
+        branch  => $self->branchcode,
+    } );
+    $prefix .= "-" if ( $prefix );
+    return $prefix;
+}
+
+=head3 _censor
+
+    my $params = $illRequest->_censor($params);
+
+Return $params, modified to reflect our censorship requirements.
+
+=cut
+
+sub _censor {
+    my ( $self, $params ) = @_;
+    my $censorship = $self->_config->censorship;
+    $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
+        if ( $params->{opac} );
+    $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
+
+    return $params;
+}
+
+=head1 AUTHOR
+
+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
+
+=cut
+
+1;
diff --git a/Koha/Illrequest/Config.pm b/Koha/Illrequest/Config.pm
new file mode 100644 (file)
index 0000000..3bc78d1
--- /dev/null
@@ -0,0 +1,384 @@
+package Koha::Illrequest::Config;
+
+# Copyright 2013,2014 PTFS Europe Ltd
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use Modern::Perl;
+use C4::Context;
+
+=head1 NAME
+
+Koha::Illrequest::Config - Koha ILL Configuration Object
+
+=head1 SYNOPSIS
+
+Object-oriented class that giving access to the illconfig data derived
+from ill/config.yaml.
+
+=head1 DESCRIPTION
+
+Config object providing abstract representation of the expected XML
+returned by ILL API.
+
+In particular the config object uses a YAML file, whose path is
+defined by <illconfig> in koha-conf.xml. That YAML file provides the
+data structure exposed in this object.
+
+By default the configured data structure complies with fields used by
+the British Library Interlibrary Loan DSS API.
+
+The config file also provides mappings for Record Object accessors.
+
+=head1 API
+
+=head2 Class Methods
+
+=head3 new
+
+    my $config = Koha::Illrequest::Config->new();
+
+Create a new Koha::Illrequest::Config object, with mapping data loaded from the
+ILL configuration file.
+
+=cut
+
+sub new {
+    my ( $class ) = @_;
+    my $self  = {};
+
+    $self->{configuration} = _load_configuration(
+        C4::Context->config("interlibrary_loans"),
+        C4::Context->preference("UnmediatedILL")
+      );
+
+    bless $self, $class;
+
+    return $self;
+}
+
+=head3 backend
+
+    $backend = $config->backend($name);
+    $backend = $config->backend;
+
+Standard setter/accessor for our backend.
+
+=cut
+
+sub backend {
+    my ( $self, $new ) = @_;
+    $self->{configuration}->{backend} = $new if $new;
+    return $self->{configuration}->{backend};
+}
+
+=head3 backend_dir
+
+    $backend_dir = $config->backend_dir($new_path);
+    $backend_dir = $config->backend_dir;
+
+Standard setter/accessor for our backend_directory.
+
+=cut
+
+sub backend_dir {
+    my ( $self, $new ) = @_;
+    $self->{configuration}->{backend_directory} = $new if $new;
+    return $self->{configuration}->{backend_directory};
+}
+
+=head3 partner_code
+
+    $partner_code = $config->partner_code($new_code);
+    $partner_code = $config->partner_code;
+
+Standard setter/accessor for our partner_code.
+
+=cut
+
+sub partner_code {
+    my ( $self, $new ) = @_;
+    $self->{configuration}->{partner_code} = $new if $new;
+    return $self->{configuration}->{partner_code};
+}
+
+=head3 limits
+
+    $limits = $config->limits($limitshash);
+    $limits = $config->limits;
+
+Standard setter/accessor for our limits.  No parsing is performed on
+$LIMITSHASH, so caution should be exercised when using this setter.
+
+=cut
+
+sub limits {
+    my ( $self, $new ) = @_;
+    $self->{configuration}->{limits} = $new if $new;
+    return $self->{configuration}->{limits};
+}
+
+=head3 getPrefixes
+
+    my $prefixes = $config->getPrefixes('brw_cat' | 'branch');
+
+Return the prefix for ILLs defined by our config.
+
+=cut
+
+sub getPrefixes {
+    my ( $self, $type ) = @_;
+    die "Unexpected type." unless ( $type eq 'brw_cat' || $type eq 'branch' );
+    my $values = $self->{configuration}->{prefixes}->{$type};
+    $values->{default} = $self->{configuration}->{prefixes}->{default};
+    return $values;
+}
+
+=head3 getLimitRules
+
+    my $rules = $config->getLimitRules('brw_cat' | 'branch')
+
+Return the hash of ILL limit rules defined by our config.
+
+=cut
+
+sub getLimitRules {
+    my ( $self, $type ) = @_;
+    die "Unexpected type." unless ( $type eq 'brw_cat' || $type eq 'branch' );
+    my $values = $self->{configuration}->{limits}->{$type};
+    $values->{default} = $self->{configuration}->{limits}->{default};
+    return $values;
+}
+
+=head3 getDigitalRecipients
+
+    my $recipient_rules= $config->getDigitalRecipients('brw_cat' | 'branch');
+
+Return the hash of digital_recipient settings defined by our config.
+
+=cut
+
+sub getDigitalRecipients {
+    my ( $self, $type ) = @_;
+    die "Unexpected type." unless ( $type eq 'brw_cat' || $type eq 'branch' );
+    my $values = $self->{configuration}->{digital_recipients}->{$type};
+    $values->{default} =
+        $self->{configuration}->{digital_recipients}->{default};
+    return $values;
+}
+
+=head3 censorship
+
+    my $censoredValues = $config->censorship($hash);
+    my $censoredValues = $config->censorship;
+
+Standard setter/accessor for our limits.  No parsing is performed on $HASH, so
+caution should be exercised when using this setter.
+
+Return our censorship values for the OPAC as loaded from the koha-conf.xml, or
+the fallback value (no censorship).
+
+=cut
+
+sub censorship {
+    my ( $self, $new ) = @_;
+    $self->{configuration}->{censorship} = $new if $new;
+    return $self->{configuration}->{censorship};
+}
+
+=head3 _load_configuration
+
+    my $configuration = $config->_load_configuration($config_from_xml);
+
+Read the configuration values passed as the parameter, and populate a hashref
+suitable for use with these.
+
+A key task performed here is the parsing of the input in the configuration
+file to ensure we have only valid input there.
+
+=cut
+
+sub _load_configuration {
+    my ( $xml_config, $unmediated ) = @_;
+    my $xml_backend_dir = $xml_config->{backend_directory};
+
+    # Default data structure to be returned
+    my $configuration = {
+        backend_directory  => $xml_backend_dir,
+        censorship         => {
+            censor_notes_staff => 0,
+            censor_reply_date => 0,
+        },
+        limits             => {},
+        digital_recipients => {},
+        prefixes           => {},
+        partner_code       => 'ILLLIBS',
+        raw_config         => $xml_config,
+    };
+
+    # Per Branch Configuration
+    my $branches = $xml_config->{branch};
+    if ( ref($branches) eq "ARRAY" ) {
+        # Multiple branch overrides defined
+        map {
+            _load_unit_config({
+                unit   => $_,
+                id     => $_->{code},
+                config => $configuration,
+                type   => 'branch'
+            })
+        } @{$branches};
+    } elsif ( ref($branches) eq "HASH" ) {
+        # Single branch override defined
+        _load_unit_config({
+            unit   => $branches,
+            id     => $branches->{code},
+            config => $configuration,
+            type   => 'branch'
+        });
+    }
+
+    # Per Borrower Category Configuration
+    my $brw_cats = $xml_config->{borrower_category};
+    if ( ref($brw_cats) eq "ARRAY" ) {
+        # Multiple borrower category overrides defined
+        map {
+            _load_unit_config({
+                unit   => $_,
+                id     => $_->{code},
+                config => $configuration,
+                type   => 'brw_cat'
+            })
+        } @{$brw_cats};
+    } elsif ( ref($brw_cats) eq "HASH" ) {
+        # Single branch override defined
+        _load_unit_config({
+            unit   => $brw_cats,
+            id     => $brw_cats->{code},
+            config => $configuration,
+            type   => 'brw_cat'
+        });
+    }
+
+    # Default Configuration
+    _load_unit_config({
+        unit   => $xml_config,
+        id     => 'default',
+        config => $configuration
+    });
+
+    # Censorship
+    my $staff_comments = $xml_config->{staff_request_comments} || 0;
+    $configuration->{censorship}->{censor_notes_staff} = 1
+        if ( $staff_comments && 'hide' eq $staff_comments );
+    my $reply_date = $xml_config->{reply_date} || 0;
+    $configuration->{censorship}->{censor_reply_date} = 1
+        if ( $reply_date && 'hide' eq $reply_date );
+
+    # ILL Partners
+    $configuration->{partner_code} = $xml_config->{partner_code} || 'ILLLIBS';
+
+    die "No DEFAULT_FORMATS has been defined in koha-conf.xml, but UNMEDIATEDILL is active."
+        if ( $unmediated && !$configuration->{default_formats}->{default} );
+
+    return $configuration;
+}
+
+=head3 _load_unit_config
+
+    my $configuration->{part} = _load_unit_config($params);
+
+$PARAMS is a hashref with the following elements:
+- unit: the part of the configuration we are parsing.
+- id: the name within which we will store the parsed unit in config.
+- config: the configuration we are augmenting.
+- type: the type of config unit we are parsing.  Assumed to be 'default'.
+
+Read `unit', and augment `config' with these under `id'.
+
+This is a helper for _load_configuration.
+
+A key task performed here is the parsing of the input in the configuration
+file to ensure we have only valid input there.
+
+=cut
+
+sub _load_unit_config {
+    my ( $params ) = @_;
+    my $unit = $params->{unit};
+    my $id = $params->{id};
+    my $config = $params->{config};
+    my $type = $params->{type};
+    die "TYPE should be either 'branch' or 'brw_cat' if ID is not 'default'."
+        if ( $id ne 'default' && ( $type ne 'branch' && $type ne 'brw_cat') );
+    return $config unless $id;
+
+    if ( $unit->{api_key} && $unit->{api_auth} ) {
+        $config->{credentials}->{api_keys}->{$id} = {
+            api_key  => $unit->{api_key},
+            api_auth => $unit->{api_auth},
+        };
+    }
+    # Add request_limit rules.
+    # METHOD := 'annual' || 'active'
+    # COUNT  := x >= -1
+    if ( ref $unit->{request_limit} eq 'HASH' ) {
+        my $method  = $unit->{request_limit}->{method};
+        my $count = $unit->{request_limit}->{count};
+        if ( 'default' eq $id ) {
+            $config->{limits}->{$id}->{method}  = $method
+                if ( $method && ( 'annual' eq $method || 'active' eq $method ) );
+            $config->{limits}->{$id}->{count} = $count
+                if ( $count && ( -1 <= $count ) );
+        } else {
+            $config->{limits}->{$type}->{$id}->{method}  = $method
+                if ( $method && ( 'annual' eq $method || 'active' eq $method ) );
+            $config->{limits}->{$type}->{$id}->{count} = $count
+                if ( $count && ( -1 <= $count ) );
+        }
+    }
+
+    # Add prefix rules.
+    # PREFIX := string
+    if ( $unit->{prefix} ) {
+        if ( 'default' eq $id ) {
+            $config->{prefixes}->{$id} = $unit->{prefix};
+        } else {
+            $config->{prefixes}->{$type}->{$id} = $unit->{prefix};
+        }
+    }
+
+    # Add digital_recipient rules.
+    # DIGITAL_RECIPIENT := borrower || branch (defaults to borrower)
+    if ( $unit->{digital_recipient} ) {
+        if ( 'default' eq $id ) {
+            $config->{digital_recipients}->{$id} = $unit->{digital_recipient};
+        } else {
+            $config->{digital_recipients}->{$type}->{$id} =
+                $unit->{digital_recipient};
+        }
+    }
+
+    return $config;
+}
+
+=head1 AUTHOR
+
+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
+
+=cut
+
+1;
diff --git a/Koha/Illrequestattribute.pm b/Koha/Illrequestattribute.pm
new file mode 100644 (file)
index 0000000..16bc086
--- /dev/null
@@ -0,0 +1,51 @@
+package Koha::Illrequestattribute;
+
+# Copyright PTFS Europe 2016
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# Koha; if not, write to the Free Software Foundation, Inc., 51 Franklin
+# Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use Modern::Perl;
+
+use Koha::Database;
+
+use base qw(Koha::Object);
+
+=head1 NAME
+
+Koha::Illrequestattribute - Koha Illrequestattribute Object class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head3 type
+
+=cut
+
+sub _type {
+    return 'Illrequestattribute';
+}
+
+=head1 AUTHOR
+
+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
+
+=cut
+
+1;
diff --git a/Koha/Illrequestattributes.pm b/Koha/Illrequestattributes.pm
new file mode 100644 (file)
index 0000000..e05fa6e
--- /dev/null
@@ -0,0 +1,55 @@
+package Koha::Illrequestattributes;
+
+# Copyright PTFS Europe 2016
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use Modern::Perl;
+
+use Koha::Database;
+use Koha::Illrequestattribute;
+
+use base qw(Koha::Objects);
+
+=head1 NAME
+
+Koha::Illrequestattributes - Koha Illrequestattributes Object class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head3 type
+
+=cut
+
+sub _type {
+    return 'Illrequestattribute';
+}
+
+sub object_class {
+    return 'Koha::Illrequestattribute';
+}
+
+=head1 AUTHOR
+
+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
+
+=cut
+
+1;
diff --git a/Koha/Illrequests.pm b/Koha/Illrequests.pm
new file mode 100644 (file)
index 0000000..85cc711
--- /dev/null
@@ -0,0 +1,97 @@
+package Koha::Illrequests;
+
+# Copyright PTFS Europe 2016
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use Modern::Perl;
+
+use Koha::Database;
+use Koha::Illrequest;
+use Koha::Illrequest::Config;
+
+use base qw(Koha::Objects);
+
+=head1 NAME
+
+Koha::Illrequests - Koha Illrequests Object class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head3 type
+
+=cut
+
+sub _type {
+    return 'Illrequest';
+}
+
+sub object_class {
+    return 'Koha::Illrequest';
+}
+
+##### To be implemented Facade
+
+=head3 new
+
+    my $illRequests = Koha::Illrequests->new();
+
+Create an ILLREQUESTS object, a singleton through which we can interact with
+ILLREQUEST objects stored in the database or search for ILL candidates at API
+backends.
+
+=cut
+
+sub new {
+    my ( $class, $attributes ) = @_;
+
+    my $self = $class->SUPER::new($class, $attributes);
+
+    my $config = Koha::Illrequest::Config->new; # <- Necessary
+    $self->{_config} = $config;                 # <- Necessary
+
+    return $self;
+}
+
+=head3 search_incomplete
+
+    my $requests = $illRequests->search_incomplete;
+
+A specialised version of `search`, returning all requests currently
+not considered completed.
+
+=cut
+
+sub search_incomplete {
+    my ( $self ) = @_;
+    $self->search( {
+        status => [
+            -and => { '!=', 'COMP' }, { '!=', 'GENCOMP' }
+        ]
+    } );
+}
+
+=head1 AUTHOR
+
+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
+
+=cut
+
+1;
diff --git a/Koha/REST/V1/Illrequests.pm b/Koha/REST/V1/Illrequests.pm
new file mode 100644 (file)
index 0000000..0807eb1
--- /dev/null
@@ -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;
index af6ea2d..ada4224 100644 (file)
@@ -312,6 +312,7 @@ my $target_map = {
   './etc/zebradb'               => { target => 'ZEBRA_CONF_DIR', trimdir => -1 },
   './etc/pazpar2'               => { target => 'PAZPAR2_CONF_DIR', trimdir => -1 },
   './help.pl'                   => 'INTRANET_CGI_DIR',
+  './ill'                       => 'INTRANET_CGI_DIR',
   './installer-CPAN.pl'         => 'NONE',
   './installer'                 => 'INTRANET_CGI_DIR',
   './errors'                    => {target => 'INTRANET_CGI_DIR'},
index 147e985..9c3a6d2 100644 (file)
@@ -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 (file)
index 0000000..ddafd80
--- /dev/null
@@ -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"
+                }
+            }
+        }
+    }
+}
index 4ccee3a..067e978 100644 (file)
@@ -153,5 +153,26 @@ __PAZPAR2_TOGGLE_XML_POST__
  <plack_max_requests>50</plack_max_requests>
  <plack_workers>2</plack_workers>
 
+ <interlibrary_loans>
+     <!-- Path to where Illbackends are located on the system
+          - This setting should normally not be touched -->
+     <backend_directory>__PERL_MODULE_DIR__/Koha/Illbackends</backend_directory>
+     <!-- How should we treat staff comments?
+          - hide: don't show in OPAC
+          - show: show in OPAC -->
+     <staff_request_comments>hide</staff_request_comments>
+     <!-- How should we treat the reply_date field?
+          - hide: don't show this field in the UI
+          - any other string: show, with this label -->
+     <reply_date>hide</reply_date>
+     <!-- Where should digital ILLs be sent?
+          - borrower: send it straight to the borrower email
+          - branch: send the ILL to the branch email -->
+     <digital_recipient>branch</digital_recipient>
+     <!-- What patron category should we use for p2p ILL requests?
+          - By default this is set to 'ILLLIBS' -->
+     <partner_code>ILLLIBS</partner_code>
+ </interlibrary_loans>
+
 </config>
 </yazgfs>
diff --git a/ill/ill-requests.pl b/ill/ill-requests.pl
new file mode 100755 (executable)
index 0000000..26cb350
--- /dev/null
@@ -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');
+}
index cf90142..020d6e7 100644 (file)
@@ -3049,3 +3049,95 @@ fieldset.rows + fieldset.action {
 #patron_search #filters {
     display: none;
 }
+
+#interlibraryloans h1 {
+    margin: 1em 0;
+}
+
+#interlibraryloans h2 {
+    margin-bottom: 20px;
+}
+
+#interlibraryloans h3 {
+    margin-top: 20px;
+}
+
+#interlibraryloans .bg-info {
+    overflow: auto;
+    position: relative;
+}
+
+#interlibraryloans #search-summary {
+    -webkit-transform: translateY(-50%);
+    -ms-transform: translateY(-50%);
+    -o-transform: translateY(-50%);
+    transform: translateY(-50%);
+    position: absolute;
+    top: 50%;
+}
+
+#interlibraryloans .format h5 {
+    margin-top: 20px;
+}
+
+#interlibraryloans .format li {
+    list-style: none;
+}
+
+#interlibraryloans .format h4 {
+    margin-bottom: 20px;
+}
+
+#interlibraryloans .format input {
+    margin: 10px 0;
+}
+
+#interlibraryloans #freeform-fields .custom-name {
+    width: 9em;
+    margin-right: 1em;
+    text-align: right;
+}
+
+#interlibraryloans #freeform-fields .delete-new-field {
+    margin-left: 1em;
+}
+
+#interlibraryloans #add-new-fields {
+    margin: 1em;
+}
+
+#interlibraryloans #column-toggle,
+#interlibraryloans #reset-toggle {
+    margin: 15px 0;
+    line-height: 1.5em;
+    font-weight: 700;
+}
+
+#ill-view-panel {
+    margin-top: 15px;
+}
+
+#ill-view-panel h3 {
+    margin-bottom: 10px;
+}
+
+#ill-view-panel h4 {
+    margin-bottom: 20px;
+}
+
+#ill-view-panel .rows div {
+    height: 1em;
+    margin-bottom: 1em;
+}
+
+#ill-view-panel #requestattributes .label {
+    width: auto;
+}
+
+table#ill-requests {
+    width: 100% !important;
+}
+
+table#ill-requests th {
+    text-transform: capitalize;
+}
index d34ff9f..de75551 100644 (file)
     [% IF Koha.Preference('HouseboundModule') %]
         [% IF houseboundview %]<li class="active">[% ELSE %]<li>[% END %]<a href="/cgi-bin/koha/members/housebound.pl?borrowernumber=[% borrowernumber %]">Housebound</a></li>
     [% END %]
+    [% IF Koha.Preference('ILLModule') %]
+        <li><a href="/cgi-bin/koha/ill/ill-requests.pl?borrowernumber=[% borrowernumber %]">Interlibrary loans</a></li>
+    [% END %]
 </ul></div>
 [% END %]
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 (file)
index 0000000..3f541d9
--- /dev/null
@@ -0,0 +1,24 @@
+[% USE Koha %]
+[% IF Koha.Preference('ILLModule ') %]
+    <div id="toolbar" class="btn-toolbar">
+        [% IF backends.size > 1 %]
+            <div class="dropdown btn-group">
+                <button class="btn btn-sm btn-default dropdown-toggle" type="button" id="ill-backend-dropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
+                    <i class="fa fa-plus"></i> New ILL request <span class="caret"></span>
+                </button>
+                <ul class="dropdown-menu" aria-labelledby="ill-backend-dropdown">
+                    [% FOREACH backend IN backends %]
+                        <li><a href="/cgi-bin/koha/ill/ill-requests.pl?method=create&amp;backend=[% backend %]">[% backend %]</a></li>
+                    [% END %]
+                </ul>
+            </div>
+        [% ELSE %]
+            <a id="ill-new" class="btn btn-sm btn-default" href="/cgi-bin/koha/ill/ill-requests.pl?method=create&amp;backend=[% backends.0 %]">
+                <i class="fa fa-plus"></i> New ILL request
+            </a>
+        [% END %]
+        <a id="ill-list" class="btn btn-sm btn-default btn-group" href="/cgi-bin/koha/ill/ill-requests.pl">
+            <i class="fa fa-list"></i> List requests
+        </a>
+    </div>
+[% END %]
index 7c6275a..4a1ce81 100644 (file)
@@ -20,6 +20,7 @@
     [%- CASE 'plugins' -%]<span>Koha plugins</span>
     [%- CASE 'lists' -%]<span>Lists</span>
     [%- CASE 'clubs' -%]<span>Patron clubs</span>
+    [%- CASE 'ill' -%]<span>Create and modify Interlibrary loan requests</span>
     [%- END -%]
 [%- END -%]
 
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 (file)
index 0000000..c013727
--- /dev/null
@@ -0,0 +1,698 @@
+[% USE Branches %]
+[% USE Koha %]
+
+[% INCLUDE 'doc-head-open.inc' %]
+<title>Koha &rsaquo; ILL requests  &rsaquo;</title>
+[% INCLUDE 'doc-head-close.inc' %]
+<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.tablesorter.min.js"></script>
+<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.checkboxes.min.js"></script>
+<link rel="stylesheet" type="text/css" href="[% interface %]/[% theme %]/css/datatables.css">
+[% INCLUDE 'datatables.inc' %]
+<script type="text/javascript">
+    //<![CDATA[
+    $(document).ready(function() {
+
+        // Illview Datatable setup
+
+        // Fields we don't want to display
+        var ignore = [
+            'accessurl',
+            'backend',
+            'completed',
+            'branch',
+            'capabilities',
+            'cost',
+            'medium',
+            'notesopac',
+            'notesstaff',
+            'placed',
+            'replied'
+        ];
+
+        // Fields we need to expand (flatten)
+        var expand = [
+            'metadata',
+            'patron'
+        ];
+
+        // Expanded fields
+        // This is auto populated
+        var expanded = {};
+
+        // The core fields that should be displayed first
+        var core = [
+            'metadata_Author',
+            'metadata_Title',
+            'borrowername',
+            'biblio_id',
+            'branchcode',
+            'status',
+            'updated',
+            'illrequest_id',
+            'action'
+        ];
+
+        // Extra fields that we need to tack on to the end
+        var extra = [ 'action' ];
+
+        // Remove any fields we're ignoring
+        var removeIgnore = function(dataObj) {
+            dataObj.forEach(function(thisRow) {
+                ignore.forEach(function(thisIgnore) {
+                    if (thisRow.hasOwnProperty(thisIgnore)) {
+                        delete thisRow[thisIgnore];
+                    }
+                });
+            });
+        };
+
+        // Expand any fields we're expanding
+        var expandExpand = function(row) {
+            expand.forEach(function(thisExpand) {
+                if (row.hasOwnProperty(thisExpand)) {
+                    if (!expanded.hasOwnProperty(thisExpand)) {
+                        expanded[thisExpand] = [];
+                    }
+                    var expandObj = row[thisExpand];
+                    Object.keys(expandObj).forEach(
+                        function(thisExpandCol) {
+                            var expColName = thisExpand + '_' + thisExpandCol;
+                            // Keep a list of fields that have been expanded
+                            // so we can create toggle links for them
+                            if (expanded[thisExpand].indexOf(expColName) == -1) {
+                                expanded[thisExpand].push(expColName);
+                            }
+                            expandObj[expColName] =
+                                expandObj[thisExpandCol];
+                            delete expandObj[thisExpandCol];
+                        }
+                    );
+                    $.extend(true, row, expandObj);
+                    delete row[thisExpand];
+                }
+            });
+        };
+
+        // Build a de-duped list of all column names
+        var allCols = {};
+        core.map(function(thisCore) {
+            allCols[thisCore] = 1;
+        });
+        var unionColumns = function(row) {
+            Object.keys(row).forEach(function(col) {
+                if (ignore.indexOf(col) == -1) {
+                    allCols[col] = 1;
+                }
+            });
+        };
+
+        // Some rows may not have fields that other rows have,
+        // so make sure all rows have the same fields
+        var fillMissing = function(row) {
+            Object.keys(allCols).forEach(function(thisCol) {
+                row[thisCol] = (!row.hasOwnProperty(thisCol)) ?
+                    null :
+                    row[thisCol];
+            });
+        }
+
+        // Strip the expand prefix if it exists, we do this for display
+        var stripPrefix = function(value) {
+            expand.forEach(function(thisExpand) {
+                var regex = new RegExp(thisExpand + '_', 'g');
+                value = value.replace(regex, '');
+            });
+            return value;
+        };
+
+        // Our 'render' function for borrowerlink
+        var createBorrowerLink = function(data, type, row) {
+            return '<a title="View borrower details" ' +
+                'href="/cgi-bin/koha/members/moremember.pl?' +
+                'borrowernumber='+row.borrowernumber+'">' +
+                row.patron_firstname + ' ' + row.patron_surname +
+                '</a>';
+        };
+
+        // Render function for request ID
+        var createRequestId = function(data, type, row) {
+            return row.id_prefix + row.illrequest_id;
+        };
+
+        // Render function for request status
+        var createStatus = function(data, type, row, meta) {
+            var origData = meta.settings.oInit.originalData;
+            if (origData.length > 0) {
+                return meta.settings.oInit.originalData[0].capabilities[
+                    row.status
+                ].name;
+            } else {
+                return '';
+            }
+        };
+
+        // Render function for creating a row's action link
+        var createActionLink = function(data, type, row) {
+            return '<a class="btn btn-default btn-sm" ' +
+                'href="/cgi-bin/koha/ill/ill-requests.pl?' +
+                'method=illview&amp;illrequest_id=' +
+                row.illrequest_id +
+                '">Manage request</a>' +
+                '</div>'
+        };
+
+        // Columns that require special treatment
+        var specialCols = {
+            action: {
+                name: '',
+                func: createActionLink
+            },
+            borrowername: {
+                name: 'Borrower',
+                func: createBorrowerLink
+            },
+            illrequest_id: {
+                name: 'Request number',
+                func: createRequestId
+            },
+            status: {
+                name: 'Status',
+                func: createStatus
+            },
+            biblio_id: {
+                name: 'Biblio number'
+            },
+            branchcode: {
+                name: 'Branch code'
+            }
+        };
+
+        // Helper for handling prefilter column names
+        function toColumnName(myVal) {
+            return myVal
+                .replace(/^filter/, '')
+                .replace(/([A-Z])/g, "_$1")
+                .replace(/^_/,'').toLowerCase();
+        };
+
+        // Toggle request attributes in Illview
+        $('#toggle_requestattributes').click(function() {
+            $('#requestattributes').toggleClass('content_hidden');
+        });
+
+        // Filter partner list
+        $('#partner_filter').keyup(function() {
+            var needle = $('#partner_filter').val();
+            $('#partners > option').each(function() {
+                var regex = new RegExp(needle, 'i');
+                if (
+                    needle.length == 0 ||
+                    $(this).is(':selected') ||
+                    $(this).text().match(regex)
+                ) {
+                    $(this).show();
+                } else {
+                    $(this).hide();
+                }
+            });
+        });
+
+        // Get our data from the API and process it prior to passing
+        // it to datatables
+        var ajax = $.ajax(
+            '/api/v1/illrequests?embed=metadata,patron,capabilities,branch'
+            ).done(function() {
+                var data = JSON.parse(ajax.responseText);
+                // Make a copy, we'll be removing columns next and need
+                // to be able to refer to data that has been removed
+                var dataCopy = $.extend(true, [], data);
+                // Remove all columns we're not interested in
+                removeIgnore(dataCopy);
+                // Expand columns that need it and create an array
+                // of all column names
+                $.each(dataCopy, function(k, row) {
+                    expandExpand(row);
+                    unionColumns(row);
+                });
+                // Append any extra columns we need to tag on
+                if (extra.length > 0) {
+                    extra.forEach(function(thisExtra) {
+                        allCols[thisExtra] = 1;
+                    });
+                };
+                // Different requests will have different columns,
+                // make sure they all have the same
+                $.each(dataCopy, function(k, row) {
+                    fillMissing(row);
+                });
+
+                // Assemble an array of column definitions for passing
+                // to datatables
+                var colData = [];
+                Object.keys(allCols).forEach(function(thisCol) {
+                    // We may have defined a pretty name for this column
+                    var colName = (
+                        specialCols.hasOwnProperty(thisCol) &&
+                        specialCols[thisCol].hasOwnProperty('name')
+                    ) ?
+                        specialCols[thisCol].name :
+                        thisCol;
+                    // Create the table header for this column
+                    var str = '<th>' + stripPrefix(colName) + '</th>';
+                    $(str).appendTo('#illview-header');
+                    // Create the base column object
+                    var colObj = {
+                        name: thisCol,
+                        className: thisCol
+                    };
+                    // We may need to process the data going in this
+                    // column, so do it if necessary
+                    if (
+                        specialCols.hasOwnProperty(thisCol) &&
+                        specialCols[thisCol].hasOwnProperty('func')
+                    ) {
+                        colObj.render = specialCols[thisCol].func;
+                    } else {
+                        colObj.data = thisCol
+                    }
+                    colData.push(colObj);
+                });
+
+                // Create the toggle links for all metadata fields
+                var links = [];
+                expanded.metadata.forEach(function(thisExpanded) {
+                    if (core.indexOf(thisExpanded) == -1) {
+                        links.push(
+                            '<a href="#" class="toggle-vis" data-column="' +
+                            thisExpanded + '">' + stripPrefix(thisExpanded) +
+                            '</a>'
+                        );
+                    }
+                });
+                $('#column-toggle').append(links.join(' | '));
+
+                // Initialise the datatable
+                var myTable = $('#ill-requests').DataTable($.extend(true, {}, dataTablesDefaults, {
+                    aoColumnDefs: [  // Last column shouldn't be sortable or searchable
+                        {
+                            aTargets: [ 'action' ],
+                            bSortable: false,
+                            bSearchable: false
+                        },
+                    ],
+                    aaSorting: [[ 6, 'desc' ]], // Default sort, updated descending
+                    processing: true, // Display a message when manipulating
+                    language: {
+                        loadingRecords: "Please wait - loading requests...",
+                        zeroRecords: "No requests were found"
+                    },
+                    iDisplayLength: 10, // 10 results per page
+                    sPaginationType: "full_numbers", // Pagination display
+                    deferRender: true, // Improve performance on big datasets
+                    data: dataCopy,
+                    columns: colData,
+                    originalData: data // Enable render functions to access
+                                       // our original data
+                }));
+
+                // Reset columns to default
+                var resetColumns = function() {
+                    Object.keys(allCols).forEach(function(thisCol) {
+                        myTable.column(thisCol + ':name').visible(core.indexOf(thisCol) != -1);
+                    });
+                    myTable.columns.adjust().draw(false);
+                };
+
+                // Handle the click event on a toggle link
+                $('a.toggle-vis').on('click', function(e) {
+                    e.preventDefault();
+                    var column = myTable.column(
+                        $(this).data('column') + ':name'
+                    );
+                    column.visible(!column.visible());
+                });
+
+                // Reset column toggling
+                $('#reset-toggle').click(function() {
+                    resetColumns();
+                });
+
+                // Handle a prefilter request and do the prefiltering
+                var filters = $('#ill-requests').data();
+                if (typeof filters !== 'undefined') {
+                    var filterNames = Object.keys(filters).filter(
+                        function(thisData) {
+                            return thisData.match(/^filter/);
+                        }
+                    );
+                    filterNames.forEach(function(thisFilter) {
+                        var filterName = toColumnName(thisFilter) + ':name';
+                        var regex = '^'+filters[thisFilter]+'$';
+                        console.log(regex);
+                        myTable.columns(filterName).search(regex, true, false);
+                    });
+                    myTable.draw();
+                }
+
+                // Initialise column hiding
+                resetColumns();
+
+            }
+        );
+
+    });
+    //]]>
+</script>
+</head>
+
+<body id="acq_suggestion" class="acq">
+[% INCLUDE 'header.inc' %]
+[% INCLUDE 'cat-search.inc' %]
+
+<div id="breadcrumbs">
+    <a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo;
+    [% IF query_type == 'create' %]
+        <a href=[% parent %]>ILL requests</a> &rsaquo; New request
+    [% ELSIF query_type == 'status' %]
+        <a href=[% parent %]>ILL requests</a> &rsaquo; Status
+    [% ELSE %]
+        ILL requests
+    [% END %]
+</div>
+
+<div id="doc3" class="yui-t2">
+    <div id="bd">
+        <div id="yui-main">
+            <div id="interlibraryloans" class="yui-b">
+                [% INCLUDE 'ill-toolbar.inc' %]
+
+                [% IF whole.error %]
+                    <h1>Error performing operation</h1>
+                    <!-- Dispatch on Status -->
+                    <p>We encountered an error:</p>
+                    <p>
+                      <pre>[% whole.message %] ([% whole.status %])</pre>
+                    </p>
+                [% END %]
+
+                [% IF query_type == 'create' %]
+                    <h1>New ILL request</h1>
+                    [% IF whole.stage == 'copyrightclearance' %]
+                        <div>
+                            <p>
+                                [% Koha.Preference('ILLModuleCopyrightClearance') %]
+                            </p>
+                            <a href="?method=create&stage=copyrightclearance&backend=[% whole.value.backend %]"
+                               class="btn btn-sm btn-default btn-group"><i class="fa fa-check">Yes</i></a>
+                            <a href="/cgi-bin/koha/ill/ill-requests.pl"
+                               class="btn btn-sm btn-default btn-group"><i class="fa fa-times">No</i></a>
+                        </div>
+                    [% ELSE %]
+                        [% PROCESS $whole.template %]
+                    [% END %]
+
+                [% ELSIF query_type == 'confirm' %]
+                    <h1>Confirm ILL request</h1>
+                    [% PROCESS $whole.template %]
+
+                [% ELSIF query_type == 'cancel' and !whole.error %]
+                    <h1>Cancel a confirmed request</h1>
+                    [% PROCESS $whole.template %]
+
+                [% ELSIF query_type == 'generic_confirm' %]
+                    <h1>Place request with partner libraries</h1>
+                    <!-- Start of GENERIC_EMAIL case -->
+                    [% IF whole.value.partners %]
+                       [% ill_url = here_link _ "?method=illview&illrequest_id=" _ request.illrequest_id %]
+                        <form method="POST" action=[% here_link %]>
+                            <fieldset class="rows">
+                                <legend>Interlibrary loan request details</legend>
+                                <ol>
+                                    <li>
+                                        <label for="partner_filter">Filter partner libraries:</label>
+                                        <input type="text" id="partner_filter">
+                                    </li>
+                                    <li>
+                                        <label for="partners">Select partner libraries:</label>
+                                        <select size="5" multiple="true" id="partners"
+                                                name="partners">
+                                            [% FOREACH partner IN whole.value.partners %]
+                                                <option value=[% partner.email %]>
+                                                    [% partner.branchcode _ " - " _ partner.surname %]
+                                                </option>
+                                            [% END %]
+                                        </select>
+
+                                    </li>
+                                    <li>
+                                        <label for="subject">Subject Line</label>
+                                        <input type="text" name="subject"
+                                               id="subject" type="text"
+                                               value="[% whole.value.draft.subject %]"/>
+                                    </li>
+                                    <li>
+                                        <label for="body">Email text:</label>
+                                        <textarea name="body" id="body" rows="20" cols="80">[% whole.value.draft.body %]</textarea>
+                                    </li>
+                                </ol>
+                                <input type="hidden" value="generic_confirm" name="method">
+                                <input type="hidden" value="draft" name="stage">
+                                <input type="hidden" value="[% request.illrequest_id %]" name="illrequest_id">
+                            </fieldset>
+                            <fieldset class="action">
+                                <input type="submit" class="btn btn-default" value="Send email"/>
+                                <span><a href="[% ill_url %]" title="Return to request details">Cancel</a></span>
+                            </fieldset>
+                        </form>
+                    [% ELSE %]
+                        <fieldset class="rows">
+                            <legend>Interlibrary loan request details</legend>
+                            <p>No partners have been defined yet. Please create appropriate patron records (by default ILLLIBS category).</p>
+                            <p>Be sure to provide email addresses for these patrons.</p>
+                            <p><span><a href="[% ill_url %]" title="Return to request details">Cancel</a></span></p>
+                        </fieldset>
+                    [% END %]
+                <!-- generic_confirm ends here -->
+
+                [% ELSIF query_type == 'edit_action' %]
+                    <form method="POST" action=[% here_link %]>
+                        <fieldset class="rows">
+                            <legend>Request details</legend>
+                            <ol>
+                                <li class="borrowernumber">
+                                    <label for="borrowernumber">Borrower number:</label>
+                                    <input name="borrowernumber" id="borrowernumber" type="text" value="[% request.borrowernumber %]">
+                                </li>
+                                <li class="biblio_id">
+                                    <label for="biblio_id" class="biblio_id">Biblio number:</label>
+                                    <input name="biblio_id" id="biblio_id" type="text" value="[% request.biblio_id %]">
+                                </li>
+                                <li class="branchcode">
+                                    <label for="branchcode" class="branchcode">Branch:</label>
+                                    <select name="branchcode" id="branch">
+                                        [% FOREACH branch IN branches %]
+                                            [% IF ( branch.branchcode == request.branchcode ) %]
+                                                <option value="[% branch.branchcode %]" selected="selected">
+                                                    [% branch.branchname %]
+                                                </option>
+                                            [% ELSE %]
+                                                <option value="[% branch.branchcode %]">
+                                                    [% branch.branchname %]
+                                                </option>
+                                            [% END %]
+                                        [% END %]
+                                    </select>
+                                </li>
+                                <li class="status">
+                                    <label class="status">Status:</label>
+                                    [% stat = request.status %]
+                                    [% request.capabilities.$stat.name %]
+                                </li>
+                                <li class="updated">
+                                    <label class="updated">Last updated:</label>
+                                    [% request.updated %]
+                                </li>
+                                <li class="medium">
+                                    <label class="medium">Request type:</label>
+                                    [% request.medium %]
+                                </li>
+                                <li class="cost">
+                                    <label class="cost">Cost:</label>
+                                    [% request.cost %]
+                                </li>
+                                <li class="req_id">
+                                    <label class="req_id">Request number:</label>
+                                    [% request.id_prefix _ request.illrequest_id %]
+                                </li>
+                                <li class="notesstaff">
+                                    <label for="notesstaff" class="notesstaff">Staff notes:</label>
+                                    <textarea name="notesstaff" id="notesstaff" rows="5">[% request.notesstaff %]</textarea>
+                                </li>
+                                <li class="notesopac">
+                                    <label for="notesopac" class="notesopac">Opac notes:</label>
+                                    <textarea name="notesopac" id="notesopac" rows="5">[% request.notesopac %]</textarea>
+                                </li>
+                            </ol>
+                        </fieldset>
+                        <fieldset class="action">
+                            <input type="hidden" value="edit_action" name="method">
+                            <input type="hidden" value="form" name="stage">
+                            <input type="hidden" value="[% request.illrequest_id %]" name="illrequest_id">
+                            <input type="submit" value="Submit">
+                            <a class="cancel" href="/cgi-bin/koha/ill/ill-requests.pl?method=illview&amp;illrequest_id=[% request.id %]">Cancel</a>
+                        </fieldset>
+                    </form>
+
+                [% ELSIF query_type == 'delete_confirm' %]
+
+                    <div class="dialog alert">
+                        <h3>Are you sure you wish to delete this request?</h3>
+                        <p>
+                            <a class="btn btn-default btn-sm approve" href="?method=delete&amp;illrequest_id=[% request.id %]&amp;confirmed=1"><i class="fa fa-fw fa-check"></i>Yes</a>
+                            <a class="btn btn-default btn-sm deny" href="?method=illview&amp;illrequest_id=[% request.id %]"><i class="fa fa-fw fa-remove"></i>No</a>
+                        </p>
+                    </div>
+
+
+                [% ELSIF query_type == 'illview' %]
+                    [% actions = request.available_actions %]
+                    [% capabilities = request.capabilities %]
+                    [% req_status = request.status %]
+                    <h1>Manage ILL request</h1>
+                    <div id="toolbar" class="btn-toolbar">
+                        <a title="Edit request" id="ill-toolbar-btn-edit-action" class="btn btn-sm btn-default" href="/cgi-bin/koha/ill/ill-requests.pl?method=edit_action&amp;illrequest_id=[% request.illrequest_id %]">
+                        <span class="fa fa-pencil"></span>
+                        Edit request
+                        </a>
+                        [% FOREACH action IN actions %]
+                            [% IF action.method != 0 %]
+                                <a title="[% action.ui_method_name %]" id="ill-toolbar-btn-[% action.id | lower %]" class="btn btn-sm btn-default" href="/cgi-bin/koha/ill/ill-requests.pl?method=[% action.method %]&amp;illrequest_id=[% request.illrequest_id %]">
+                                <span class="fa [% action.ui_method_icon %]"></span>
+                                [% action.ui_method_name %]
+                                </a>
+                            [% END %]
+                        [% END %]
+                    </div>
+                    <div id="ill-view-panel" class="panel panel-default">
+                        <div class="panel-heading">
+                            <h3>Request details</h3>
+                        </div>
+                        <div class="panel-body">
+                            <h4>Details from library</h4>
+                            <div class="rows">
+                                <div class="orderid">
+                                    <span class="label orderid">Order ID:</span>
+                                    [% request.orderid || "N/A" %]
+                                </div>
+                                <div class="borrowernumber">
+                                    <span class="label borrowernumber">Borrower:</span>
+                                    [% borrowerlink = "/cgi-bin/koha/members/moremember.pl"
+                                    _ "?borrowernumber=" _ request.patron.borrowernumber %]
+                                    <a href="[% borrowerlink %]" title="View borrower details">
+                                    [% request.patron.firstname _ " "
+                                    _ request.patron.surname _ " ["
+                                    _ request.patron.cardnumber
+                                    _ "]" %]
+                                    </a>
+                                </div>
+
+                                <div class="biblio_id">
+                                    <span class="label biblio_id">Biblio number:</span>
+                                    [% request.biblio_id || "N/A" %]
+                                </div>
+                                <div class="branchcode">
+                                    <span class="label branchcode">Branch:</span>
+                                    [% Branches.GetName(request.branchcode) %]
+                                </div>
+                                <div class="status">
+                                    <span class="label status">Status:</span>
+                                    [% capabilities.$req_status.name %]
+                                </div>
+                                <div class="updated">
+                                    <span class="label updated">Last updated:</span>
+                                    [% request.updated %]
+                                </div>
+                                <div class="medium">
+                                    <span class="label medium">Request type:</span>
+                                    [% request.medium %]
+                                </div>
+                                <div class="cost">
+                                    <span class="label cost">Cost:</span>
+                                    [% request.cost || "N/A" %]
+                                </div>
+                                <div class="req_id">
+                                    <span class="label req_id">Request number:</span>
+                                    [% request.id_prefix _ request.illrequest_id %]
+                                </div>
+                                <div class="notesstaff">
+                                    <span class="label notes_staff">Staff notes:</span>
+                                    <pre>[% request.notesstaff %]</pre>
+                                </div>
+                                <div class="notesopac">
+                                    <span class="label notes_opac">Notes:</span>
+                                    <pre>[% request.notesopac %]</pre>
+                                </div>
+                            </div>
+                            <div class="rows">
+                                <h4>Details from supplier ([% request.backend %])</h4>
+                                [% FOREACH meta IN request.metadata %]
+                                    <div class="requestmeta-[% meta.key %]">
+                                        <span class="label">[% meta.key %]:</span>
+                                        [% meta.value %]
+                                    </div>
+                                [% END %]
+                            </div>
+                            <div class="rows">
+                                <h3><a id="toggle_requestattributes" href="#">Toggle full supplier metadata</a></h3>
+                                <div id="requestattributes" class="content_hidden">
+                                    [% FOREACH attr IN request.illrequestattributes %]
+                                        <div class="requestattr-[% attr.type %]">
+                                            <span class="label">[% attr.type %]:</span>
+                                            [% attr.value %]
+                                        </div>
+                                    [% END %]
+                                </div>
+
+                            </div>
+                        </div>
+                    </div>
+
+                [% ELSIF query_type == 'illlist' %]
+                    <!-- illlist -->
+                    <h1>View ILL requests</h1>
+                    <div id="results">
+                        <h3>Details for all requests</h3>
+
+                        <div id="column-toggle">
+                            Toggle additional columns:
+                        </div>
+                        <div id="reset-toggle"><a href="#">Reset toggled columns</a></div>
+
+                        <table
+                            [% FOREACH filter IN prefilters %]
+                            data-filter-[% filter.name %]="[% filter.value %]"
+                            [% END %]
+                            id="ill-requests">
+                            <thead>
+                                <tr id="illview-header"></tr>
+                            </thead>
+                            <tbody id="illview-body">
+                            </tbody>
+                        </table>
+                    </div>
+                [% ELSE %]
+                <!-- Custom Backend Action -->
+                [% INCLUDE $whole.template %]
+
+                [% END %]
+            </div>
+        </div>
+    </div>
+</div>
+
+[% TRY %]
+[% PROCESS backend_jsinclude %]
+[% CATCH %]
+[% END %]
+
+[% INCLUDE 'intranet-bottom.inc' %]
index 2a69dd9..32a1409 100644 (file)
                     <li>
                         <a class="icon_general icon_authorities" href="/cgi-bin/koha/authorities/authorities-home.pl">Authorities</a>
                     </li>
+                    [% IF Koha.Preference('ILLModule') %]
+                    <li>
+                        <a class="icon_general icon_ill" href="/cgi-bin/koha/ill/ill-requests.pl">ILL requests</a>
+                    </li>
+                    [% END %]
                 </ul>
             </div><!-- /area-list-left -->
         </div><!-- /yui-u first -->
index 9afa331..7d27f22 100644 (file)
                 [% END %]
                 <a href="/cgi-bin/koha/opac-discharge.pl">ask for a discharge</a></li>
             [% END %]
+
+            [% IF Koha.Preference( 'ILLModule' ) == 1 %]
+                [% IF ( illrequestsview ) %]
+                    <li class="active">
+                [% ELSE %]
+                    <li>
+                [% END %]
+                <a href="/cgi-bin/koha/opac-illrequests.pl">your interlibrary loan requests</a></li>
+            [% END %]
         </ul>
     </div>
 [% END %]
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 (file)
index 0000000..e0448c1
--- /dev/null
@@ -0,0 +1,221 @@
+[% USE Koha %]
+[% USE Branches %]
+[% INCLUDE 'doc-head-open.inc' %]
+<title>[% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha online[% END %] catalog &rsaquo;   Your Interlibrary loan requests</title>[% INCLUDE 'doc-head-close.inc' %]
+[% BLOCK cssinclude %][% END %]
+</head>
+[% INCLUDE 'bodytag.inc' bodyid='opac-illrequests' bodyclass='scrollto' %]
+[% BLOCK messages %]
+    [% IF message == "1" %]
+        <div class="alert alert-success" role="alert">Request updated</div>
+    [% ELSIF message == "2" %]
+        <div class="alert alert-success" role="alert">Request placed</div>
+    [% END %]
+[% END %]
+[% INCLUDE 'masthead.inc' %]
+<div class="main">
+    <ul class="breadcrumb noprint">
+        <li><a href="/cgi-bin/koha/opac-main.pl">Home</a> <span class="divider">&rsaquo;</span></li>
+        [% IF ( loggedinusername ) %]
+            <li><a href="/cgi-bin/koha/opac-user.pl">[% USER_INFO.title %] [% USER_INFO.firstname %] [% USER_INFO.surname %]</a> <span class="divider">&rsaquo;</span></li>
+        [% END %]
+
+        [% IF method != 'list' %]
+            <li><a href="/cgi-bin/koha/opac-illrequests.pl">Interlibrary loan requests</a> <span class="divider">&rsaquo;</span></li>
+            [% IF method == 'create' %]
+                <li>New Interlibrary loan request</li>
+            [% ELSIF method == 'view' %]
+                <li>View Interlibrary loan request</li>
+            [% END %]
+        [% ELSE %]
+            <li>Interlibrary loan requests</li>
+        [% END %]
+
+    </ul> <!-- / .breadcrumb -->
+
+<div class="container-fluid">
+    <div class="row-fluid">
+        [% IF ( OpacNav||loggedinusername ) && !print %]
+            <div class="span2">
+                <div id="navigation">
+                    [% INCLUDE 'navigation.inc' IsPatronPage=1 %]
+                </div>
+            </div>
+        [% END %]
+
+        [% IF ( OpacNav||loggedinusername ) %]
+            <div class="span10">
+        [% ELSE %]
+            <div class="span12">
+        [% END %]
+            <div id="illrequests" class="maincontent">
+                [% IF method == 'create' %]
+                    <h2>New Interlibrary loan request</h2>
+                    [% INCLUDE messages %]
+                    [% IF backends %]
+                        <form method="post" id="illrequestcreate-form" novalidate="novalidate">
+                            <fieldset class="rows">
+                                <label for="backend">Provider:</label>
+                                <select name="backend">
+                                    [% FOREACH backend IN backends %]
+                                        <option value="[% backend %]">[% backend %]</option>
+                                    [% END %]
+                                </select>
+                            </fieldset>
+                            <fieldset class="action">
+                                <input type="hidden" name="method" value="create">
+                                <input type="submit" name="create_select_backend" value="Next &raquo;">
+                            </fieldset>
+                        </form>
+                    [% ELSE %]
+                        [% PROCESS $whole.opac_template %]
+                    [% END %]
+                [% ELSIF method == 'list' %]
+                    <h2>Interlibrary loan requests</h2>
+                    [% INCLUDE messages %]
+
+                    <div id="illrequests-create-button" class="dropdown btn-group">
+                        [% IF backends.size > 1 %]
+                                <button class="btn btn-default dropdown-toggle" type="button" id="ill-backend-dropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
+                                    <i class="fa fa-plus"></i> Create a new request <span class="caret"></span>
+                                </button>
+                                <ul id="backend-dropdown-options" class="dropdown-menu nojs" aria-labelledby="ill-backend-dropdown">
+                                    [% FOREACH backend IN backends %]
+                                        <li><a href="/cgi-bin/koha/opac-illrequests.pl?method=create&amp;backend=[% backend %]">[% backend %]</a></li>
+                                    [% END %]
+                                </ul>
+                        [% ELSE %]
+                            <a id="ill-new" class="btn btn-default" href="/cgi-bin/koha/opac-illrequests.pl?method=create&amp;backend=[% backends.0 %]">
+                                <i class="fa fa-plus"></i> Create a new request
+                            </a>
+                        [% END %]
+                    </div>
+
+                    <table id="illrequestlist" class="table table-bordered table-striped">
+                        <thead>
+                            <tr>
+                                <th>Author</th>
+                                <th>Title</th>
+                                <th>Requested from</th>
+                                <th>Request type</th>
+                                <th>Status</th>
+                                <th>Request placed</th>
+                                <th>Last updated</th>
+                                <th></th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            [% FOREACH request IN requests %]
+                                [% status = request.status %]
+                                <tr>
+                                    <td>[% request.metadata.Author || 'N/A' %]</td>
+                                    <td>[% request.metadata.Title || 'N/A' %]</td>
+                                    <td>[% request.backend %]</td>
+                                    <td>[% request.medium %]</td>
+                                    <td>[% request.capabilities.$status.name %]</td>
+                                    <td>[% request.placed %]</td>
+                                    <td>[% request.updated %]</td>
+                                    <td>
+                                        <a href="/cgi-bin/koha/opac-illrequests.pl?method=view&amp;illrequest_id=[% request.id %]" class="btn btn-default btn-small pull-right">View</a>
+                                    </td>
+                                </tr>
+                            [% END %]
+                        </tbody>
+                    </table>
+                [% ELSIF method == 'view' %]
+                    <h2>View Interlibrary loan request</h2>
+                    [% INCLUDE messages %]
+                    [% status = request.status %]
+                    <form method="post" action="?method=update" id="illrequestupdate-form" novalidate="novalidate">
+                            <fieldset class="rows">
+                                <legend id="library_legend">Details from library</legend>
+                                <ol>
+                                    <li>
+                                        <label for="backend">Requested from:</label>
+                                        [% request.backend %]
+                                    </li>
+                                    [% IF request.biblio_id %]
+                                        <li>
+                                            <label for="biblio">Requested item:</label>
+                                            <a href="/cgi-bin/koha/opac-detail.pl?biblionumber=[% request.biblio_id %]">Click here to view</a>
+                                        </li>
+                                    [% END %]
+                                    <li>
+                                        <label for="branchcode">Collection library:</label>
+                                        [% Branches.GetName(request.branchcode) %]
+                                    </li>
+                                    <li>
+                                        <label for="status">Status:</label>
+                                        [% request.capabilities.$status.name %]
+                                    </li>
+                                    <li>
+                                        <label for="medium">Request type:</label>
+                                        [% request.medium %]
+                                    </li>
+                                    <li>
+                                        <label for="placed">Request placed:</label>
+                                        [% request.placed %]
+                                    </li>
+                                    <li>
+                                        <label for="updated">Last updated:</label>
+                                        [% request.updated %]
+                                    </li>
+                                    <li>
+                                        <label for="notesopac">Notes:</label>
+                                        [% IF !request.completed %]
+                                            <textarea name="notesopac" rows="5" cols="50">[% request.notesopac %]</textarea>
+                                        [% ELSE %]
+                                            [% request.notesopac %]
+                                        [% END %]
+                                    </li>
+                                </ol>
+                            </fieldset>
+                            <div class="rows">
+                                <legend id="backend_legend">Details from [% request.backend %]</legend>
+                                [% FOREACH meta IN request.metadata %]
+                                    <div class="requestattr-[% meta.key %]">
+                                        <span class="label">[% meta.key %]:</span>
+                                        [% meta.value || 'N/A' %]
+                                    </div>
+                                [% END %]
+                            </div>
+                            <fieldset class="action illrequest-actions">
+                                <input type="hidden" name="illrequest_id" value="[% request.illrequest_id %]">
+                                <input type="hidden" name="method" value="update">
+                                [% IF !request.completed %]
+                                    [% IF request.status == "NEW" %]
+                                        <a class="cancel-illrequest btn btn-danger" href="/cgi-bin/koha/opac-illrequests.pl?method=cancreq&amp;illrequest_id=[% request.illrequest_id %]">Request cancellation</a>
+                                    [% END %]
+                                    <input type="submit" class="update-illrequest btn btn-default" value="Submit modifications">
+                                [% END %]
+                                <span class="cancel"><a href="/cgi-bin/koha/opac-illrequests.pl">Cancel</a></span>
+                            </fieldset>
+                        </form>
+                    [% END %]
+                </div> <!-- / .maincontent -->
+            </div> <!-- / .span10/12 -->
+        </div> <!-- / .row-fluid -->
+    </div> <!-- / .container-fluid -->
+</div> <!-- / .main -->
+
+[% INCLUDE 'opac-bottom.inc' %]
+
+[% BLOCK jsinclude %]
+[% INCLUDE 'datatables.inc' %]
+<script type="text/javascript">
+    //<![CDATA[
+        $("#illrequestlist").dataTable($.extend(true, {}, dataTablesDefaults, {
+            "aoColumnDefs": [
+                { "aTargets": [ -1 ], "bSortable": false, "bSearchable": false }
+            ],
+            "aaSorting": [[ 3, "desc" ]],
+            "deferRender": true
+        }));
+        $("#backend-dropdown-options").removeClass("nojs");
+    //]]>
+</script>
+[% TRY %]
+[% PROCESS backend_jsinclude %]
+[% CATCH %]
+[% END %]
+[% END %]
index 8595f9a..eaa20b3 100644 (file)
@@ -253,14 +253,30 @@ href="/cgi-bin/koha/opac-rss.pl?[% query_cgi %][% limit_cgi |html %]" />
                             [% INCLUDE 'page-numbers.inc' %]
                         [% END # / IF total %]
 
-                        [% IF Koha.Preference( 'suggestion' ) == 1 %]
-                            [% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
-                                <div class="suggestion">Not finding what you're looking for?<br />  Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></div>
-                            [% ELSE %]
-                                [% IF ( loggedinusername ) %]<div class="suggestion">Not finding what you're looking for?<br />  Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></div>[% END %]
-                            [% END %]
+                        [% IF
+                            Koha.Preference( 'suggestion' ) == 1 &&
+                            (
+                                Koha.Preference( 'AnonSuggestions' ) == 1 ||
+                                loggedinusername ||
+                                Koha.Preference( 'ILLModule' ) == 1
+                            )
+                        %]
+                            <div class="suggestion">
+                                Not finding what you're looking for?
+                                <ul>
+                                    [% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
+                                        <li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></li>
+                                    [% ELSE %]
+                                        [% IF ( loggedinusername ) %]
+                                            <li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></li>
+                                        [% END %]
+                                    [% END %]
+                                    [% IF Koha.Preference( 'ILLModule' ) == 1 && loggedinusername %]
+                                        <li>Make an <a href="/cgi-bin/koha/opac-illrequests.pl?op=create">Interlibrary loan request</a></li>
+                                    [% END %]
+                                </ul>
+                            </div>
                         [% END %]
-
                     </div> <!-- / #grouped-results -->
                 </div> <!-- /.span10/12 -->
             </div> <!-- / .row-fluid -->
index a5a7c70..11cf98e 100644 (file)
 
                     [% END # / IF total %]
 
-                    [% IF Koha.Preference( 'suggestion' ) == 1 %]
-                        [% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
-                            <div class="suggestion">Not finding what you're looking for?<br />  Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></div>
-                        [% ELSE %]
-                            [% IF ( loggedinusername ) %]
-                                <div class="suggestion">
-                                    Not finding what you're looking for?<br />  Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a>
-                                </div>
-                            [% END %]
-                        [% END %]
+                    [% IF
+                        Koha.Preference( 'suggestion' ) == 1 &&
+                        (
+                            Koha.Preference( 'AnonSuggestions' ) == 1 ||
+                            loggedinusername ||
+                            Koha.Preference( 'ILLModule' ) == 1
+                        )
+                    %]
+                        <div class="suggestion">
+                            Not finding what you're looking for?
+                            <ul>
+                                [% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
+                                    <li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></li>
+                                [% ELSE %]
+                                    [% IF ( loggedinusername ) %]
+                                        <li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add">purchase suggestion</a></li>
+                                    [% END %]
+                                [% END %]
+                                [% IF Koha.Preference( 'ILLModule' ) == 1 && loggedinusername %]
+                                    <li>Make an <a href="/cgi-bin/koha/opac-illrequests.pl?op=create">Interlibrary loan request</a></li>
+                                [% END %]
+                            </ul>
+                        </div>
                     [% END %]
                     </div> <!-- / #userresults -->
                 </div> <!-- /.span10/12 -->
index 159174f..8423e6c 100644 (file)
@@ -2508,6 +2508,44 @@ a.reviewlink:visited {
     font-size: 90%;
 }
 
+#illrequests {
+    .illrequest-actions {
+        .btn,
+        .cancel {
+            margin-right: 5px;
+        }
+        padding-top: 20px;
+        margin-bottom: 20px;
+    }
+    #illrequests-create-button {
+        margin-bottom: 20px;
+    }
+    .bg-info {
+        overflow: auto;
+        position: relative;
+    }
+    .bg-info {
+        #search-summary {
+            -webkit-transform: translateY(-50%);
+            -ms-transform: translateY(-50%);
+            -o-transform: translateY(-50%);
+            transform: translateY(-50%);
+            position: absolute;
+            top: 50%;
+        }
+
+    }
+    #freeform-fields .custom-name {
+        float: left;
+        width: 8em;
+        margin-right: 1em;
+        text-align: right;
+    }
+    .dropdown:hover .dropdown-menu.nojs {
+        display: block;
+    }
+}
+
 #dc_fieldset {
     border: 1px solid #dddddd;
     border-width: 1px;
diff --git a/opac/opac-illrequests.pl b/opac/opac-illrequests.pl
new file mode 100755 (executable)
index 0000000..9ff413a
--- /dev/null
@@ -0,0 +1,129 @@
+#!/usr/bin/perl
+
+# Copyright 2017 PTFS-Europe Ltd
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+use CGI qw ( -utf8 );
+use C4::Auth;
+use C4::Koha;
+use C4::Output;
+
+use Koha::Illrequests;
+use Koha::Libraries;
+use Koha::Patrons;
+
+my $query = new CGI;
+
+# Grab all passed data
+# 'our' since Plack changes the scoping
+# of 'my'
+our $params = $query->Vars();
+
+# if illrequests is disabled, leave immediately
+if ( ! C4::Context->preference('ILLModule') ) {
+    print $query->redirect("/cgi-bin/koha/errors/404.pl");
+    exit;
+}
+
+my ( $template, $loggedinuser, $cookie ) = get_template_and_user({
+    template_name   => "opac-illrequests.tt",
+    query           => $query,
+    type            => "opac",
+    authnotrequired => ( C4::Context->preference("OpacPublic") ? 1 : 0 ),
+});
+
+my $op = $params->{'method'} || 'list';
+
+if ( $op eq 'list' ) {
+
+    my $requests = Koha::Illrequests->search(
+        { borrowernumber => $loggedinuser }
+    );
+    my $req = Koha::Illrequest->new;
+    $template->param(
+        requests => $requests,
+        backends    => $req->available_backends
+    );
+
+} elsif ( $op eq 'view') {
+    my $request = Koha::Illrequests->find({
+        borrowernumber => $loggedinuser,
+        illrequest_id  => $params->{illrequest_id}
+    });
+    $template->param(
+        request => $request
+    );
+
+} elsif ( $op eq 'update') {
+    my $request = Koha::Illrequests->find({
+        borrowernumber => $loggedinuser,
+        illrequest_id  => $params->{illrequest_id}
+    });
+    $request->notesopac($params->{notesopac})->store;
+    print $query->redirect(
+        '/cgi-bin/koha/opac-illrequests.pl?method=view&illrequest_id=' .
+        $params->{illrequest_id} .
+        '&message=1'
+    );
+} elsif ( $op eq 'cancreq') {
+    my $request = Koha::Illrequests->find({
+        borrowernumber => $loggedinuser,
+        illrequest_id  => $params->{illrequest_id}
+    });
+    $request->status('CANCREQ')->store;
+    print $query->redirect(
+        '/cgi-bin/koha/opac-illrequests.pl?method=view&illrequest_id=' .
+        $params->{illrequest_id} .
+        '&message=1'
+    );
+
+} elsif ( $op eq 'create' ) {
+    if (!$params->{backend}) {
+        my $req = Koha::Illrequest->new;
+        $template->param(
+            backends    => $req->available_backends
+        );
+    } else {
+        my $request = Koha::Illrequest->new
+            ->load_backend($params->{backend});
+        $params->{cardnumber} = Koha::Patrons->find({
+            borrowernumber => $loggedinuser
+        })->cardnumber;
+        my $backend_result = $request->backend_create($params);
+        $template->param(
+            media       => [ "Book", "Article", "Journal" ],
+            branches    => Koha::Libraries->search->unblessed,
+            whole       => $backend_result,
+            request     => $request
+        );
+        if ($backend_result->{stage} eq 'commit') {
+            print $query->redirect('/cgi-bin/koha/opac-illrequests.pl?message=2');
+        }
+    }
+
+
+}
+
+$template->param(
+    message         => $params->{message},
+    illrequestsview => 1,
+    method              => $op
+);
+
+output_html_with_http_headers $query, $cookie, $template->output;
diff --git a/t/db_dependent/Illrequest/Config.t b/t/db_dependent/Illrequest/Config.t
new file mode 100644 (file)
index 0000000..f44fa85
--- /dev/null
@@ -0,0 +1,473 @@
+#!/usr/bin/perl
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+use Koha::Database;
+use t::lib::Mocks;
+use t::lib::TestBuilder;
+use Test::MockObject;
+use Test::Exception;
+
+use Test::More tests => 5;
+
+my $schema = Koha::Database->new->schema;
+my $builder = t::lib::TestBuilder->new;
+use_ok('Koha::Illrequest::Config');
+
+my $base_limits = {
+    branch => { CPL => { count => 1, method => 'annual' } },
+    brw_cat => { A => { count => -1, method => 'active' } },
+    default => { count => 10, method => 'annual' },
+};
+
+my $base_censorship = { censor_notes_staff => 1, censor_reply_date => 1 };
+
+subtest 'Basics' => sub {
+
+    plan tests => 19;
+
+    $schema->storage->txn_begin;
+
+    t::lib::Mocks::mock_preference("UnmediatedILL", 0);
+    t::lib::Mocks::mock_config("interlibrary_loans", {});
+
+    my $config = Koha::Illrequest::Config->new;
+    isa_ok($config, "Koha::Illrequest::Config",
+           "Correctly create and load a config object.");
+
+    # backend:
+    is($config->backend, undef, "backend: Undefined backend is undefined.");
+    is($config->backend("Mock"), "Mock", "backend: setter works.");
+    is($config->backend, "Mock", "backend: setter is persistent.");
+
+    # backend_dir:
+    is($config->backend_dir, undef, "backend_dir: Undefined backend_dir is undefined.");
+    is($config->backend_dir("/tmp/"), "/tmp/", "backend_dir: setter works.");
+    is($config->backend_dir, "/tmp/", "backend_dir: setter is persistent.");
+
+    # partner_code:
+    is($config->partner_code, "ILLLIBS", "partner_code: Undefined partner_code is undefined.");
+    is($config->partner_code("ILLLIBSTST"), "ILLLIBSTST", "partner_code: setter works.");
+    is($config->partner_code, "ILLLIBSTST", "partner_code: setter is persistent.");
+
+    # limits:
+    is_deeply($config->limits, {}, "limits: Undefined limits is empty hash.");
+    is_deeply($config->limits($base_limits), $base_limits, "limits: setter works.");
+    is_deeply($config->limits, $base_limits, "limits: setter is persistent.");
+
+    # censorship:
+    is_deeply($config->censorship, { censor_notes_staff => 0, censor_reply_date => 0 },
+              "censorship: Undefined censorship is default values.");
+    is_deeply($config->censorship($base_censorship), $base_censorship, "censorship: setter works.");
+    is_deeply($config->censorship, $base_censorship, "censorship: setter is persistent.");
+
+    # getLimitRules
+    dies_ok( sub { $config->getLimitRules("FOO") }, "getLimitRules: die if not correct type.");
+    is_deeply($config->getLimitRules("brw_cat"), {
+        A => { count => -1, method => 'active' },
+        default => { count => 10, method => 'annual' },
+    }, "getLimitRules: fetch brw_cat limits.");
+    is_deeply($config->getLimitRules("branch"), {
+        CPL => { count => 1, method => 'annual' },
+        default => { count => 10, method => 'annual' },
+    }, "getLimitRules: fetch brw_cat limits.");
+
+    $schema->storage->txn_rollback;
+};
+
+# _load_unit_config:
+
+subtest '_load_unit_config' => sub {
+
+    plan tests => 10;
+
+    $schema->storage->txn_begin;
+
+    my $config = Koha::Illrequest::Config->new;
+
+    dies_ok(
+        sub { Koha::Illrequest::Config::_load_unit_config({
+            id => 'durineadu', type => 'baz'
+        }) },
+        "_load_unit_config: die if ID is not default, and type is not branch or brw_cat."
+    );
+    is_deeply(
+        Koha::Illrequest::Config::_load_unit_config({
+            unit => {}, id => 'default', config => {}, test => 1
+        }), {}, "_load_unit_config: invocation without id returns unmodified config."
+    );
+
+    is_deeply(
+        Koha::Illrequest::Config::_load_unit_config({
+            unit => { api_key => 'foo', api_auth => 'bar' },
+            id => "CPL", type => 'branch', config => {}
+        }),
+        { credentials => { api_keys => { CPL => { api_key => 'foo', api_auth => 'bar' } } } },
+        "_load_unit_config: add auth values."
+    );
+
+    # Populate request_limits
+    is_deeply(
+        Koha::Illrequest::Config::_load_unit_config({
+            unit => { request_limit => [ 'heelo', 1234 ] },
+            id => "CPL", type => 'branch', config => {}
+        }), {}, "_load_unit_config: invalid request_limit structure."
+    );
+    is_deeply(
+        Koha::Illrequest::Config::_load_unit_config({
+            unit => { request_limit => { method => 'eudiren', count => '-5465' } },
+            id => "CPL", type => 'branch', config => {}
+        }), {}, "_load_unit_config: invalid method & count."
+    );
+    is_deeply(
+        Koha::Illrequest::Config::_load_unit_config({
+            unit => { request_limit => { method => 'annual', count => 6 } },
+            id => "default", config => {}
+        }),
+        { limits => { default => { method => 'annual', count => 6 } } },
+        "_load_unit_config: correct default request_limits."
+    );
+
+    # Populate prefix
+    is_deeply(
+        Koha::Illrequest::Config::_load_unit_config({
+            unit => { prefix => 'Foo-ill' },
+            id => "default", config => {}
+        }),
+        { prefixes => { default => 'Foo-ill' } },
+        "_load_unit_config: correct default prefix."
+    );
+    is_deeply(
+        Koha::Illrequest::Config::_load_unit_config({
+            unit => { prefix => 'Foo-ill' },
+            id => "A", config => {}, type => 'brw_cat'
+        }),
+        { prefixes => { brw_cat => { A => 'Foo-ill' } } },
+        "_load_unit_config: correct brw_cat prefix."
+    );
+
+    # Populate digital_recipient
+    is_deeply(
+        Koha::Illrequest::Config::_load_unit_config({
+            unit => { digital_recipient => 'borrower' },
+            id => "default", config => {}
+        }),
+        { digital_recipients => { default => 'borrower' } },
+        "_load_unit_config: correct default digital_recipient."
+    );
+    is_deeply(
+        Koha::Illrequest::Config::_load_unit_config({
+            unit => { digital_recipient => 'branch' },
+            id => "A", config => {}, type => 'brw_cat'
+        }),
+        { digital_recipients => { brw_cat => { A => 'branch' } } },
+        "_load_unit_config: correct brw_cat digital_recipient."
+    );
+
+    $schema->storage->txn_rollback;
+};
+
+# _load_configuration:
+
+# We have already tested _load_unit_config, so we are reasonably confident
+# that the per-branch, per-borrower_category & default sections parsing is
+# good.
+#
+# Now we need to ensure that Arrays & Hashes are handled correctly, that
+# censorship & ill partners are loaded correctly and that the backend
+# directory is set correctly.
+
+subtest '_load_configuration' => sub {
+
+    plan tests => 9;
+
+    $schema->storage->txn_begin;
+
+    my $config = Koha::Illrequest::Config->new;
+
+    # Return basic configuration
+    is_deeply(
+        Koha::Illrequest::Config::_load_configuration({}, 0),
+        {
+            backend_directory  => undef,
+            censorship         => {
+                censor_notes_staff => 0,
+                censor_reply_date => 0,
+            },
+            limits             => {},
+            digital_recipients => {},
+            prefixes           => {},
+            partner_code       => 'ILLLIBS',
+            raw_config         => {},
+        },
+        "load_configuration: return the base configuration."
+    );
+
+    # Return correct backend_dir
+    is_deeply(
+        Koha::Illrequest::Config::_load_configuration({ backend_directory => '/tmp/' }, 0),
+        {
+            backend_directory  => '/tmp/',
+            censorship         => {
+                censor_notes_staff => 0,
+                censor_reply_date => 0,
+            },
+            limits             => {},
+            digital_recipients => {},
+            prefixes           => {},
+            partner_code       => 'ILLLIBS',
+            raw_config         => { backend_directory => '/tmp/' },
+        },
+        "load_configuration: return the correct backend_dir."
+    );
+
+    # Map over branch configs
+    my $xml_config = {
+        backend_directory => '/tmp/',
+        branch => [
+            { code => '1', request_limit => { method => 'annual', count => 1 } },
+            { code => '2', prefix => '2-prefix' },
+            { code => '3', digital_recipient => 'branch' }
+        ]
+    };
+    is_deeply(
+        Koha::Illrequest::Config::_load_configuration($xml_config, 0),
+        {
+            backend_directory  => '/tmp/',
+            censorship         => {
+                censor_notes_staff => 0,
+                censor_reply_date => 0,
+            },
+            limits             => { branch => { 1 => { method => 'annual', count => 1 } } },
+            digital_recipients => { branch => { 3 => 'branch' } },
+            prefixes           => { branch => { 2 => '2-prefix' } },
+            partner_code       => 'ILLLIBS',
+            raw_config         => $xml_config,
+        },
+        "load_configuration: multi branch config parsed correctly."
+    );
+    # Single branch config
+    $xml_config = {
+        backend_directory => '/tmp/',
+        branch => {
+            code => '1',
+            request_limit => { method => 'annual', count => 1 },
+            prefix => '2-prefix',
+            digital_recipient => 'branch',
+        }
+    };
+    is_deeply(
+        Koha::Illrequest::Config::_load_configuration($xml_config, 0),
+        {
+            backend_directory  => '/tmp/',
+            censorship         => {
+                censor_notes_staff => 0,
+                censor_reply_date => 0,
+            },
+            limits             => { branch => { 1 => { method => 'annual', count => 1 } } },
+            digital_recipients => { branch => { 1 => 'branch' } },
+            prefixes           => { branch => { 1 => '2-prefix' } },
+            partner_code       => 'ILLLIBS',
+            raw_config         => $xml_config,
+        },
+        "load_configuration: single branch config parsed correctly."
+    );
+
+    # Map over borrower_category settings
+    $xml_config = {
+        backend_directory => '/tmp/',
+        borrower_category => [
+            { code => 'A', request_limit => { method => 'annual', count => 1 } },
+            { code => 'B', prefix => '2-prefix' },
+            { code => 'C', digital_recipient => 'branch' }
+        ]
+    };
+    is_deeply(
+        Koha::Illrequest::Config::_load_configuration($xml_config, 0),
+        {
+            backend_directory  => '/tmp/',
+            censorship         => {
+                censor_notes_staff => 0,
+                censor_reply_date => 0,
+            },
+            limits             => { brw_cat => { A => { method => 'annual', count => 1 } } },
+            digital_recipients => { brw_cat => { C => 'branch' } },
+            prefixes           => { brw_cat => { B => '2-prefix' } },
+            partner_code       => 'ILLLIBS',
+            raw_config         => $xml_config,
+        },
+        "load_configuration: multi borrower_category config parsed correctly."
+    );
+    # Single borrower_category config
+    $xml_config = {
+        backend_directory => '/tmp/',
+        borrower_category => {
+            code => '1',
+            request_limit => { method => 'annual', count => 1 },
+            prefix => '2-prefix',
+            digital_recipient => 'branch',
+        }
+    };
+    is_deeply(
+        Koha::Illrequest::Config::_load_configuration($xml_config, 0),
+        {
+            backend_directory  => '/tmp/',
+            censorship         => {
+                censor_notes_staff => 0,
+                censor_reply_date => 0,
+            },
+            limits             => { brw_cat => { 1 => { method => 'annual', count => 1 } } },
+            digital_recipients => { brw_cat => { 1 => 'branch' } },
+            prefixes           => { brw_cat => { 1 => '2-prefix' } },
+            partner_code       => 'ILLLIBS',
+            raw_config         => $xml_config,
+        },
+        "load_configuration: single borrower_category config parsed correctly."
+    );
+
+    # Default Configuration
+    $xml_config = {
+        backend_directory => '/tmp/',
+        request_limit => { method => 'annual', count => 1 },
+        prefix => '2-prefix',
+        digital_recipient => 'branch',
+    };
+    is_deeply(
+        Koha::Illrequest::Config::_load_configuration($xml_config, 0),
+        {
+            backend_directory  => '/tmp/',
+            censorship         => {
+                censor_notes_staff => 0,
+                censor_reply_date => 0,
+            },
+            limits             => { default => { method => 'annual', count => 1 } },
+            digital_recipients => { default => 'branch' },
+            prefixes           => { default => '2-prefix' },
+            partner_code       => 'ILLLIBS',
+            raw_config         => $xml_config,
+        },
+        "load_configuration: parse the default configuration."
+    );
+
+    # Censorship
+    $xml_config = {
+        backend_directory => '/tmp/',
+        staff_request_comments => 'hide',
+        reply_date => 'hide'
+    };
+    is_deeply(
+        Koha::Illrequest::Config::_load_configuration($xml_config, 0),
+        {
+            backend_directory  => '/tmp/',
+            censorship         => {
+                censor_notes_staff => 1,
+                censor_reply_date => 1,
+            },
+            limits             => {},
+            digital_recipients => {},
+            prefixes           => {},
+            partner_code       => 'ILLLIBS',
+            raw_config         => $xml_config,
+        },
+        "load_configuration: parse censorship settings configuration."
+    );
+
+    # Partner library category
+    is_deeply(
+        Koha::Illrequest::Config::_load_configuration({ partner_code => 'FOOBAR' }),
+        {
+            backend_directory  => undef,
+            censorship         => {
+                censor_notes_staff => 0,
+                censor_reply_date => 0,
+            },
+            limits             => {},
+            digital_recipients => {},
+            prefixes           => {},
+            partner_code       => 'FOOBAR',
+            raw_config         => { partner_code => 'FOOBAR' },
+        },
+        "load_configuration: Set partner code."
+    );
+
+    $schema->storage->txn_rollback;
+};
+
+
+subtest 'Final tests' => sub {
+
+    plan tests => 10;
+
+    $schema->storage->txn_begin;
+
+    t::lib::Mocks::mock_preference("UnmediatedILL", 0);
+    t::lib::Mocks::mock_config("interlibrary_loans", {});
+
+    my $config = Koha::Illrequest::Config->new;
+
+    # getPrefixes (error & undef):
+    dies_ok( sub { $config->getPrefixes("FOO") }, "getPrefixes: die if not correct type.");
+    is_deeply($config->getPrefixes("brw_cat"), { default => undef},
+              "getPrefixes: Undefined brw_cat prefix is undefined.");
+    is_deeply($config->getPrefixes("branch"), { default => undef},
+              "getPrefixes: Undefined branch prefix is undefined.");
+
+    # getDigitalRecipients (error & undef):
+    dies_ok( sub { $config->getDigitalRecipients("FOO") },
+             "getDigitalRecipients: die if not correct type.");
+    is_deeply($config->getDigitalRecipients("brw_cat"), { default => undef},
+              "getDigitalRecipients: Undefined brw_cat dig rec is undefined.");
+    is_deeply($config->getDigitalRecipients("branch"), { default => undef},
+              "getDigitalRecipients: Undefined branch dig rec is undefined.");
+
+    $config->{configuration} = Koha::Illrequest::Config::_load_configuration({
+            backend_directory => '/tmp/',
+            prefix => 'DEFAULT-prefix',
+            digital_recipient => 'branch',
+            borrower_category => [
+                { code => 'B', prefix => '2-prefix' },
+                { code => 'C', digital_recipient => 'branch' }
+            ],
+            branch => [
+                { code => '1', prefix => 'T-prefix' },
+                { code => '2', digital_recipient => 'borrower' }
+            ]
+        }, 0
+    );
+
+    # getPrefixes (values):
+    is_deeply($config->getPrefixes("brw_cat"),
+              { B => '2-prefix', default => 'DEFAULT-prefix' },
+              "getPrefixes: return configuration brw_cat prefixes.");
+    is_deeply($config->getPrefixes("branch"),
+              { 1 => 'T-prefix', default => 'DEFAULT-prefix' },
+              "getPrefixes: return configuration branch prefixes.");
+
+    # getDigitalRecipients (values):
+    is_deeply($config->getDigitalRecipients("brw_cat"),
+              { C => 'branch', default => 'branch' },
+              "getDigitalRecipients: return brw_cat digital_recipients.");
+    is_deeply($config->getDigitalRecipients("branch"),
+              { 2 => 'borrower', default => 'branch' },
+              "getDigitalRecipients: return branch digital_recipients.");
+
+    $schema->storage->txn_rollback;
+};
+
+
+1;
diff --git a/t/db_dependent/Illrequestattributes.t b/t/db_dependent/Illrequestattributes.t
new file mode 100644 (file)
index 0000000..ceb0474
--- /dev/null
@@ -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 (file)
index 0000000..34b9eda
--- /dev/null
@@ -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 (file)
index 0000000..fb306ba
--- /dev/null
@@ -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;