From a899c9728e07c92ff17152ada0dbcd38015df00c Mon Sep 17 00:00:00 2001 From: Jiri Kozlovsky Date: Sat, 10 Dec 2016 11:43:49 +0000 Subject: [PATCH] Bug 17314: Routes to handle purchase suggestions --- Koha/Patron.pm | 19 +- Koha/REST/V1/Suggestions.pm | 264 +++++++++++++++++ api/v1/swagger/definitions.json | 3 + api/v1/swagger/definitions/suggestion.json | 131 +++++++++ api/v1/swagger/parameters.json | 3 + api/v1/swagger/parameters/suggestion.json | 9 + api/v1/swagger/paths.json | 6 + api/v1/swagger/paths/suggestions.json | 311 +++++++++++++++++++++ 8 files changed, 745 insertions(+), 1 deletion(-) create mode 100644 Koha/REST/V1/Suggestions.pm create mode 100644 api/v1/swagger/definitions/suggestion.json create mode 100644 api/v1/swagger/parameters/suggestion.json create mode 100644 api/v1/swagger/paths/suggestions.json diff --git a/Koha/Patron.pm b/Koha/Patron.pm index fca796ff28..793ac27cd4 100644 --- a/Koha/Patron.pm +++ b/Koha/Patron.pm @@ -1476,10 +1476,27 @@ sub add_guarantor { )->store(); } +=head3 get_suggestions + +my $suggestions = $patron->get_suggestions + +Return user's suggestions + +=cut + +sub get_suggestions { + my ($self) = @_; + my $suggestions = Koha::Suggestions->search( + { + 'suggestedby' => $self->borrowernumber + } + ); + return $suggestions; +} + =head2 Internal methods =head3 _type - =cut sub _type { diff --git a/Koha/REST/V1/Suggestions.pm b/Koha/REST/V1/Suggestions.pm new file mode 100644 index 0000000000..4b771ff25c --- /dev/null +++ b/Koha/REST/V1/Suggestions.pm @@ -0,0 +1,264 @@ +package Koha::REST::V1::Suggestions; + +# 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 C4::Auth qw( haspermission ); +use C4::Context; +use C4::Koha; + +use Koha::Suggestion; +use Koha::Suggestions; + +use Koha::ItemTypes; +use Koha::Libraries; + +use Try::Tiny; + +sub list { + my ($c, $args, $cb) = @_; + + my $suggestions; + my $filter; + $args //= {}; + + for my $filter_param ( keys %$args ) { + $filter->{$filter_param} = { LIKE => $args->{$filter_param} . '%' }; + } + + return try { + $suggestions = Koha::Suggestions->search($filter)->unblessed; + return $c->$cb( $suggestions, 200 ); + } + catch { + if ( $_->isa('DBIx::Class::Exception') ) { + return $c->$cb( { error => $_->{msg} }, 500 ); + } + else { + return $c->$cb( + { error => 'Something went wrong, check the logs.' }, 500 ); + } + }; +} + +sub get { + my ($c, $args, $cb) = @_; + + my $suggestion = Koha::Suggestions->find($args->{suggestionid}); + unless ($suggestion) { + return $c->$cb({error => 'Suggestion not found'}, 404); + } + + return $c->$cb($suggestion->unblessed, 200); +} + +sub add { + my ( $c, $args, $cb ) = @_; + + my $error = _validate_body($c, $args, $cb, 0); + return $error if $error; + + my $suggestion = Koha::Suggestion->new( $args->{body} ); + + return try { + $suggestion->store; + return $c->$cb( $suggestion->unblessed, 200 ); + } + catch { + if ( $_->isa('DBIx::Class::Exception') ) { + return $c->$cb( { error => $_->msg }, 500 ); + } + else { + return $c->$cb( + { error => 'Something went wrong, check the logs.' }, 500 ); + } + }; +} + +sub update { + my ( $c, $args, $cb ) = @_; + + my $suggestion; + + return try { + + $suggestion = Koha::Suggestions->find( $args->{suggestionid} ); + + my $body = $args->{body}; + + # Remove unchaned fields so that we can use our body validation from the add subroutine + foreach my $param ( keys %{ $body } ) { + if (exists $body->{$param}) { + delete $body->{$param} if $body->{$param} eq $suggestion->unblessed->{$param}; + } + } + + my $error = _validate_body($c, $args, $cb, 1); + return $error if $error; + + $suggestion->set( $body ); + $suggestion->store(); + return $c->$cb( $suggestion->unblessed, 200 ); + } + catch { + if ( not defined $suggestion ) { + return $c->$cb( { error => 'Object not found' }, 404 ); + } + elsif ( $_->isa('Koha::Exceptions::Object') ) { + return $c->$cb( { error => $_->message }, 500 ); + } + else { + return $c->$cb( + { error => 'Something went wrong, check the logs.' }, 500 ); + } + }; + +} + +sub delete { + my ( $c, $args, $cb ) = @_; + + my $suggestion; + + return try { + $suggestion = Koha::Suggestions->find( $args->{suggestionid} ); + $suggestion->delete; + return $c->$cb( '', 200 ); + } + catch { + if ( not defined $suggestion ) { + return $c->$cb( { error => 'Object not found' }, 404 ); + } + elsif ( $_->isa('DBIx::Class::Exception') ) { + return $c->$cb( { error => $_->msg }, 500 ); + } + else { + return $c->$cb( + { error => 'Something went wrong, check the logs.' }, 500 ); + } + }; + +} + +sub _validate_body { + my ( $c, $args, $cb, $updating ) = @_; + + my $body = $args->{body}; + my $user = $c->stash('koha.user'); + + my $has_acquisition = C4::Auth::haspermission($user->userid, {acquisition => 1}); + + if (not $has_acquisition) { + # Regular user cannot change anything ... + my @allowed_fields = ('suggestedby', 'title', 'author', 'copyrightdate', 'isbn', + 'publishercode', 'collectiontitle', 'place', 'itemtype', 'patronreason', 'note'); + + # Hmm, how about branches? + if ( C4::Context->preference('AllowPurchaseSuggestionBranchChoice') ) { + push(@allowed_fields, 'branchcode'); + } + + foreach my $param ( keys %{ $body } ) { + unless (/^$param$/ ~~ @allowed_fields) { + # Ouch ! Some mandatory field is missing! + my $verb = $updating ? 'updated ' : 'specified '; + return $c->$cb({error => 'You ' . $verb . $param . ', but allowed fields are only ' . + join(', ', @allowed_fields)}, 403); + } + } + } + + if (not $updating) { + # Check for missing fields + my @mandatory_fields = split /,/, C4::Context->preference('OPACSuggestionMandatoryFields'); + my @missing_fields = (); + for my $mandatory_field (@mandatory_fields) { + push(@missing_fields, $mandatory_field) if (not exists $body->{$mandatory_field}); + } + + if ( @missing_fields ) { + return $c->$cb({error => 'Missing mandatory fields: ' . join(', ', @missing_fields)}, 400); + } + } + + # Is suggester anonymous? + my $is_anonymous = not (defined $body->{suggestedby} and + $body->{suggestedby} ne '' and + $body->{suggestedby} ne C4::Context->preference('AnonymousPatron')); + + # Refuse if are anonymous suggestions disabled + if ( $is_anonymous ) { + return $c->$cb({error => 'Anonymous suggestions are disabled'}, 403) + unless C4::Context->preference('AnonSuggestions'); + } + + # Refuse adding another suggestion if max reached for a user + my $max_open_suggestions = C4::Context->preference('MaxOpenSuggestions'); + if ( $max_open_suggestions gt 0 and not $is_anonymous ) { + my $count = Koha::Suggestions->search({suggestedby => $body->{suggestedby}})->count(); + + return $c->$cb({error => + 'You have ' . $count . ' opened suggestions out of ' . $max_open_suggestions}, 403) + if ( $count >= $max_open_suggestions ); + } + + # Check STATUS is valid + if ( exists $body->{STATUS} ) { + return $c->$cb({error => 'STATUS must be one of ASKED, CHECKED, ACCEPTED, or REJECTED'}, 400) + unless ($body->{STATUS} =~ m/^(ASKED|CHECKED|ACCEPTED|REJECTED)$/); + } + + # Check itemtype is valid + if ( exists $body->{itemtype} ) { + my @item_types = map {$_->unblessed->{itemtype}} Koha::ItemTypes->search; + return $c->$cb({error => 'itemtype must be one of ' . join(', ', @item_types)}, 400) + unless /^$body->{itemtype}$/ ~~ @item_types; + } + + # Check branchcode is valid + if ( exists $body->{branchcode} ) { + my @branch_codes = map {$_->unblessed->{branchcode}} Koha::Libraries->search; + return $c->$cb({error => 'branchcode must be one of ' . join(', ', @branch_codes)}, 400) + unless /^$body->{branchcode}$/ ~~ @branch_codes; + } + + # Check patron reason is valid + if ( exists $body->{patronreason} ) { + my @authorized_values = map { $_->{authorised_value} } @{ C4::Koha::GetAuthorisedValues('OPAC_SUG') }; + return $c->$cb({error => 'patronreason must be one of ' . join(', ', @authorized_values)}, 400) + unless /^$body->{patronreason}$/ ~~ @authorized_values; + } + + # Check suggestedby patron exists + if ( exists $body->{suggestedby} ) { + return $c->$cb({error => 'suggestedby patron not found'}, 400) + unless Koha::Patrons->find($body->{suggestedby}); + } + + # Check managedby patron exists + if ( exists $body->{managedby} ) { + return $c->$cb({error => 'managedby patron not found'}, 400) + unless Koha::Patrons->find($body->{managedby}); + } + + # Everything's fine .. + return 0; +} + +1; diff --git a/api/v1/swagger/definitions.json b/api/v1/swagger/definitions.json index 4ac42cf925..211892d6fb 100644 --- a/api/v1/swagger/definitions.json +++ b/api/v1/swagger/definitions.json @@ -43,5 +43,8 @@ }, "fund": { "$ref": "definitions/fund.json" + }, + "suggestion": { + "$ref": "definitions/suggestion.json" } } diff --git a/api/v1/swagger/definitions/suggestion.json b/api/v1/swagger/definitions/suggestion.json new file mode 100644 index 0000000000..12ff222722 --- /dev/null +++ b/api/v1/swagger/definitions/suggestion.json @@ -0,0 +1,131 @@ +{ + "type": "object", + "properties": { + "suggestionid": { + "type": "string", + "description": "unique identifier assigned automatically by Koha" + }, + "suggestedby": { + "type": "string", + "description": "borrowernumber for the person making the suggestion, foreign key linking to the borrowers table" + }, + "suggesteddate": { + "type": "string", + "description": "the suggestion was submitted" + }, + "managedby": { + "type": ["string", "null"], + "description": "borrowernumber for the librarian managing the suggestion, foreign key linking to the borrowers table" + }, + "manageddate": { + "type": ["string", "null"], + "description": "date the suggestion was updated" + }, + "acceptedby": { + "type": ["string", "null"], + "description": "borrowernumber for the librarian who accepted the suggestion, foreign key linking to the borrowers table" + }, + "accepteddate": { + "type": ["string", "null"], + "description": "date the suggestion was marked as accepted" + }, + "rejectedby": { + "type": ["string", "null"], + "description": "borrowernumber for the librarian who rejected the suggestion, foreign key linking to the borrowers table" + }, + "rejecteddate": { + "type": ["string", "null"], + "description": "date the suggestion was marked as rejected" + }, + "STATUS": { + "type": "string", + "description": "suggestion status (ASKED, CHECKED, ACCEPTED, or REJECTED)" + }, + "note": { + "type": ["string", "null"], + "description": "note entered on the suggestion" + }, + "author": { + "type": ["string", "null"], + "description": "author of the suggested item" + }, + "title": { + "type": ["string", "null"], + "description": "title of the suggested item" + }, + "copyrightdate": { + "type": ["string", "null"], + "description": "copyright date of the suggested item" + }, + "publishercode": { + "type": ["string", "null"], + "description": "publisher of the suggested item" + }, + "date": { + "type": ["string", "null"], + "description": "date created" + }, + "volumedesc": { + "type": ["string", "null"], + "description": "volume description" + }, + "publicationyear": { + "type": "string", + "description": "year of publication" + }, + "place": { + "type": ["string", "null"], + "description": "publication place of the suggested item" + }, + "isbn": { + "type": ["string", "null"], + "description": "isbn of the suggested item" + }, + "biblionumber": { + "type": ["string", "null"], + "description": "foreign key linking the suggestion to the biblio table after the suggestion has been ordered" + }, + "reason": { + "type": ["string", "null"], + "description": "reason for accepting or rejecting the suggestion" + }, + "patronreason": { + "type": ["string", "null"], + "description": "reason for making the suggestion" + }, + "budgetid": { + "type": ["string", "null"], + "description": "foreign key linking the suggested budget to the aqbudgets table" + }, + "branchcode": { + "type": ["string", "null"], + "description": "foreign key linking the suggested branch to the branches table" + }, + "collectiontitle": { + "type": ["string", "null"], + "description": "collection name for the suggested item" + }, + "itemtype": { + "type": ["string", "null"], + "description": "suggested item type" + }, + "quantity": { + "type": ["string", "null"], + "description": "suggested quantity to be purchased" + }, + "currency": { + "type": ["string", "null"], + "description": "suggested currency for the suggested price" + }, + "price": { + "type": ["string", "null"], + "description": "suggested price" + }, + "total": { + "type": ["string", "null"], + "description": "suggested total cost (price*quantity updated for currency)" + } + }, + "additionalProperties": false, + "required": ["title"] +} diff --git a/api/v1/swagger/parameters.json b/api/v1/swagger/parameters.json index 932d251d42..55837c0a8f 100644 --- a/api/v1/swagger/parameters.json +++ b/api/v1/swagger/parameters.json @@ -69,5 +69,8 @@ }, "fundidPathParam": { "$ref": "parameters/fund.json#/fundidPathParam" + }, + "suggestionidPathParam": { + "$ref": "parameters/suggestion.json#/suggestionidPathParam" } } diff --git a/api/v1/swagger/parameters/suggestion.json b/api/v1/swagger/parameters/suggestion.json new file mode 100644 index 0000000000..75647ea81c --- /dev/null +++ b/api/v1/swagger/parameters/suggestion.json @@ -0,0 +1,9 @@ +{ + "suggestionidPathParam": { + "name": "suggestionid", + "in": "path", + "description": "Internal suggestion identifier", + "required": true, + "type": "integer" + } +} diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json index d705760f63..d5ae06ad6e 100644 --- a/api/v1/swagger/paths.json +++ b/api/v1/swagger/paths.json @@ -85,5 +85,11 @@ }, "/public/patrons/{patron_id}/guarantors/can_see_checkouts": { "$ref": "paths/public_patrons.json#/~1public~1patrons~1{patron_id}~1guarantors~1can_see_checkouts" + }, + "/suggestions": { + "$ref": "paths/suggestions.json#/~1suggestions" + }, + "/suggestions/{suggestionid}": { + "$ref": "paths/suggestions.json#/~1suggestions~1{suggestionid}" } } diff --git a/api/v1/swagger/paths/suggestions.json b/api/v1/swagger/paths/suggestions.json new file mode 100644 index 0000000000..dfef6cba05 --- /dev/null +++ b/api/v1/swagger/paths/suggestions.json @@ -0,0 +1,311 @@ +{ + "/suggestions": { + "get": { + "x-mojo-controller": "Koha::REST::V1::Suggestions", + "operationId": "list", + "tags": ["patrons", "suggestions"], + "parameters": [ + { + "name": "suggestionid", + "in": "query", + "type": "integer", + "description": "Internal suggestion identifier" + }, + { + "name": "suggestedby", + "in": "query", + "type": "integer", + "description": "borrowernumber for the person making the suggestion, foreign key linking to the borrowers table" + }, + { + "name": "managedby", + "in": "query", + "type": "integer", + "description": "borrowernumber for the librarian managing the suggestion, foreign key linking to the borrowers table" + }, + { + "name": "acceptedby", + "in": "query", + "type": "integer", + "description": "borrowernumber for the librarian who accepted the suggestion, foreign key linking to the borrowers table" + }, + { + "name": "rejectedby", + "in": "query", + "type": "integer", + "description": "borrowernumber for the librarian who rejected the suggestion, foreign key linking to the borrowers table" + }, + { + "name": "STATUS", + "in": "query", + "type": "string", + "description": "suggestion status (ASKED, CHECKED, ACCEPTED, or REJECTED)" + }, + { + "name": "author", + "in": "query", + "type": "string", + "description": "author of the suggested item" + }, + { + "name": "title", + "in": "query", + "type": "string", + "description": "title of the suggested item" + }, + { + "name": "publishercode", + "in": "query", + "type": "string", + "description": "publisher of the suggested item" + }, + { + "name": "date", + "in": "query", + "type": "string", + "description": "date created" + }, + { + "name": "publicationyear", + "in": "query", + "type": "string", + "description": "year of publication" + }, + { + "name": "isbn", + "in": "query", + "type": "string", + "description": "isbn of the suggested item" + }, + { + "name": "collectiontitle", + "in": "query", + "type": "string", + "description": "collection name for the suggested item" + }, + { + "name": "itemtype", + "in": "query", + "type": "string", + "description": "suggested item type" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A list of suggestions", + "schema": { + "type": "array", + "items": { + "$ref": "../definitions.json#/suggestion" + } + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "allow-owner": true, + "permissions": { + "acquisition": "1" + } + } + }, + "post": { + "x-mojo-controller": "Koha::REST::V1::Suggestions", + "operationId": "add", + "tags": ["patrons", "suggestions"], + "parameters": [{ + "name": "body", + "in": "body", + "description": "A JSON object containing informations about the new suggestion", + "required": true, + "schema": { + "$ref": "../definitions.json#/suggestion" + } + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Suggestion added", + "schema": { + "$ref": "../definitions.json#/suggestion" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "allow-owner": true, + "permissions": { + "acquisition": "1" + } + } + } + }, + "/suggestions/{suggestionid}": { + "get": { + "x-mojo-controller": "Koha::REST::V1::Suggestions", + "operationId": "get", + "tags": ["patrons", "suggestions"], + "parameters": [{ + "$ref": "../parameters.json#/suggestionidPathParam" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A suggestion", + "schema": { + "$ref": "../definitions.json#/suggestion" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Suggestion not found", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "allow-owner": true, + "allow-guarantor": true, + "permissions": { + "acquisition": "1" + } + } + }, + "put": { + "x-mojo-controller": "Koha::REST::V1::Suggestions", + "operationId": "update", + "tags": ["patrons", "suggestions"], + "parameters": [{ + "$ref": "../parameters.json#/suggestionidPathParam" + }, { + "name": "body", + "in": "body", + "description": "A JSON object containing informations about the new hold", + "required": true, + "schema": { + "$ref": "../definitions.json#/suggestion" + } + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A suggestion", + "schema": { + "$ref": "../definitions.json#/suggestion" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Suggestion not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "allow-owner": true, + "permissions": { + "acquisition": "1" + } + } + }, + "delete": { + "x-mojo-controller": "Koha::REST::V1::Suggestions", + "operationId": "delete", + "tags": ["patrons", "suggestions"], + "parameters": [{ + "$ref": "../parameters.json#/suggestionidPathParam" + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Suggestion deleted", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Suggestion not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "allow-owner": true, + "permissions": { + "acquisition": "1" + } + } + } + } +} -- 2.23.0