Bug 16825: Add API route for getting an item
authorLari Taskula <larit@student.uef.fi>
Mon, 27 Jun 2016 13:03:49 +0000 (16:03 +0300)
committerMartin Renvoize <martin.renvoize@ptfs-europe.com>
Thu, 12 Sep 2019 15:41:09 +0000 (16:41 +0100)
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>

Koha/REST/V1/Items.pm [new file with mode: 0644]
api/v1/swagger/definitions.json
api/v1/swagger/definitions/item.json [new file with mode: 0644]
api/v1/swagger/parameters.json
api/v1/swagger/parameters/item.json [new file with mode: 0644]
api/v1/swagger/paths.json
api/v1/swagger/paths/items.json [new file with mode: 0644]
t/db_dependent/api/v1/items.t [new file with mode: 0644]

diff --git a/Koha/REST/V1/Items.pm b/Koha/REST/V1/Items.pm
new file mode 100644 (file)
index 0000000..d93af2e
--- /dev/null
@@ -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;
index e3001b5..4ac42cf 100644 (file)
@@ -23,6 +23,9 @@
   "library": {
     "$ref": "definitions/library.json"
   },
+  "item": {
+    "$ref": "definitions/item.json"
+  },
   "patron": {
     "$ref": "definitions/patron.json"
   },
diff --git a/api/v1/swagger/definitions/item.json b/api/v1/swagger/definitions/item.json
new file mode 100644 (file)
index 0000000..2643bc1
--- /dev/null
@@ -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"]
+}
index fce13be..b5aa33e 100644 (file)
@@ -17,6 +17,9 @@
   "library_id_pp": {
     "$ref": "parameters/library.json#/library_id_pp"
   },
+  "item_id_pp": {
+    "$ref": "parameters/item.json#/item_id_pp"
+  },
   "vendoridPathParam": {
     "$ref": "parameters/vendor.json#/vendoridPathParam"
   },
diff --git a/api/v1/swagger/parameters/item.json b/api/v1/swagger/parameters/item.json
new file mode 100644 (file)
index 0000000..8e54e9d
--- /dev/null
@@ -0,0 +1,9 @@
+{
+  "item_id_pp": {
+    "name": "item_id",
+    "in": "path",
+    "description": "Internal item identifier",
+    "required": true,
+    "type": "integer"
+  }
+}
index 5d544f5..946de0a 100644 (file)
@@ -47,6 +47,9 @@
   "/checkouts/{checkout_id}/allows_renewal": {
     "$ref": "paths/checkouts.json#/~1checkouts~1{checkout_id}~1allows_renewal"
   },
+  "/items/{item_id}": {
+    "$ref": "paths/items.json#/~1items~1{item_id}"
+  },
   "/patrons": {
     "$ref": "paths/patrons.json#/~1patrons"
   },
diff --git a/api/v1/swagger/paths/items.json b/api/v1/swagger/paths/items.json
new file mode 100644 (file)
index 0000000..28e136d
--- /dev/null
@@ -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"
+        }
+      }
+    }
+  }
+}
diff --git a/t/db_dependent/api/v1/items.t b/t/db_dependent/api/v1/items.t
new file mode 100644 (file)
index 0000000..a3724dc
--- /dev/null
@@ -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;
+};