Bugzilla – Attachment 45842 Details for
Bug 13903
Add API routes to list, create, update, delete holds
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 13903: Add API routes to list, create, update, delete reserves
Bug-13903-Add-API-routes-to-list-create-update-del.patch (text/plain), 18.95 KB, created by
Julian Maurice
on 2015-12-18 15:31:46 UTC
(
hide
)
Description:
Bug 13903: Add API routes to list, create, update, delete reserves
Filename:
MIME Type:
Creator:
Julian Maurice
Created:
2015-12-18 15:31:46 UTC
Size:
18.95 KB
patch
obsolete
>From 95ba8e693e6234b23fead7e32a198656b3e08df1 Mon Sep 17 00:00:00 2001 >From: Julian Maurice <julian.maurice@biblibre.com> >Date: Tue, 24 Mar 2015 11:30:00 +0100 >Subject: [PATCH] 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 >--- > 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 > >diff --git a/Koha/REST/V1/Reserve.pm b/Koha/REST/V1/Reserve.pm >new file mode 100644 >index 0000000..beb0abb >--- /dev/null >+++ b/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; >diff --git a/api/v1/definitions/index.json b/api/v1/definitions/index.json >index e940eee..4e17693 100644 >--- a/api/v1/definitions/index.json >+++ b/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" } > } >diff --git a/api/v1/definitions/reserve.json b/api/v1/definitions/reserve.json >new file mode 100644 >index 0000000..74370fe >--- /dev/null >+++ b/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": "" >+ } >+ } >+} >diff --git a/api/v1/definitions/reserves.json b/api/v1/definitions/reserves.json >new file mode 100644 >index 0000000..431c444 >--- /dev/null >+++ b/api/v1/definitions/reserves.json >@@ -0,0 +1,4 @@ >+{ >+ "type": "array", >+ "items": { "$ref": "reserve.json" } >+} >diff --git a/api/v1/swagger.json b/api/v1/swagger.json >index e821c44..b5c6d0a 100644 >--- a/api/v1/swagger.json >+++ b/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" > } > } > } >diff --git a/t/db_dependent/api/v1/reserves.t b/t/db_dependent/api/v1/reserves.t >new file mode 100644 >index 0000000..9741542 >--- /dev/null >+++ b/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; >+} >-- >1.9.1
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 13903
:
37210
|
40589
|
41164
|
41549
|
45842
|
49843
|
50852
|
50853
|
50959