Browse Source
GET /api/v1/items/{item_id} Gets one Item This patch adds route to get one item from koha.items table. To test: 1. Apply patch 2. Open a browser tab on Koha staff and log in (to create CGISESSID cookie). 3. Send GET request to http://yourlibrary/api/v1/items/YYY where YYY is an existing itemnumber. 4. Make sure the returned data is correct. 5. Run unit tests in t/db_dependent/api/v1/items.t Sponsored-by: Koha-Suomi Oy Signed-off-by: Josef Moravec <josef.moravec@gmail.com> Signed-off-by: Johanna Raisa <johanna.raisa@gmail.com> Signed-off-by: Michal Denar <black23@gmail.com> Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com> Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>remotes/origin/19.11.x
Lari Taskula
8 years ago
committed by
Martin Renvoize
8 changed files with 550 additions and 0 deletions
@ -0,0 +1,216 @@ |
|||
package Koha::REST::V1::Items; |
|||
|
|||
# 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 Mojo::JSON; |
|||
|
|||
use C4::Auth qw( haspermission ); |
|||
use C4::Items qw( GetHiddenItemnumbers ); |
|||
|
|||
use Koha::Items; |
|||
|
|||
use Try::Tiny; |
|||
|
|||
sub get { |
|||
my $c = shift->openapi->valid_input or return; |
|||
|
|||
my $item; |
|||
try { |
|||
$item = Koha::Items->find($c->validation->param('item_id')); |
|||
return $c->render( status => 200, openapi => _to_api( $item->TO_JSON ) ); |
|||
} |
|||
catch { |
|||
unless ( defined $item ) { |
|||
return $c->render( status => 404, |
|||
openapi => { error => 'Item not found'} ); |
|||
} |
|||
if ( $_->isa('DBIx::Class::Exception') ) { |
|||
return $c->render( status => 500, |
|||
openapi => { error => $_->{msg} } ); |
|||
} |
|||
else { |
|||
return $c->render( status => 500, |
|||
openapi => { error => "Something went wrong, check the logs."} ); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
=head3 _to_api |
|||
|
|||
Helper function that maps unblessed Koha::Hold objects into REST api |
|||
attribute names. |
|||
|
|||
=cut |
|||
|
|||
sub _to_api { |
|||
my $item = shift; |
|||
|
|||
# Rename attributes |
|||
foreach my $column ( keys %{ $Koha::REST::V1::Items::to_api_mapping } ) { |
|||
my $mapped_column = $Koha::REST::V1::Items::to_api_mapping->{$column}; |
|||
if ( exists $item->{ $column } |
|||
&& defined $mapped_column ) |
|||
{ |
|||
# key != undef |
|||
$item->{ $mapped_column } = delete $item->{ $column }; |
|||
} |
|||
elsif ( exists $item->{ $column } |
|||
&& !defined $mapped_column ) |
|||
{ |
|||
# key == undef |
|||
delete $item->{ $column }; |
|||
} |
|||
} |
|||
|
|||
return $item; |
|||
} |
|||
|
|||
=head3 _to_model |
|||
|
|||
Helper function that maps REST api objects into Koha::Hold |
|||
attribute names. |
|||
|
|||
=cut |
|||
|
|||
sub _to_model { |
|||
my $item = shift; |
|||
|
|||
foreach my $attribute ( keys %{ $Koha::REST::V1::Items::to_model_mapping } ) { |
|||
my $mapped_attribute = $Koha::REST::V1::Items::to_model_mapping->{$attribute}; |
|||
if ( exists $item->{ $attribute } |
|||
&& defined $mapped_attribute ) |
|||
{ |
|||
# key => !undef |
|||
$item->{ $mapped_attribute } = delete $item->{ $attribute }; |
|||
} |
|||
elsif ( exists $item->{ $attribute } |
|||
&& !defined $mapped_attribute ) |
|||
{ |
|||
# key => undef / to be deleted |
|||
delete $item->{ $attribute }; |
|||
} |
|||
} |
|||
|
|||
return $item; |
|||
} |
|||
|
|||
=head2 Global variables |
|||
|
|||
=head3 $to_api_mapping |
|||
|
|||
=cut |
|||
|
|||
our $to_api_mapping = { |
|||
itemnumber => 'item_id', |
|||
biblionumber => 'biblio_id', |
|||
biblioitemnumber => undef, |
|||
barcode => 'external_id', |
|||
dateaccessioned => 'acquisition_date', |
|||
booksellerid => 'acquisition_source', |
|||
homebranch => 'home_library_id', |
|||
price => 'purchase_price', |
|||
replacementprice => 'replacement_price', |
|||
replacementpricedate => 'replacement_price_date', |
|||
datelastborrowed => 'last_checkout_date', |
|||
datelastseen => 'last_seen_date', |
|||
stack => undef, |
|||
notforloan => 'not_for_loan_status', |
|||
damaged => 'damaged_status', |
|||
damaged_on => 'damaged_date', |
|||
itemlost => 'lost_status', |
|||
itemlost_on => 'lost_date', |
|||
withdrawn => 'withdrawn', |
|||
withdrawn_on => 'withdrawn_date', |
|||
itemcallnumber => 'callnumber', |
|||
coded_location_qualifier => 'coded_location_qualifier', |
|||
issues => 'checkouts_count', |
|||
renewals => 'renewals_count', |
|||
reserves => 'holds_count', |
|||
restricted => 'restricted_status', |
|||
itemnotes => 'public_notes', |
|||
itemnotes_nonpublic => 'internal_notes', |
|||
holdingbranch => 'holding_library_id', |
|||
paidfor => undef, |
|||
timestamp => 'timestamp', |
|||
location => 'location', |
|||
permanent_location => 'permanent_location', |
|||
onloan => 'checked_out_date', |
|||
cn_source => 'call_number_source', |
|||
cn_sort => 'call_number_sort', |
|||
ccode => 'collection_code', |
|||
materials => 'materials_notes', |
|||
uri => 'uri', |
|||
itype => 'item_type', |
|||
more_subfields_xml => 'extended_subfields', |
|||
enumchron => 'serial_issue_number', |
|||
copynumber => 'copy_number', |
|||
stocknumber => 'inventory_number', |
|||
new_status => 'new_status' |
|||
}; |
|||
|
|||
=head3 $to_model_mapping |
|||
|
|||
=cut |
|||
|
|||
our $to_model_mapping = { |
|||
item_id => 'itemnumber', |
|||
biblio_id => 'biblionumber', |
|||
external_id => 'barcode', |
|||
acquisition_date => 'dateaccessioned', |
|||
acquisition_source => 'booksellerid', |
|||
home_library_id => 'homebranch', |
|||
purchase_price => 'price', |
|||
replacement_price => 'replacementprice', |
|||
replacement_price_date => 'replacementpricedate', |
|||
last_checkout_date => 'datelastborrowed', |
|||
last_seen_date => 'datelastseen', |
|||
not_for_loan_status => 'notforloan', |
|||
damaged_status => 'damaged', |
|||
damaged_date => 'damaged_on', |
|||
lost_status => 'itemlost', |
|||
lost_date => 'itemlost_on', |
|||
withdrawn => 'withdrawn', |
|||
withdrawn_date => 'withdrawn_on', |
|||
callnumber => 'itemcallnumber', |
|||
coded_location_qualifier => 'coded_location_qualifier', |
|||
checkouts_count => 'issues', |
|||
renewals_count => 'renewals', |
|||
holds_count => 'reserves', |
|||
restricted_status => 'restricted', |
|||
public_notes => 'itemnotes', |
|||
internal_notes => 'itemnotes_nonpublic', |
|||
holding_library_id => 'holdingbranch', |
|||
timestamp => 'timestamp', |
|||
location => 'location', |
|||
permanent_location => 'permanent_location', |
|||
checked_out_date => 'onloan', |
|||
call_number_source => 'cn_source', |
|||
call_number_sort => 'cn_sort', |
|||
collection_code => 'ccode', |
|||
materials_notes => 'materials', |
|||
uri => 'uri', |
|||
item_type => 'itype', |
|||
extended_subfields => 'more_subfields_xml', |
|||
serial_issue_number => 'enumchron', |
|||
copy_number => 'copynumber', |
|||
inventory_number => 'stocknumber', |
|||
new_status => 'new_status' |
|||
}; |
|||
|
|||
1; |
@ -0,0 +1,183 @@ |
|||
{ |
|||
"type": "object", |
|||
"properties": { |
|||
"item_id": { |
|||
"type": "integer", |
|||
"description": "Internal item identifier" |
|||
}, |
|||
"biblio_id": { |
|||
"type": "integer", |
|||
"description": "Internal identifier for the parent bibliographic record" |
|||
}, |
|||
"external_id": { |
|||
"type": ["string", "null"], |
|||
"description": "The item's barcode" |
|||
}, |
|||
"acquisition_date": { |
|||
"type": ["string", "null"], |
|||
"format": "date", |
|||
"description": "The date the item was acquired" |
|||
}, |
|||
"acquisition_source": { |
|||
"type": ["string", "null"], |
|||
"description": "Information about the acquisition source (it is not really a vendor id)" |
|||
}, |
|||
"home_library_id": { |
|||
"type": ["string", "null"], |
|||
"description": "Internal library id for the library the item belongs to" |
|||
}, |
|||
"purchase_price": { |
|||
"type": ["number", "null"], |
|||
"description": "Purchase price" |
|||
}, |
|||
"replacement_price": { |
|||
"type": ["number", "null"], |
|||
"description": "Cost the library charges to replace the item (e.g. if lost)" |
|||
}, |
|||
"replacement_price_date": { |
|||
"type": ["string", "null"], |
|||
"format": "date", |
|||
"description": "The date the replacement price is effective from" |
|||
}, |
|||
"last_checkout_date": { |
|||
"type": ["string", "null"], |
|||
"format": "date", |
|||
"description": "The date the item was last checked out" |
|||
}, |
|||
"last_seen_date": { |
|||
"type": ["string", "null"], |
|||
"format": "date", |
|||
"description": "The date the item barcode was last scanned" |
|||
}, |
|||
"not_for_loan_status": { |
|||
"type": "integer", |
|||
"description": "Authorized value defining why this item is not for loan" |
|||
}, |
|||
"damaged_status": { |
|||
"type": "integer", |
|||
"description": "Authorized value defining this item as damaged" |
|||
}, |
|||
"damaged_date": { |
|||
"type": ["string", "null"], |
|||
"description": "The date and time an item was last marked as damaged, NULL if not damaged" |
|||
}, |
|||
"lost_status": { |
|||
"type": "integer", |
|||
"description": "Authorized value defining this item as lost" |
|||
}, |
|||
"lost_date": { |
|||
"type": ["string", "null"], |
|||
"format": "date-time", |
|||
"description": "The date and time an item was last marked as lost, NULL if not lost" |
|||
}, |
|||
"withdrawn": { |
|||
"type": "integer", |
|||
"description": "Authorized value defining this item as withdrawn" |
|||
}, |
|||
"withdrawn_date": { |
|||
"type": ["string", "null"], |
|||
"format": "date-time", |
|||
"description": "The date and time an item was last marked as withdrawn, NULL if not withdrawn" |
|||
}, |
|||
"callnumber": { |
|||
"type": ["string", "null"], |
|||
"description": "Call number for this item" |
|||
}, |
|||
"coded_location_qualifier": { |
|||
"type": ["string", "null"], |
|||
"description": "Coded location qualifier" |
|||
}, |
|||
"checkouts_count": { |
|||
"type": ["integer", "null"], |
|||
"description": "Number of times this item has been checked out/issued" |
|||
}, |
|||
"renewals_count": { |
|||
"type": ["integer", "null"], |
|||
"description": "Number of times this item has been renewed" |
|||
}, |
|||
"holds_count": { |
|||
"type": ["integer", "null"], |
|||
"description": "Number of times this item has been placed on hold/reserved" |
|||
}, |
|||
"restricted_status": { |
|||
"type": ["integer", "null"], |
|||
"description": "Authorized value defining use restrictions for this item" |
|||
}, |
|||
"public_notes": { |
|||
"type": ["string", "null"], |
|||
"description": "Public notes on this item" |
|||
}, |
|||
"internal_notes": { |
|||
"type": ["string", "null"], |
|||
"description": "Non-public notes on this item" |
|||
}, |
|||
"holding_library_id": { |
|||
"type": ["string", "null"], |
|||
"description": "Library that is currently in possession item" |
|||
}, |
|||
"timestamp": { |
|||
"type": "string", |
|||
"format": "date-time", |
|||
"description": "Date and time this item was last altered" |
|||
}, |
|||
"location": { |
|||
"type": ["string", "null"], |
|||
"description": "Authorized value for the shelving location for this item" |
|||
}, |
|||
"permanent_location": { |
|||
"type": ["string", "null"], |
|||
"description": "Linked to the CART and PROC temporary locations feature, stores the permanent shelving location" |
|||
}, |
|||
"checked_out_date": { |
|||
"type": ["string", "null"], |
|||
"format": "date", |
|||
"description": "Defines if item is checked out (NULL for not checked out, and checkout date for checked out)" |
|||
}, |
|||
"call_number_source": { |
|||
"type": ["string", "null"], |
|||
"description": "Classification source used on this item" |
|||
}, |
|||
"call_number_sort": { |
|||
"type": ["string", "null"], |
|||
"description": "?" |
|||
}, |
|||
"collection_code": { |
|||
"type": ["string", "null"], |
|||
"description": "Authorized value for the collection code associated with this item" |
|||
}, |
|||
"materials_notes": { |
|||
"type": ["string", "null"], |
|||
"description": "Materials specified" |
|||
}, |
|||
"uri": { |
|||
"type": ["string", "null"], |
|||
"description": "URL for the item" |
|||
}, |
|||
"item_type": { |
|||
"type": ["string", "null"], |
|||
"description": "Itemtype defining the type for this item" |
|||
}, |
|||
"extended_subfields": { |
|||
"type": ["string", "null"], |
|||
"description": "Additional 952 subfields in XML format" |
|||
}, |
|||
"serial_issue_number": { |
|||
"type": ["string", "null"], |
|||
"description": "serial enumeration/chronology for the item" |
|||
}, |
|||
"copy_number": { |
|||
"type": ["string", "null"], |
|||
"description": "Copy number" |
|||
}, |
|||
"inventory_number": { |
|||
"type": ["string", "null"], |
|||
"description": "Inventory number" |
|||
}, |
|||
"new_status": { |
|||
"type": ["string", "null"], |
|||
"description": "'new' value, whatever free-text information." |
|||
} |
|||
}, |
|||
"additionalProperties": false, |
|||
"required": ["item_id", "biblio_id", "not_for_loan_status", "damaged_status", "lost_status", "withdrawn"] |
|||
} |
@ -0,0 +1,9 @@ |
|||
{ |
|||
"item_id_pp": { |
|||
"name": "item_id", |
|||
"in": "path", |
|||
"description": "Internal item identifier", |
|||
"required": true, |
|||
"type": "integer" |
|||
} |
|||
} |
@ -0,0 +1,52 @@ |
|||
{ |
|||
"/items/{item_id}": { |
|||
"get": { |
|||
"x-mojo-to": "Items#get", |
|||
"operationId": "getItem", |
|||
"tags": ["items"], |
|||
"parameters": [{ |
|||
"$ref": "../parameters.json#/item_id_pp" |
|||
} |
|||
], |
|||
"consumes": ["application/json"], |
|||
"produces": ["application/json"], |
|||
"responses": { |
|||
"200": { |
|||
"description": "An item", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/item" |
|||
} |
|||
}, |
|||
"400": { |
|||
"description": "Missing or wrong parameters", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
}, |
|||
"404": { |
|||
"description": "Item not found", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
}, |
|||
"500": { |
|||
"description": "Internal server error", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
}, |
|||
"503": { |
|||
"description": "Under maintenance", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
} |
|||
}, |
|||
"x-koha-authorization": { |
|||
"permissions": { |
|||
"catalogue": "1" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,81 @@ |
|||
#!/usr/bin/env perl |
|||
|
|||
# Copyright 2016 Koha-Suomi |
|||
# |
|||
# 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 => 1; |
|||
use Test::Mojo; |
|||
use Test::Warn; |
|||
|
|||
use t::lib::TestBuilder; |
|||
use t::lib::Mocks; |
|||
|
|||
use C4::Auth; |
|||
use Koha::Items; |
|||
use Koha::Database; |
|||
|
|||
my $schema = Koha::Database->new->schema; |
|||
my $builder = t::lib::TestBuilder->new; |
|||
|
|||
t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); |
|||
|
|||
my $t = Test::Mojo->new('Koha::REST::V1'); |
|||
|
|||
subtest 'get() tests' => sub { |
|||
|
|||
plan tests => 9; |
|||
|
|||
$schema->storage->txn_begin; |
|||
|
|||
my $item = $builder->build_object( { class => 'Koha::Items' } ); |
|||
my $patron = $builder->build_object({ |
|||
class => 'Koha::Patrons', |
|||
value => { flags => 4 } |
|||
}); |
|||
|
|||
my $nonprivilegedpatron = $builder->build_object({ |
|||
class => 'Koha::Patrons', |
|||
value => { flags => 0 } |
|||
}); |
|||
|
|||
my $password = 'thePassword123'; |
|||
|
|||
$nonprivilegedpatron->set_password({ password => $password, skip_validation => 1 }); |
|||
my $userid = $nonprivilegedpatron->userid; |
|||
|
|||
$t->get_ok( "//$userid:$password@/api/v1/items/" . $item->itemnumber ) |
|||
->status_is(403) |
|||
->json_is( '/error' => 'Authorization failure. Missing required permission(s).' ); |
|||
|
|||
$patron->set_password({ password => $password, skip_validation => 1 }); |
|||
$userid = $patron->userid; |
|||
|
|||
$t->get_ok( "//$userid:$password@/api/v1/items/" . $item->itemnumber ) |
|||
->status_is( 200, 'SWAGGER3.2.2' ) |
|||
->json_is( '' => Koha::REST::V1::Items::_to_api( $item->TO_JSON ), 'SWAGGER3.3.2' ); |
|||
|
|||
my $non_existent_code = $item->itemnumber; |
|||
$item->delete; |
|||
|
|||
$t->get_ok( "//$userid:$password@/api/v1/items/" . $non_existent_code ) |
|||
->status_is(404) |
|||
->json_is( '/error' => 'Item not found' ); |
|||
|
|||
$schema->storage->txn_rollback; |
|||
}; |
Loading…
Reference in new issue