From 793f95f8cce24dc0db359bf479e750e5df253ed5 Mon Sep 17 00:00:00 2001 From: Benjamin Rokseth Date: Thu, 29 Sep 2016 02:16:45 +0200 Subject: [PATCH] Bug 17371 - REST API: add CRUD for biblios Patch to add CRUD to biblios. Test plan: 1) add dependent bug 17004 2) create some staff user with editcatalogue permission 3) play with API with routes explained in bug description 4) run test t/db_dependent/api/v1/biblios.t --- Koha/REST/V1/Biblio.pm | 173 ++++++++++++++++++++++++++++++++ api/v1/swagger/definitions.json | 6 ++ api/v1/swagger/definitions/biblio.json | 52 ++++++++++ api/v1/swagger/definitions/biblios.json | 4 + api/v1/swagger/parameters.json | 3 + api/v1/swagger/parameters/biblio.json | 9 ++ api/v1/swagger/paths.json | 12 +++ api/v1/swagger/paths/biblios.json | 153 ++++++++++++++++++++++++++++ t/db_dependent/api/v1/biblios.t | 90 +++++++++++++++++ 9 files changed, 502 insertions(+) create mode 100644 Koha/REST/V1/Biblio.pm create mode 100644 api/v1/swagger/definitions/biblio.json create mode 100644 api/v1/swagger/definitions/biblios.json create mode 100644 api/v1/swagger/parameters/biblio.json create mode 100644 api/v1/swagger/paths/biblios.json create mode 100644 t/db_dependent/api/v1/biblios.t diff --git a/Koha/REST/V1/Biblio.pm b/Koha/REST/V1/Biblio.pm new file mode 100644 index 0000000..d936530 --- /dev/null +++ b/Koha/REST/V1/Biblio.pm @@ -0,0 +1,173 @@ +package Koha::REST::V1::Biblio; + +# 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 qw( GetBiblioData AddBiblio ModBiblio DelBiblio ); +use C4::Items qw ( AddItemBatchFromMarc ); +use Koha::Biblios; +use MARC::Record; +use MARC::Batch; +use MARC::File::USMARC; +use MARC::File::XML; + +use Data::Dumper; + +sub get { + my ($c, $args, $cb) = @_; + + my $biblio = &GetBiblioData($args->{biblionumber}); + unless ($biblio) { + return $c->$cb({error => "Biblio not found"}, 404); + } + return $c->$cb($biblio, 200); +} + +sub getitems { + my ($c, $args, $cb) = @_; + + my $biblio = Koha::Biblios->find($args->{biblionumber}); + unless ($biblio) { + return $c->$cb({error => "Biblio not found"}, 404); + } + return $c->$cb({ biblio => $biblio->unblessed, items => $biblio->items->unblessed }, 200); +} + +sub getexpanded { + my ($c, $args, $cb) = @_; + + my $biblio = Koha::Biblios->find($args->{biblionumber}); + unless ($biblio) { + return $c->$cb({error => "Biblio not found"}, 404); + } + my $expanded = $biblio->items->unblessed; + for my $item (@{$expanded}) { + + # we assume item is available by default + $item->{status} = "available"; + + if ($item->{onloan}) { + $item->{status} = "onloan" + } + + if ($item->{restricted}) { + $item->{status} = "restricted"; + } + + # mark as unavailable if notforloan, damaged, lost, or withdrawn + if ($item->{damaged} || $item->{itemlost} || $item->{withdrawn} || $item->{notforloan}) { + $item->{status} = "unavailable"; + } + + my $holds = Koha::Holds->search({itemnumber => $item->{itemnumber}})->unblessed; + + # mark as onhold if item marked as hold + if (scalar(@{$holds}) > 0) { + $item->{status} = "onhold"; + } + } + + return $c->$cb({ biblio => $biblio->unblessed, items => $expanded }, 200); +} + +sub add { + my ($c, $args, $cb) = @_; + + my $biblionumber; + my $biblioitemnumber; + + my $body = $c->req->body; + unless ($body) { + return $c->$cb({error => "Missing MARCXML body"}, 400); + } + + my $record = eval {MARC::Record::new_from_xml( $body, "utf8", '')}; + if ($@) { + return $c->$cb({error => $@}, 400); + } else { + ( $biblionumber, $biblioitemnumber ) = &AddBiblio($record, ''); + } + if ($biblionumber) { + $c->res->headers->location($c->url_for('/api/v1/biblios/')->to_abs . $biblionumber); + my ( $itemnumbers, $errors ) = &AddItemBatchFromMarc( $record, $biblionumber, $biblioitemnumber, '' ); + unless (@{$errors}) { + return $c->$cb({biblionumber => $biblionumber, items => join(",", @{$itemnumbers})}, 201); + } else { + warn Dumper($errors); + return $c->$cb({error => "Error creating items, see Koha Logs for details.", biblionumber => $biblionumber, items => join(",", @{$itemnumbers})}, 400); + } + } else { + return $c->$cb({error => "unable to create record"}, 400); + } +} + +# NB: This will not update any items, Items should be a separate API route +sub update { + my ($c, $args, $cb) = @_; + + my $biblionumber = $args->{biblionumber}; + + my $biblio = Koha::Biblios->find($biblionumber); + unless ($biblio) { + return $c->$cb({error => "Biblio not found"}, 404); + } + + my $success; + my $body = $c->req->body; + my $record = eval {MARC::Record::new_from_xml( $body, "utf8", '')}; + if ($@) { + return $c->$cb({error => $@}, 400); + } else { + $success = &ModBiblio($record, $biblionumber, ''); + } + if ($success) { + $c->res->headers->location($c->url_for('/api/v1/biblios/')->to_abs . $biblionumber); + return $c->$cb({biblio => Koha::Biblios->find($biblionumber)->unblessed}, 200); + } else { + return $c->$cb({error => "unable to update record"}, 400); + } +} + +sub delete { + my ($c, $args, $cb) = @_; + + my $biblio = Koha::Biblios->find($args->{biblionumber}); + unless ($biblio) { + return $c->$cb({error => "Biblio not found"}, 404); + } + my @items = $biblio->items; + # Delete items first + my @item_errors = (); + foreach my $item (@items) { + my $res = $item->delete; + unless ($res eq 1) { + push @item_errors, $item->unblessed->{itemnumber}; + } + } + my $res = $biblio->delete; + if ($res eq '1') { + return $c->$cb({}, 200); + } elsif ($res eq '-1') { + return $c->$cb({error => "Not found. Error code: " . $res, items => @item_errors}, 404); + } else { + return $c->$cb({error => "Error code: " . $res, items => @item_errors}, 400); + } +} + +1; diff --git a/api/v1/swagger/definitions.json b/api/v1/swagger/definitions.json index a17c793..76c1484 100644 --- a/api/v1/swagger/definitions.json +++ b/api/v1/swagger/definitions.json @@ -13,5 +13,11 @@ }, "error": { "$ref": "definitions/error.json" + }, + "biblios": { + "$ref": "definitions/biblios.json" + }, + "biblio": { + "$ref": "definitions/biblio.json" } } diff --git a/api/v1/swagger/definitions/biblio.json b/api/v1/swagger/definitions/biblio.json new file mode 100644 index 0000000..db2f40b --- /dev/null +++ b/api/v1/swagger/definitions/biblio.json @@ -0,0 +1,52 @@ +{ + "type": "object", + "properties": { + "biblionumber": { + "$ref": "../x-primitives.json#/biblionumber" + }, + "author": { + "type": ["string", "null"], + "description": "statement of responsibility from MARC record (100$a in MARC21)" + }, + "title": { + "type": ["string", "null"], + "description": "title (without the subtitle) from the MARC record (245$a in MARC21)" + }, + "unititle": { + "type": ["string", "null"], + "description": "uniform title (without the subtitle) from the MARC record (240$a in MARC21)" + }, + "notes": { + "type": ["string", "null"], + "description": "values from the general notes field in the MARC record (500$a in MARC21) split by bar (|)" + }, + "serial": { + "type": ["string", "null"], + "description": "Boolean indicating whether biblio is for a serial" + }, + "seriestitle": { + "type": ["string", "null"], + "description": "" + }, + "copyrightdate": { + "type": ["string", "null"], + "description": "publication or copyright date from the MARC record" + }, + "timestamp": { + "type": ["string", "null"], + "description": "date and time this record was last touched" + }, + "datecreated": { + "type": ["string", "null"], + "description": "the date this record was added to Koha" + }, + "abstract": { + "type": ["string", "null"], + "description": "summary from the MARC record (520$a in MARC21)" + }, + "frameworkcode": { + "type": ["string", "null"], + "description": "framework used in cataloging this record" + } + } +} diff --git a/api/v1/swagger/definitions/biblios.json b/api/v1/swagger/definitions/biblios.json new file mode 100644 index 0000000..b0600e8 --- /dev/null +++ b/api/v1/swagger/definitions/biblios.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "biblio.json" } +} diff --git a/api/v1/swagger/parameters.json b/api/v1/swagger/parameters.json index 87380a8..1a61183 100644 --- a/api/v1/swagger/parameters.json +++ b/api/v1/swagger/parameters.json @@ -19,5 +19,8 @@ }, "sessionidBodyParam": { "$ref": "parameters/auth.json#/sessionidBodyParam" + }, + "biblionumberPathParam": { + "$ref": "parameters/biblio.json#/biblionumberPathParam" } } diff --git a/api/v1/swagger/parameters/biblio.json b/api/v1/swagger/parameters/biblio.json new file mode 100644 index 0000000..1d1316a --- /dev/null +++ b/api/v1/swagger/parameters/biblio.json @@ -0,0 +1,9 @@ +{ + "biblionumberPathParam": { + "name": "biblionumber", + "in": "path", + "description": "Internal biblio identifier", + "required": true, + "type": "integer" + } +} diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json index 2e8fd38..99f55b2 100644 --- a/api/v1/swagger/paths.json +++ b/api/v1/swagger/paths.json @@ -13,5 +13,17 @@ }, "/patrons/{borrowernumber}": { "$ref": "paths/patrons.json#/~1patrons~1{borrowernumber}" + }, + "/biblios": { + "$ref": "paths/biblios.json#/~1biblios" + }, + "/biblios/{biblionumber}": { + "$ref": "paths/biblios.json#/~1biblios~1{biblionumber}" + }, + "/biblios/{biblionumber}/items": { + "$ref": "paths/biblios.json#/~1biblios~1{biblionumber}~1items" + }, + "/biblios/{biblionumber}/expanded": { + "$ref": "paths/biblios.json#/~1biblios~1{biblionumber}~1expanded" } } diff --git a/api/v1/swagger/paths/biblios.json b/api/v1/swagger/paths/biblios.json new file mode 100644 index 0000000..d58f205 --- /dev/null +++ b/api/v1/swagger/paths/biblios.json @@ -0,0 +1,153 @@ +{ + "/biblios/{biblionumber}": { + "get": { + "operationId": "getBiblio", + "tags": ["biblios"], + "parameters": [ + { "$ref": "../parameters.json#/biblionumberPathParam" } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A biblio record", + "schema": { "$ref": "../definitions.json#/biblio" } + + }, + "404": { + "description": "Biblio not found", + "schema": { + "$ref": "../definitions.json#/error" + } + } + } + }, + "put": { + "operationId": "updateBiblio", + "tags": ["biblios"], + "parameters": [ + { "$ref": "../parameters.json#/biblionumberPathParam" } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "An updated biblio record" + }, + "400": { + "description": "Biblio update failed", + "schema": { "$ref": "../definitions.json#/error" } + }, + "404": { + "description": "Biblio not found", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "editcatalogue": "1" + } + } + }, + "delete": { + "operationId": "deleteBiblio", + "tags": ["biblios"], + "parameters": [ + { "$ref": "../parameters.json#/biblionumberPathParam" } + ], + "produces": ["application/json"], + "responses": { + "200": { + "description": "Biblio record deleted successfully", + "schema": { + "type": "object" + } + }, + "400": { + "description": "Biblio deletion failed", + "schema": { "$ref": "../definitions.json#/error" } + }, + "404": { + "description": "Biblio not found", + "schema": { "$ref": "../definitions.json#/error" } + } + }, + "x-koha-authorization": { + "permissions": { + "editcatalogue": "1" + } + } + } + }, + "/biblios/{biblionumber}/items": { + "get": { + "operationId": "getitemsByBiblio", + "tags": ["biblios", "items"], + "parameters": [ + { "$ref": "../parameters.json#/biblionumberPathParam" } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A biblio record with items" + }, + "404": { + "description": "Biblio not found", + "schema": { + "$ref": "../definitions.json#/error" + } + } + } + } + }, + "/biblios/{biblionumber}/expanded": { + "get": { + "operationId": "getexpandedByBiblio", + "tags": ["biblios", "items", "item status"], + "parameters": [ + { "$ref": "../parameters.json#/biblionumberPathParam" } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A biblio record with items and item statuses" + }, + "404": { + "description": "Biblio not found", + "schema": { + "$ref": "../definitions.json#/error" + } + } + } + } + }, + "/biblios": { + "post": { + "operationId": "addBiblio", + "tags": ["biblios"], + "produces": ["application/json"], + "responses": { + "201": { + "description": "A new biblio record" + }, + "400": { + "description": "Unable to create biblio record", + "schema": { "$ref": "../definitions.json#/error" } + } + } + }, + "x-koha-authorization": { + "permissions": { + "editcatalogue": "1" + } + } + } +} diff --git a/t/db_dependent/api/v1/biblios.t b/t/db_dependent/api/v1/biblios.t new file mode 100644 index 0000000..08b9b7d --- /dev/null +++ b/t/db_dependent/api/v1/biblios.t @@ -0,0 +1,90 @@ +#!/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 t::lib::TestBuilder; + +use Test::More tests => 3; +use Test::Mojo; +use Data::Dumper; +use C4::Auth; +use C4::Context; +use Koha::Database; +use MARC::File::XML ( BinaryEncoding => 'utf8', RecordFormat => 'UNIMARC' ); + +BEGIN { + use_ok('Koha::Biblios'); +} + +my $schema = Koha::Database->schema; +my $dbh = C4::Context->dbh; +my $builder = t::lib::TestBuilder->new; + +$ENV{REMOTE_ADDR} = '127.0.0.1'; +my $t = Test::Mojo->new('Koha::REST::V1'); + +$schema->storage->txn_begin; + +my $file = MARC::File::XML->in( 't/db_dependent/Record/testrecords/marcxml_utf8.xml' ); +my $record = $file->next(); +my ( $biblionumber, $itemnumber ); + +my $librarian = $builder->build({ + source => "Borrower", + value => { + categorycode => 'S', + branchcode => 'NPL', + flags => 1, # editcatalogue + }, +}); + +my $session = C4::Auth::get_session(''); +$session->param('number', $librarian->{ borrowernumber }); +$session->param('id', $librarian->{ userid }); +$session->param('ip', '127.0.0.1'); +$session->param('lasttime', time()); +$session->flush; + +subtest 'Create biblio' => sub { + plan tests => 5; + + my $tx = $t->ua->build_tx(POST => '/api/v1/biblios' => $record->as_xml()); + $tx->req->env({REMOTE_ADDR => '127.0.0.1'}); + $tx->req->cookies({name => 'CGISESSID', value => $session->id}); + $t->request_ok($tx) + ->status_is(201); + $biblionumber = $tx->res->json->{biblionumber}; + $itemnumber = $tx->res->json->{items}; + + $t->json_is('/biblionumber' => $biblionumber) + ->json_is('/items' => $itemnumber) + ->header_like(Location => qr/$biblionumber/, 'Location header contains biblionumber'); +}; + +subtest 'Delete biblio' => sub { + plan tests => 2; + + my $tx = $t->ua->build_tx(DELETE => "/api/v1/biblios/$biblionumber"); + $tx->req->env({REMOTE_ADDR => '127.0.0.1'}); + $tx->req->cookies({name => 'CGISESSID', value => $session->id}); + $t->request_ok($tx) + ->status_is(200); +}; + + +$schema->storage->txn_rollback; -- 2.1.4