From a5ea08df5b0a8de8125cb05d309dbae2ea0aca43 Mon Sep 17 00:00:00 2001 From: Andrew Isherwood Date: Wed, 16 Oct 2019 16:02:29 +0100 Subject: [PATCH] Bug 23838: Add 'renewals' API route This patch adds a /patrons/x/renewals API GET route which returns a patron's renewals, optionally filtered by item_id. Tests are also included Sponsored-by: Loughborough University Signed-off-by: Myka Kennedy Stephens Signed-off-by: Owen Leonard Signed-off-by: Bouzid Fergani --- Koha/REST/V1/Patrons/Renewals.pm | 162 ++++++++++++++++++++++++ api/v1/swagger/definitions.json | 3 + api/v1/swagger/definitions/patron_renewal.json | 23 ++++ api/v1/swagger/definitions/patron_renewals.json | 6 + api/v1/swagger/paths.json | 3 + api/v1/swagger/paths/patrons_renewals.json | 90 +++++++++++++ t/db_dependent/api/v1/patrons_renewals.t | 160 +++++++++++++++++++++++ 7 files changed, 447 insertions(+) create mode 100644 Koha/REST/V1/Patrons/Renewals.pm create mode 100644 api/v1/swagger/definitions/patron_renewal.json create mode 100644 api/v1/swagger/definitions/patron_renewals.json create mode 100644 api/v1/swagger/paths/patrons_renewals.json create mode 100644 t/db_dependent/api/v1/patrons_renewals.t diff --git a/Koha/REST/V1/Patrons/Renewals.pm b/Koha/REST/V1/Patrons/Renewals.pm new file mode 100644 index 0000000..9432bc6 --- /dev/null +++ b/Koha/REST/V1/Patrons/Renewals.pm @@ -0,0 +1,162 @@ +package Koha::REST::V1::Patrons::Renewals; + +# 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::ActionLogs; +use Koha::Patrons; +use Koha::DateUtils qw/ output_pref /; + +=head1 NAME + +Koha::REST::V1::Patrons::Renewals + +=head1 API + +=head2 Methods + +=head3 get + +Controller function that handles retrieving a patron's renewals, optionally +limiting by item id + +=cut + +sub get { + my $c = shift->openapi->valid_input or return; + + my $patron_id = $c->validation->param('patron_id'); + unless ($patron_id) { + return $c->render( + status => 400, + openapi => { error => "Patron ID must be specified." } + ); + } + + my $item_id = $c->validation->param('item_id'); + # Create a hash where all keys are embedded values + # Enables easy checking + my $args = $c->req->params->to_hash // {}; + my %embed; + my $args_arr = (ref $args->{embed} eq 'ARRAY') ? $args->{embed} : [ $args->{embed} ]; + if (defined $args->{embed}) { + %embed = map { $_ => 1 } @{$args_arr}; + delete $args->{embed}; + } + + # Fetch renewals + my $where = { + module => 'CIRCULATION', + action => 'RENEWAL', + object => $patron_id + }; + $where->{info} = $item_id if $item_id; + my $renewals = Koha::ActionLogs->search($where); + + # Now fetch all users associated with these renewals, so we can + # embed their details in the response, if appropriate + my $users = {}; + if ($embed{renewed_by} || $embed{patron}) { + my $unique_bn = {}; + foreach my $renewal( @{ $renewals->as_list } ) { + $unique_bn->{$renewal->user} = 1; + $unique_bn->{$renewal->object} = 1; + } + my @borrowernumbers = keys %{$unique_bn}; + my $users_rs = Koha::Patrons->search({ + borrowernumber => { 'in' => \@borrowernumbers } + }); + foreach my $user( @{ $users_rs->as_list } ) { + $users->{$user->borrowernumber} = $user; + } + } + + # Prepare the response + my @to_return = (); + foreach my $return( @{$renewals->as_list} ) { + # Map property names + $return = _to_api( $return->TO_JSON ); + # Correctly format the timestamp + $return->{timestamp} = output_pref({ + str => $return->{timestamp}, + dateonly => 1 + }); + # Embed as appropriate + if ($embed{renewed_by}) { + # If this was a system renewal, create a fake patron to + # return containing basic details + if ($return->{renewed_by_id} == 0) { + $return->{renewed_by} = { + firstname => 'System', + surname => '' + }; + } else { + $return->{renewed_by} = $users->{$return->{renewed_by_id}}; + } + } + if ($embed{patron}) { + $return->{patron} = $users->{$return->{patron_id}}; + } + push @to_return, $return; + } + + return $c->render( status => 200, openapi => \@to_return ); +} + +=head3 _to_api + +Helper function that maps Koha::ActionLog objects +into REST API attribute names. + +=cut + +sub _to_api { + my $action_log = shift; + + # Rename attributes + foreach my $column ( + keys %{ $Koha::REST::V1::Patrons::Renewals::to_api_mapping } + ) { + my $mapped_column = + $Koha::REST::V1::Patrons::Renewals::to_api_mapping->{$column}; + if (exists $action_log->{ $column } && defined $mapped_column ) { + $action_log->{ $mapped_column } = delete $action_log->{ $column }; + } elsif (exists $action_log->{ $column } && !defined $mapped_column ) { + delete $action_log->{ $column }; + } + } + + return $action_log; +} + +=head2 Global variables + +=head3 $to_api_mapping + +=cut + +our $to_api_mapping = { + timestamp => 'timestamp', + user => 'renewed_by_id', + object => 'patron_id', + info => 'item_id', + interface => 'interface' +}; + +1; diff --git a/api/v1/swagger/definitions.json b/api/v1/swagger/definitions.json index 9037414..66a0b65 100644 --- a/api/v1/swagger/definitions.json +++ b/api/v1/swagger/definitions.json @@ -35,6 +35,9 @@ "patron_balance": { "$ref": "definitions/patron_balance.json" }, + "patron_renewals": { + "$ref": "definitions/patron_renewals.json" + }, "allows_renewal": { "$ref": "definitions/allows_renewal.json" }, diff --git a/api/v1/swagger/definitions/patron_renewal.json b/api/v1/swagger/definitions/patron_renewal.json new file mode 100644 index 0000000..eacb83c --- /dev/null +++ b/api/v1/swagger/definitions/patron_renewal.json @@ -0,0 +1,23 @@ +{ + "type": "object", + "properties": { + "timestamp": { + "type": "string", + "description": "action timestamp" + }, + "user": { + "$ref": "../x-primitives.json#/patron_id" + }, + "object": { + "$ref": "../x-primitives.json#/patron_id" + }, + "info": { + "type": "integer", + "description": "Item ID" + }, + "interface": { + "type": ["string"], + "description": "The interface in which the renewal took place" + } + } +} diff --git a/api/v1/swagger/definitions/patron_renewals.json b/api/v1/swagger/definitions/patron_renewals.json new file mode 100644 index 0000000..4d94b66 --- /dev/null +++ b/api/v1/swagger/definitions/patron_renewals.json @@ -0,0 +1,6 @@ +{ + "type": "array", + "items": { + "$ref": "patron_renewal.json" + } +} diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json index 95278ec..e90e140 100644 --- a/api/v1/swagger/paths.json +++ b/api/v1/swagger/paths.json @@ -74,6 +74,9 @@ "/patrons/{patron_id}/password": { "$ref": "paths/patrons_password.json#/~1patrons~1{patron_id}~1password" }, + "/patrons/{patron_id}/renewals": { + "$ref": "paths/patrons_renewals.json#/~1patrons~1{patron_id}~1renewals" + }, "/illrequests": { "$ref": "paths/illrequests.json#/~1illrequests" }, diff --git a/api/v1/swagger/paths/patrons_renewals.json b/api/v1/swagger/paths/patrons_renewals.json new file mode 100644 index 0000000..788acea --- /dev/null +++ b/api/v1/swagger/paths/patrons_renewals.json @@ -0,0 +1,90 @@ +{ + "/patrons/{patron_id}/renewals": { + "get": { + "x-mojo-to": "Patrons::Renewals#get", + "operationId": "getPatronRenewals", + "tags": [ + "patron" + ], + "parameters": [ + { + "$ref": "../parameters.json#/patron_id_pp" + }, + { + "name": "item_id", + "in": "query", + "required": false, + "description": "An optional item ID", + "type": "integer" + }, + { + "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", + "renewed_by" + ] + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Patron's renewals", + "schema": { + "$ref": "../definitions.json#/patron_renewals" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Patron not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "borrowers": "edit_borrowers" + } + } + } + } +} diff --git a/t/db_dependent/api/v1/patrons_renewals.t b/t/db_dependent/api/v1/patrons_renewals.t new file mode 100644 index 0000000..faa286c --- /dev/null +++ b/t/db_dependent/api/v1/patrons_renewals.t @@ -0,0 +1,160 @@ +#!/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::MockModule; +use Test::MockObject; +use Test::Mojo; +use Test::Warn; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +use C4::Auth; +use Koha::ActionLogs; +use Koha::DateUtils qw( format_sqldatetime ); + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +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 => 24; + + $schema->storage->txn_begin; + + Koha::ActionLogs->search->delete; + # borrowers => 4 (userflags.sql) + my ( $borrowernumber, $session_id ) = create_user_and_session({ authorized => 4 }); + + ## Authorized user tests + my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); + # No logs, so empty array should be returned + my $tx = $t->ua->build_tx( GET => '/api/v1/patrons/999/renewals'); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx)->status_is(200)->json_is( [] ); + + # Create log entries + my $log1 = $builder->build_object( + { + class => 'Koha::ActionLogs', + value => { + module => 'CIRCULATION', + action => 'RENEWAL', + user => $borrowernumber, # Renewing staff member + object => $patron->borrowernumber, # Patron + info => 101, # Item ID + interface => 'opac' # Renewing interface + } + } + ); + my $log2 = $builder->build_object( + { + class => 'Koha::ActionLogs', + value => { + module => 'CIRCULATION', + action => 'RENEWAL', + user => $borrowernumber, # Renewing staff member + object => $patron->borrowernumber, # Patron + info => 102, # Item ID + interface => 'opac' # Renewing interface + } + } + ); + # Getting all log entries for user + $tx = $t->ua->build_tx( GET => '/api/v1/patrons/'.$patron->borrowernumber.'/renewals'); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx)->status_is(200)->json_is( '/0/patron_id' => $patron->borrowernumber ); + $t->request_ok($tx)->status_is(200)->json_is( '/1/patron_id' => $patron->borrowernumber ); + + # Get log entries filtered by item ID + $tx = $t->ua->build_tx( GET => '/api/v1/patrons/'.$patron->borrowernumber.'/renewals?item_id=102'); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx)->status_is(200)->json_is( '/0/item_id' => 102 ); + + # Get logs with patron embedded + Koha::ActionLogs->search->delete; + my $embed_patron = $builder->build_object({ class => 'Koha::Patrons' }); + my $log_patron = $builder->build_object( + { + class => 'Koha::ActionLogs', + value => { + module => 'CIRCULATION', + action => 'RENEWAL', + object => $embed_patron->borrowernumber, # Patron + user => $embed_patron->borrowernumber, # Renewing staff member + } + } + ); + # Get log entry including patron embed + $tx = $t->ua->build_tx( GET => '/api/v1/patrons/'.$embed_patron->borrowernumber.'/renewals?embed=patron'); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx)->status_is(200)->json_is( '/0/patron/borrowernumber' => $embed_patron->borrowernumber ); + # Get log entry including renewed_by embed + $tx = $t->ua->build_tx( GET => '/api/v1/patrons/'.$embed_patron->borrowernumber.'/renewals?embed=renewed_by'); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx)->status_is(200)->json_is( '/0/renewed_by/borrowernumber' => $embed_patron->borrowernumber ); + # Get log entry including both embeds + $tx = $t->ua->build_tx( GET => '/api/v1/patrons/'.$embed_patron->borrowernumber.'/renewals?embed=patron,renewed_by'); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx)->status_is(200)->json_is( '/0/patron/borrowernumber' => $embed_patron->borrowernumber ); + $t->request_ok($tx)->status_is(200)->json_is( '/0/renewed_by/borrowernumber' => $embed_patron->borrowernumber ); + + $schema->storage->txn_rollback; +}; + +sub create_user_and_session { + + my $args = shift; + my $dbh = C4::Context->dbh; + + my $flags = ( $args->{authorized} ) ? 2**$args->{authorized} : 0; + + 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; + + return ( $user->{borrowernumber}, $session->id ); +} + +1; -- 2.7.4