@@ -, +, @@ reserves in the swagger.json file is reached (you should have a 403 error) --- Koha/REST/V1/Reserve.pm | 160 +++++++++++++++++++++++++++++++++++++ api/v1/definitions/index.json | 2 + api/v1/definitions/reserve.json | 63 +++++++++++++++ api/v1/definitions/reserves.json | 4 + api/v1/swagger.json | 165 +++++++++++++++++++++++++++++++++++++++ t/db_dependent/api/v1/reserves.t | 154 ++++++++++++++++++++++++++++++++++++ 6 files changed, 548 insertions(+) create mode 100644 Koha/REST/V1/Reserve.pm create mode 100644 api/v1/definitions/reserve.json create mode 100644 api/v1/definitions/reserves.json create mode 100644 t/db_dependent/api/v1/reserves.t --- a/Koha/REST/V1/Reserve.pm +++ a/Koha/REST/V1/Reserve.pm @@ -0,0 +1,160 @@ +package Koha::REST::V1::Reserve; + +# 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::Biblio; +use C4::Reserves; + +use Koha::Borrowers; +use Koha::DateUtils; + +sub list { + my ($c, $args, $cb) = @_; + + my $borrowernumber = $c->param('borrowernumber'); + my $borrower = Koha::Borrowers->find($borrowernumber); + unless ($borrower) { + return $c->$cb({error => "Borrower not found"}, 404); + } + + my @reserves = C4::Reserves::GetReservesFromBorrowernumber($borrowernumber); + + return $c->$cb(\@reserves, 200); +} + +sub add { + my ($c, $args, $cb) = @_; + + my $body = $c->req->json; + + my $borrowernumber = $body->{borrowernumber}; + my $biblionumber = $body->{biblionumber}; + my $itemnumber = $body->{itemnumber}; + my $branchcode = $body->{branchcode}; + my $expirationdate = $body->{expirationdate}; + + my $borrower = Koha::Borrowers->find($borrowernumber); + unless ($borrower) { + return $c->$cb({error => "Borrower not found"}, 404); + } + + unless ($biblionumber or $itemnumber) { + return $c->$cb({ + error => "At least one of biblionumber, itemnumber should be given" + }, 400); + } + unless ($branchcode) { + return $c->$cb({ + error => "Branchcode is required" + }, 400); + } + + my $item_biblionumber = C4::Biblio::GetBiblionumberFromItemnumber($itemnumber); + if ($biblionumber and $biblionumber != $item_biblionumber) { + return $c->$cb({ + error => "Item $itemnumber doesn't belong to biblio $biblionumber" + }, 400); + } + + $biblionumber ||= $item_biblionumber; + my $biblio = C4::Biblio::GetBiblio($biblionumber); + + my $can_reserve = + $itemnumber + ? CanItemBeReserved( $borrowernumber, $itemnumber ) + : CanBookBeReserved( $borrowernumber, $biblionumber ); + + unless ($can_reserve eq 'OK') { + return $c->$cb({ + error => "Reserve cannot be placed. Reason: $can_reserve" + }, 403); + } + + my $priority = C4::Reserves::CalculatePriority($biblionumber); + $itemnumber ||= undef; + + # AddReserve expects date to be in syspref format + if ($expirationdate) { + $expirationdate = output_pref(dt_from_string($expirationdate, 'iso')); + } + + my $reserve_id = C4::Reserves::AddReserve($branchcode, $borrowernumber, + $biblionumber, undef, $priority, undef, $expirationdate, undef, + $biblio->{title}, $itemnumber); + + unless ($reserve_id) { + return $c->$cb({ + error => "Error while placing reserve. See Koha logs for details." + }, 500); + } + + my $reserve = C4::Reserves::GetReserve($reserve_id); + + return $c->$cb($reserve, 201); +} + +sub edit { + my ($c, $args, $cb) = @_; + + my $reserve_id = $args->{reserve_id}; + my $reserve = C4::Reserves::GetReserve($reserve_id); + + unless ($reserve) { + return $c->$cb({error => "Reserve not found"}, 404); + } + + my $body = $c->req->json; + + my $branchcode = $body->{branchcode}; + my $priority = $body->{priority}; + my $suspend_until = $body->{suspend_until}; + + if ($suspend_until) { + $suspend_until = output_pref(dt_from_string($suspend_until, 'iso')); + } + + my $params = { + reserve_id => $reserve_id, + branchcode => $branchcode, + rank => $priority, + suspend_until => $suspend_until, + }; + C4::Reserves::ModReserve($params); + $reserve = C4::Reserves::GetReserve($reserve_id); + + return $c->$cb($reserve, 200); +} + +sub delete { + my ($c, $args, $cb) = @_; + + my $reserve_id = $args->{reserve_id}; + my $reserve = C4::Reserves::GetReserve($reserve_id); + + unless ($reserve) { + return $c->$cb({error => "Reserve not found"}, 404); + } + + C4::Reserves::CancelReserve({ reserve_id => $reserve_id }); + + return $c->$cb({}, 200); +} + +1; --- a/api/v1/definitions/index.json +++ a/api/v1/definitions/index.json @@ -1,4 +1,6 @@ { "patron": { "$ref": "patron.json" }, + "reserves": { "$ref": "reserves.json" }, + "reserve": { "$ref": "reserve.json" }, "error": { "$ref": "error.json" } } --- a/api/v1/definitions/reserve.json +++ a/api/v1/definitions/reserve.json @@ -0,0 +1,63 @@ +{ + "type": "object", + "properties": { + "reserve_id": { + "description": "Internal reserve identifier" + }, + "borrowernumber": { + "type": "string", + "description": "internally assigned user identifier" + }, + "reservedate": { + "description": "the date the reserve was placed" + }, + "biblionumber": { + "type": "string", + "description": "internally assigned biblio identifier" + }, + "branchcode": { + "type": ["string", "null"], + "description": "internally assigned branch identifier" + }, + "notificationdate": { + "description": "currently unused" + }, + "reminderdate": { + "description": "currently unused" + }, + "cancellationdate": { + "description": "the date the reserve was cancelled" + }, + "reservenotes": { + "description": "notes related to this reserve" + }, + "priority": { + "description": "where in the queue the patron sits" + }, + "found": { + "description": "a one letter code defining what the status of the reserve is after it has been confirmed" + }, + "timestamp": { + "description": "date and time the reserve was last updated" + }, + "itemnumber": { + "type": ["string", "null"], + "description": "internally assigned item identifier" + }, + "waitingdate": { + "description": "the date the item was marked as waiting for the patron at the library" + }, + "expirationdate": { + "description": "the date the reserve expires" + }, + "lowestPriority": { + "description": "" + }, + "suspend": { + "description": "" + }, + "suspend_until": { + "description": "" + } + } +} --- a/api/v1/definitions/reserves.json +++ a/api/v1/definitions/reserves.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "reserve.json" } +} --- a/api/v1/swagger.json +++ a/api/v1/swagger.json @@ -73,6 +73,164 @@ } } } + }, + "/reserves": { + "get": { + "operationId": "listReserves", + "tags": ["borrowers", "reserves"], + "parameters": [ + { + "name": "borrowernumber", + "in": "query", + "description": "Internal borrower identifier", + "required": true, + "type": "integer" + } + ], + "produces": ["application/json"], + "responses": { + "200": { + "description": "A list of reserves", + "schema": { "$ref": "#/definitions/reserves" } + }, + "404": { + "description": "Borrower not found", + "schema": { "$ref": "#/definitions/error" } + } + } + }, + "post": { + "operationId": "addReserve", + "tags": ["borrowers", "reserves"], + "parameters": [ + { + "name": "body", + "in": "body", + "description": "A JSON object containing informations about the new reserve", + "required": true, + "schema": { + "type": "object", + "properties": { + "borrowernumber": { + "description": "Borrower internal identifier", + "type": "integer" + }, + "biblionumber": { + "description": "Biblio internal identifier", + "type": "integer" + }, + "itemnumber": { + "description": "Item internal identifier", + "type": "integer" + }, + "branchcode": { + "description": "Pickup location", + "type": "string" + }, + "expirationdate": { + "description": "Reserve end date", + "type": "string", + "format": "date" + } + } + } + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "responses": { + "201": { + "description": "Created reserve", + "schema": { "$ref": "#/definitions/reserve" } + }, + "400": { + "description": "Missing or wrong parameters", + "schema": { "$ref": "#/definitions/error" } + }, + "403": { + "description": "Reserve not allowed", + "schema": { "$ref": "#/definitions/error" } + }, + "404": { + "description": "Borrower not found", + "schema": { "$ref": "#/definitions/error" } + }, + "500": { + "description": "Internal error", + "schema": { "$ref": "#/definitions/error" } + } + } + } + }, + "/reserves/{reserve_id}": { + "put": { + "operationId": "editReserve", + "tags": ["reserves"], + "parameters": [ + { "$ref": "#/parameters/reserveIdPathParam" }, + { + "name": "body", + "in": "body", + "description": "A JSON object containing fields to modify", + "required": true, + "schema": { + "type": "object", + "properties": { + "priority": { + "description": "Position in waiting queue", + "type": "integer", + "minimum": 1 + }, + "branchcode": { + "description": "Pickup location", + "type": "string" + }, + "suspend_until": { + "description": "Suspend until", + "type": "string", + "format": "date" + } + } + } + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "responses": { + "200": { + "description": "Updated reserve", + "schema": { "$ref": "#/definitions/reserve" } + }, + "400": { + "description": "Missing or wrong parameters", + "schema": { "$ref": "#/definitions/error" } + }, + "404": { + "description": "Reserve not found", + "schema": { "$ref": "#/definitions/error" } + } + } + }, + "delete": { + "operationId": "deleteReserve", + "tags": ["reserves"], + "parameters": [ + { "$ref": "#/parameters/reserveIdPathParam" } + ], + "produces": ["application/json"], + "responses": { + "200": { + "description": "Successful deletion", + "schema": { + "type": "object" + } + }, + "404": { + "description": "Reserve not found", + "schema": { "$ref": "#/definitions/error" } + } + } + } } }, "definitions": { @@ -85,6 +243,13 @@ "description": "Internal patron identifier", "required": true, "type": "integer" + }, + "reserveIdPathParam": { + "name": "reserve_id", + "in": "path", + "description": "Internal reserve identifier", + "required": true, + "type": "integer" } } } --- a/t/db_dependent/api/v1/reserves.t +++ a/t/db_dependent/api/v1/reserves.t @@ -0,0 +1,154 @@ +#!/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 => 30; +use Test::Mojo; + +use DateTime; + +use C4::Context; +use C4::Biblio; +use C4::Items; +use C4::Reserves; + +use Koha::Database; +use Koha::Borrower; + +my $dbh = C4::Context->dbh; +$dbh->{AutoCommit} = 0; +$dbh->{RaiseError} = 1; + +my $t = Test::Mojo->new('Koha::REST::V1'); + +my $categorycode = Koha::Database->new()->schema()->resultset('Category')->first()->categorycode(); +my $branchcode = Koha::Database->new()->schema()->resultset('Branch')->first()->branchcode(); + +my $borrower = Koha::Borrower->new; +$borrower->categorycode( $categorycode ); +$borrower->branchcode( $branchcode ); +$borrower->surname("Test Surname"); +$borrower->store; +my $borrowernumber = $borrower->borrowernumber; + +my $borrower2 = Koha::Borrower->new; +$borrower2->categorycode( $categorycode ); +$borrower2->branchcode( $branchcode ); +$borrower2->surname("Test Surname 2"); +$borrower2->store; +my $borrowernumber2 = $borrower2->borrowernumber; + +my $biblionumber = create_biblio('RESTful Web APIs'); +my $itemnumber = create_item($biblionumber, 'TEST000001'); + +my $reserve_id = C4::Reserves::AddReserve($branchcode, $borrowernumber, + $biblionumber, undef, 1, undef, undef, undef, '', $itemnumber); + +# Add another reserve to be able to change first reserve's rank +C4::Reserves::AddReserve($branchcode, $borrowernumber2, + $biblionumber, undef, 2, undef, undef, undef, '', $itemnumber); + +my $suspend_until = DateTime->now->add(days => 10)->ymd; +my $put_data = { + priority => 2, + suspend_until => $suspend_until, +}; +$t->put_ok("/api/v1/reserves/$reserve_id" => json => $put_data) + ->status_is(200) + ->json_is('/reserve_id', $reserve_id) + ->json_is('/suspend_until', $suspend_until . ' 00:00:00') + ->json_is('/priority', 2); + +$t->delete_ok("/api/v1/reserves/$reserve_id") + ->status_is(200); + +$t->put_ok("/api/v1/reserves/$reserve_id" => json => $put_data) + ->status_is(404) + ->json_has('/error'); + +$t->delete_ok("/api/v1/reserves/$reserve_id") + ->status_is(404) + ->json_has('/error'); + + +$t->get_ok("/api/v1/reserves?borrowernumber=$borrowernumber") + ->status_is(200) + ->json_is([]); + +my $inexisting_borrowernumber = $borrowernumber2 + 1; +$t->get_ok("/api/v1/reserves?borrowernumber=$inexisting_borrowernumber") + ->status_is(404) + ->json_has('/error'); + +$dbh->do('DELETE FROM issuingrules'); +$dbh->do(q{ + INSERT INTO issuingrules (categorycode, branchcode, itemtype, reservesallowed) + VALUES (?, ?, ?, ?) +}, {}, '*', '*', '*', 1); + +my $expirationdate = DateTime->now->add(days => 10)->ymd; +my $post_data = { + borrowernumber => int($borrowernumber), + biblionumber => int($biblionumber), + itemnumber => int($itemnumber), + branchcode => $branchcode, + expirationdate => $expirationdate, +}; +$t->post_ok("/api/v1/reserves" => json => $post_data) + ->status_is(201) + ->json_has('/reserve_id'); + +$reserve_id = $t->tx->res->json->{reserve_id}; + +$t->get_ok("/api/v1/reserves?borrowernumber=$borrowernumber") + ->status_is(200) + ->json_is('/0/reserve_id', $reserve_id) + ->json_is('/0/expirationdate', $expirationdate) + ->json_is('/0/branchcode', $branchcode); + +$t->post_ok("/api/v1/reserves" => json => $post_data) + ->status_is(403) + ->json_like('/error', qr/tooManyReserves/); + + +$dbh->rollback; + +sub create_biblio { + my ($title) = @_; + + my $record = new MARC::Record; + $record->append_fields( + new MARC::Field('200', ' ', ' ', a => $title), + ); + + my ($biblionumber) = C4::Biblio::AddBiblio($record, ''); + + return $biblionumber; +} + +sub create_item { + my ($biblionumber, $barcode) = @_; + + my $item = { + barcode => $barcode, + }; + + my $itemnumber = C4::Items::AddItem($item, $biblionumber); + + return $itemnumber; +} --