From 613cf72ef59e9f374bcb88dcc92181924f3fed99 Mon Sep 17 00:00:00 2001 From: Agustin Moyano Date: Thu, 19 Aug 2021 23:51:38 -0300 Subject: [PATCH] Bug 27947: Add cancellation reason to article request This bug adds a cancellation reason authorised values to article requests To test: 1. apply this patch 2. updatedatabase 3. in staff interface go to /cgi-bin/koha/admin/authorised_values.pl CHECK => AR_CANCELLATION category should appears 4. place several article requests 5. in staff interface go to /cgi-bin/koha/circ/article-requests.pl 6. select multiple requests, or just one and cancel them SUCCESS => a modal pops up offering to select a cancellation reason 7. repeat steps 4 to 6 but for /cgi-bin/koha/circ/request-article.pl 8. repeat steps 4 to 6 but for /cgi-bin/koha/opac-user.pl#opac-user-article-requests in opac interface --- Koha/ArticleRequest.pm | 4 +- Koha/Exceptions/ArticleRequests.pm | 17 ++++ Koha/REST/V1/ArticleRequests.pm | 75 ++++++++++++++++ Koha/REST/V1/Patrons.pm | 48 ++++++++++ api/v1/swagger/parameters.json | 6 ++ .../swagger/parameters/article_request.json | 16 ++++ api/v1/swagger/paths.json | 8 +- api/v1/swagger/paths/article_requests.json | 67 ++++++++++++++ api/v1/swagger/paths/public_patrons.json | 67 ++++++++++++++ .../prog/en/modules/circ/article-requests.tt | 88 ++++++++++++++----- .../prog/en/modules/circ/request-article.tt | 77 +++++++++++----- .../bootstrap/en/modules/opac-user.tt | 45 +++++++--- 12 files changed, 460 insertions(+), 58 deletions(-) create mode 100644 Koha/Exceptions/ArticleRequests.pm create mode 100644 Koha/REST/V1/ArticleRequests.pm create mode 100644 api/v1/swagger/parameters/article_request.json create mode 100644 api/v1/swagger/paths/article_requests.json diff --git a/Koha/ArticleRequest.pm b/Koha/ArticleRequest.pm index 05b0e32542..def15464e2 100644 --- a/Koha/ArticleRequest.pm +++ b/Koha/ArticleRequest.pm @@ -83,10 +83,10 @@ sub complete { =cut sub cancel { - my ( $self, $notes ) = @_; + my ( $self, $cancellation_reason ) = @_; $self->status(Koha::ArticleRequest::Status::Canceled); - $self->notes($notes) if $notes; + $self->cancellation_reason($cancellation_reason) if $cancellation_reason; $self->store(); $self->notify(); return $self; diff --git a/Koha/Exceptions/ArticleRequests.pm b/Koha/Exceptions/ArticleRequests.pm new file mode 100644 index 0000000000..669529ca47 --- /dev/null +++ b/Koha/Exceptions/ArticleRequests.pm @@ -0,0 +1,17 @@ +package Koha::Exceptions::ArticleRequests; + +use Modern::Perl; + +use Exception::Class ( + + 'Koha::Exceptions::ArticleRequests' => { + description => 'Something went wrong!', + }, + 'Koha::Exceptions::ArticleRequests::FailedCancel' => { + isa => 'Koha::Exceptions::ArticleRequests', + description => 'Failed to cancel article request' + } + +); + +1; \ No newline at end of file diff --git a/Koha/REST/V1/ArticleRequests.pm b/Koha/REST/V1/ArticleRequests.pm new file mode 100644 index 0000000000..2151d6bda0 --- /dev/null +++ b/Koha/REST/V1/ArticleRequests.pm @@ -0,0 +1,75 @@ +package Koha::REST::V1::ArticleRequests; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Controller'; + +use Koha::Database; +use Koha::ArticleRequests; + +use Scalar::Util qw( blessed ); +use Try::Tiny qw( catch try ); + +=head1 NAME + +Koha::REST::V1::ArticleRequests + +=head1 API + +=head2 Methods + +=head3 cancel + +Controller function that handles cancelling a Koha::ArticleRequest object + +=cut + +sub cancel { + my $c = shift->openapi->valid_input or return; + + my $ar = Koha::ArticleRequests->find( $c->validation->param('ar_id') ); + + unless ( $ar ) { + return $c->render( + status => 404, + openapi => { error => "Article request not found" } + ); + } + + my $reason = $c->validation->param('cancellation_reason'); + + return try { + + $ar->cancel($reason); + return $c->render( + status => 204, + openapi => q{} + ); + } catch { + if ( blessed $_ && $_->isa('Koha::Exceptions::ArticleRequests::FailedCancel') ) { + return $c->render( + status => 403, + openapi => { error => "Article request cannot be canceled" } + ); + } + + $c->unhandled_exception($_); + }; +} + +1; \ No newline at end of file diff --git a/Koha/REST/V1/Patrons.pm b/Koha/REST/V1/Patrons.pm index 80b64ee963..6ceeaa3553 100644 --- a/Koha/REST/V1/Patrons.pm +++ b/Koha/REST/V1/Patrons.pm @@ -419,4 +419,52 @@ sub guarantors_can_see_checkouts { }; } +=head3 cancel_article_request + +Controller function that handles cancelling a patron's Koha::ArticleRequest object + +=cut + +sub cancel_article_request { + my $c = shift->openapi->valid_input or return; + + my $patron = Koha::Patrons->find( $c->validation->param('patron_id') ); + + unless ( $patron ) { + return $c->render( + status => 404, + openapi => { error => "Patron not found" } + ); + } + + my $ar = $patron->article_requests->find( $c->validation->param('ar_id') ); + + unless ( $ar ) { + return $c->render( + status => 404, + openapi => { error => "Article request not found" } + ); + } + + my $reason = $c->validation->param('cancellation_reason'); + + return try { + + $ar->cancel($reason); + return $c->render( + status => 204, + openapi => q{} + ); + } catch { + if ( blessed $_ && $_->isa('Koha::Exceptions::ArticleRequests::FailedCancel') ) { + return $c->render( + status => 403, + openapi => { error => "Article request cannot be canceled" } + ); + } + + $c->unhandled_exception($_); + }; +} + 1; diff --git a/api/v1/swagger/parameters.json b/api/v1/swagger/parameters.json index 95464e1461..ebcad9db64 100644 --- a/api/v1/swagger/parameters.json +++ b/api/v1/swagger/parameters.json @@ -56,6 +56,12 @@ "cashup_id_pp": { "$ref": "parameters/cashup.json#/cashup_id_pp" }, + "ar_id_pp": { + "$ref": "parameters/article_request.json#/ar_id_pp" + }, + "ar_reason_qp": { + "$ref": "parameters/article_request.json#/ar_reason_qp" + }, "match": { "name": "_match", "in": "query", diff --git a/api/v1/swagger/parameters/article_request.json b/api/v1/swagger/parameters/article_request.json new file mode 100644 index 0000000000..b1c9af4642 --- /dev/null +++ b/api/v1/swagger/parameters/article_request.json @@ -0,0 +1,16 @@ +{ + "ar_id_pp": { + "name": "ar_id", + "in": "path", + "description": "Article request identifier", + "required": true, + "type": "integer" + }, + "ar_reason_qp": { + "name": "cancellation_reason", + "in": "query", + "description": "Article request cancellation reason", + "required": false, + "type": "string" + } +} diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json index cdcd0368c5..91949ac46b 100644 --- a/api/v1/swagger/paths.json +++ b/api/v1/swagger/paths.json @@ -17,6 +17,9 @@ "/acquisitions/funds": { "$ref": "paths/acquisitions_funds.json#/~1acquisitions~1funds" }, + "/article_requests/{ar_id}": { + "$ref": "paths/article_requests.json#/~1article_requests~1{ar_id}" + }, "/biblios/{biblio_id}": { "$ref": "paths/biblios.json#/~1biblios~1{biblio_id}" }, @@ -134,7 +137,7 @@ "/patrons/{patron_id}/extended_attributes/{extended_attribute_id}": { "$ref": "paths/patrons_extended_attributes.json#/~1patrons~1{patron_id}~1extended_attributes~1{extended_attribute_id}" }, - "/patrons/{patron_id}/holds": { + "/patrons/{patron_id}/holds": { "$ref": "paths/patrons_holds.json#/~1patrons~1{patron_id}~1holds" }, "/patrons/{patron_id}/password": { @@ -170,6 +173,9 @@ "/public/patrons/{patron_id}/guarantors/can_see_checkouts": { "$ref": "paths/public_patrons.json#/~1public~1patrons~1{patron_id}~1guarantors~1can_see_checkouts" }, + "/public/patrons/{patron_id}/article_requests/{ar_id}": { + "$ref": "paths/public_patrons.json#/~1public~1patrons~1{patron_id}~1article_requests~1{ar_id}" + }, "/quotes": { "$ref": "paths/quotes.json#/~1quotes" }, diff --git a/api/v1/swagger/paths/article_requests.json b/api/v1/swagger/paths/article_requests.json new file mode 100644 index 0000000000..74da0e13e7 --- /dev/null +++ b/api/v1/swagger/paths/article_requests.json @@ -0,0 +1,67 @@ +{ + "/article_requests/{ar_id}": { + "delete": { + "x-mojo-to": "ArticleRequests#cancel", + "operationId": "cancelArticleRequest", + "tags": [ + "article_requests" + ], + "summary": "Cancel article requests", + "parameters": [ + { + "$ref": "../parameters.json#/ar_id_pp" + }, + { + "$ref": "../parameters.json#/ar_reason_qp" + } + ], + "produces": ["application/json"], + "responses": { + "204": { + "description": "Article request canceled" + }, + "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": { + "reserveforothers": "1" + } + } + } + } +} \ No newline at end of file diff --git a/api/v1/swagger/paths/public_patrons.json b/api/v1/swagger/paths/public_patrons.json index 90d6cfe4c4..d73f6b8164 100644 --- a/api/v1/swagger/paths/public_patrons.json +++ b/api/v1/swagger/paths/public_patrons.json @@ -242,5 +242,72 @@ "allow-owner": true } } + }, + "/public/patrons/{patron_id}/article_requests/{ar_id}": { + "delete": { + "x-mojo-to": "Patrons#cancel_article_request", + "operationId": "cancelPatronArticleRequest", + "tags": [ + "patrons", + "article_requests" + ], + "summary": "Cancel patron's article requests", + "parameters": [ + { + "$ref": "../parameters.json#/patron_id_pp" + }, + { + "$ref": "../parameters.json#/ar_id_pp" + }, + { + "$ref": "../parameters.json#/ar_reason_qp" + } + ], + "produces": ["application/json"], + "responses": { + "204": { + "description": "Patron's article request canceled" + }, + "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": { + "allow-owner": true + } + } } } diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/article-requests.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/article-requests.tt index 61e94ca026..1a2e06a1d9 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/article-requests.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/article-requests.tt @@ -36,7 +36,7 @@ Complete request - + Cancel request @@ -75,6 +75,40 @@ [% END %] +[% BLOCK cancel_modal %] + +[% END %] [% INCLUDE 'header.inc' %] @@ -346,6 +380,7 @@ + [% PROCESS cancel_modal %] [% MACRO jsinclude BLOCK %] [% INCLUDE 'datatables.inc' %] @@ -442,30 +477,37 @@ window.open(link, 'popup', 'width=600,height=400,resizable=1,toolbar=0,scrollbars=1,top'); } + // Confirm cancellation of article requests + let cancel_id; + let cancel_a; + $("#cancelModalConfirmBtn").on("click",function(e) { + let reason = $("#modal-cancellation-reason").val(); + + HandleMulti(function(id, a) { + var table_row = a.closest('tr'); + table_row.find('.ar-process-request').remove(); + table_row.find('input[type="checkbox"]').prop('checked', false); + + + a.closest('td').prepend('').find('div.dropdown').hide(); + + $.ajax({ + type: "DELETE", + url: '/api/v1/article_requests/'+id+'?cancellation_reason='+reason, + success: function( data ) { + active_datatable.row( a.closest('tr') ).remove().draw(); + UpdateTabCounts(); + activateBatchActions( active_tab ); + } + }); + }, cancel_id, cancel_a) + }); + function Cancel( id, a ) { - // last_cancel_reason: undefined means 'prompt for new reason' - // a null value indicates that prompt was cancelled - if( last_cancel_reason === undefined ) last_cancel_reason = prompt(_("Please specify the reason for cancelling selected item(s):")); - if ( last_cancel_reason === null ) { - return; - } + cancel_id = id; + cancel_a = a; - a.closest('td').prepend('').find('div.dropdown').hide(); - $.ajax({ - type: "POST", - url: '/cgi-bin/koha/svc/article_request', - data: { - action: 'cancel', - id: id, - notes: last_cancel_reason - }, - success: function( data ) { - active_datatable.row( a.closest('tr') ).remove().draw(); - UpdateTabCounts(); - activateBatchActions( active_tab ); - }, - dataType: 'json' - }); + $('#cancelModal').modal(); } function Process( id, a ) { diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/request-article.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/request-article.tt index 9206dba4e2..14574f6405 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/request-article.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/request-article.tt @@ -3,6 +3,7 @@ [% USE KohaDates %] [% USE Branches %] [% USE ItemTypes %] +[% USE AuthorisedValues %] [% SET footerjs = 1 %] [% SET article_requests_view = 1 %] [% SET biblionumber = biblio.biblionumber %] @@ -11,6 +12,41 @@ [% INCLUDE 'doc-head-close.inc' %] +[% BLOCK cancel_modal %] + +[% END %] + [% INCLUDE 'header.inc' %] [% INCLUDE 'circ-search.inc' %] @@ -310,6 +346,7 @@ [% END %] + [% PROCESS cancel_modal %] [% END %] @@ -394,29 +431,27 @@ } }); - $(".ar-cancel-request").on("click", function(){ - var a = $(this); - var notes = prompt(_("Reason for cancellation:")); + let cancel_a; + $("#cancelModalConfirmBtn").on("click",function(e) { + var id = cancel_a.attr('id').split("cancel-")[1]; + $("#cancel-processing-" + id ).hide('slow'); + $("#cancel-processing-spinner-" + id ).show('slow'); - if ( notes != null ) { - var id = this.id.split("cancel-")[1]; - $("#cancel-processing-" + id ).hide('slow'); - $("#cancel-processing-spinner-" + id ).show('slow'); + let reason = $("#modal-cancellation-reason").val(); - $.ajax({ - type: "POST", - url: '/cgi-bin/koha/svc/article_request', - data: { - action: 'cancel', - id: id, - notes: notes - }, - success: function( data ) { - a.parents('tr').hide('slow'); - }, - dataType: 'json' - }); - } + $.ajax({ + type: "DELETE", + url: '/api/v1/article_requests/'+id+'?cancellation_reason='+reason, + success: function( data ) { + cancel_a.parents('tr').hide('slow'); + } + }); + }); + + + $(".ar-cancel-request").on("click", function(){ + cancel_a = $(this); + $('#cancelModal').modal(); }); // Initialize format(s) diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt index 7c92ba2f9e..7d4464d4e3 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt @@ -5,6 +5,7 @@ [% USE Branches %] [% USE ItemTypes %] [% USE Price %] +[% USE AuthorisedValues %] [% SET AdlibrisEnabled = Koha.Preference('AdlibrisCoversEnabled') %] [% SET AdlibrisURL = Koha.Preference('AdlibrisCoversURL') %] @@ -715,7 +716,7 @@
[% IF logged_in_user.article_requests_current.count %] - + @@ -801,12 +802,7 @@ [% END %] @@ -843,6 +839,9 @@ [% INCLUDE 'calendar.inc' %] [% INCLUDE 'datatables.inc' %]
Article requests ([% logged_in_user.article_requests_current.count | html %] total)Article requests
Record title - Cancel: -
- Cancel article request - - -
+