Browse Source

Bug 13903: Add API routes to list, create, update, delete reserves

GET    /reserves?borrowernumber=X (list)
POST   /reserves                  (create)
PUT    /reserves/{reserve_id}     (update)
DELETE /reserves/{reserve_id}     (delete)

Unit tests in t/db_dependent/api/v1/reserves.t

Test plan:
1. Apply patch
2. Run unit tests
3. Play with the API with your favorite REST client, using documentation
   in the swagger.json file
4. Try to make reserves until the maximum number of reserves for a user
   is reached (you should have a 403 error)

Depends on bug 15126

Signed-off-by: Benjamin Rokseth <benjamin.rokseth@kul.oslo.kommune.no>

Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
16.05.x
Julian Maurice 8 years ago
committed by Kyle M Hall
parent
commit
70928807d8
  1. 161
      Koha/REST/V1/Reserve.pm
  2. 2
      api/v1/definitions/index.json
  3. 63
      api/v1/definitions/reserve.json
  4. 4
      api/v1/definitions/reserves.json
  5. 165
      api/v1/swagger.json
  6. 154
      t/db_dependent/api/v1/reserves.t

161
Koha/REST/V1/Reserve.pm

@ -0,0 +1,161 @@
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::Patrons;
use Koha::DateUtils;
sub list {
my ($c, $args, $cb) = @_;
my $borrowernumber = $c->param('borrowernumber');
my $borrower = Koha::Patrons->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::Patrons->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);
}
if ($itemnumber) {
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;

2
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" }
}

63
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": ""
}
}
}

4
api/v1/definitions/reserves.json

@ -0,0 +1,4 @@
{
"type": "array",
"items": { "$ref": "reserve.json" }
}

165
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"
}
}
}

154
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::Patron;
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::Patron->new;
$borrower->categorycode( $categorycode );
$borrower->branchcode( $branchcode );
$borrower->surname("Test Surname");
$borrower->store;
my $borrowernumber = $borrower->borrowernumber;
my $borrower2 = Koha::Patron->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;
}
Loading…
Cancel
Save