From 82bdf93ab71f4d16ced622667ef89bafb5008d1e Mon Sep 17 00:00:00 2001 From: Andreas Jonsson Date: Sat, 16 Sep 2023 10:13:15 +0000 Subject: [PATCH] Bug 34008: Add REST endpoint for list of itemtypes Test plan: * Enable the system preference RESTBasicAuth * curl -s --request GET http://kohadev-intra.mydnsname.org:8081/api/v1/itemtypes should give 401 Unauthorized * curl -s -u koha:koha --request GET http://kohadev-intra.mydnsname.org:8081/api/v1/itemtypes should produce JSON-list of itemtypes * curl -s -u koha:koha --header "x-koha-embed: translated_descriptions" --request GET http://kohadev-intra.mydnsname.org:8081/api/v1/itemtypes should include the field translated_descriptions containing the translated descriptions, if any Signed-off-by: David Nind Signed-off-by: Marcel de Rooy [EDIT] perltidy -b t/db_dependent/api/v1/itemtypes.t # Resolve bad score of 44 [EDIT] chmod 755 t/db_dependent/api/v1/itemtypes.t [EDIT] perltidy -b Koha/REST/V1/ItemTypes.pm Lesson: Please run qa tools yourself and adjust accordingly? Edit (tcohen): I restored the item_type_translated_description.yaml file as the entire API was broken because of the lack of it. Signed-off-by: Tomas Cohen Arazi --- Koha/ItemType.pm | 85 +++++++---- Koha/REST/V1/ItemTypes.pm | 48 ++++++ api/v1/swagger/definitions/item_type.yaml | 96 ++++++++++++ .../item_type_translated_description.yaml | 13 ++ api/v1/swagger/paths/item_types.yaml | 59 ++++++++ api/v1/swagger/swagger.yaml | 7 + t/db_dependent/api/v1/item_types.t | 142 ++++++++++++++++++ 7 files changed, 418 insertions(+), 32 deletions(-) create mode 100644 Koha/REST/V1/ItemTypes.pm create mode 100644 api/v1/swagger/definitions/item_type.yaml create mode 100644 api/v1/swagger/definitions/item_type_translated_description.yaml create mode 100644 api/v1/swagger/paths/item_types.yaml create mode 100755 t/db_dependent/api/v1/item_types.t diff --git a/Koha/ItemType.pm b/Koha/ItemType.pm index 18f2544604..200ac166ea 100644 --- a/Koha/ItemType.pm +++ b/Koha/ItemType.pm @@ -17,7 +17,6 @@ package Koha::ItemType; use Modern::Perl; - use C4::Koha qw( getitemtypeimagelocation ); use C4::Languages; use Koha::Caches; @@ -52,8 +51,7 @@ sub store { if ( !$self->in_storage ) { $flush = 1; - } - else { + } else { my $self_from_storage = $self->get_from_storage; $flush = 1 if ( $self_from_storage->description ne $self->description ); } @@ -96,22 +94,25 @@ sub image_location { sub translated_description { my ( $self, $lang ) = @_; if ( my $translated_description = eval { $self->get_column('translated_description') } ) { + # If the value has already been fetched (eg. from sarch_with_localization), # do not search for it again # Note: This is a bit hacky but should be fast return $translated_description - ? $translated_description - : $self->description; + ? $translated_description + : $self->description; } $lang ||= C4::Languages::getlanguage; - my $translated_description = Koha::Localizations->search({ - code => $self->itemtype, - entity => 'itemtypes', - lang => $lang - })->next; + my $translated_description = Koha::Localizations->search( + { + code => $self->itemtype, + entity => 'itemtypes', + lang => $lang + } + )->next; return $translated_description - ? $translated_description->translation - : $self->description; + ? $translated_description->translation + : $self->description; } =head3 translated_descriptions @@ -119,18 +120,21 @@ sub translated_description { =cut sub translated_descriptions { - my ( $self ) = @_; + my ($self) = @_; my @translated_descriptions = Koha::Localizations->search( - { entity => 'itemtypes', + { + entity => 'itemtypes', code => $self->itemtype, } )->as_list; - return [ map { - { - lang => $_->lang, - translation => $_->translation, - } - } @translated_descriptions ]; + return [ + map { + { + lang => $_->lang, + translation => $_->translation, + } + } @translated_descriptions + ]; } =head3 can_be_deleted @@ -142,8 +146,8 @@ Counts up the number of biblioitems and items with itemtype (code) and hands bac =cut sub can_be_deleted { - my ($self) = @_; - my $nb_items = Koha::Items->search( { itype => $self->itemtype } )->count; + my ($self) = @_; + my $nb_items = Koha::Items->search( { itype => $self->itemtype } )->count; my $nb_biblioitems = Koha::Biblioitems->search( { itemtype => $self->itemtype } )->count; return $nb_items + $nb_biblioitems == 0 ? 1 : 0; } @@ -162,10 +166,12 @@ sub may_article_request { my $itemtype = $self->itemtype; my $category = $params->{categorycode}; - my $guess = Koha::CirculationRules->guess_article_requestable_itemtypes({ - $category ? ( categorycode => $category ) : (), - }); - return ( $guess->{ $itemtype // q{} } || $guess->{ '*' } ) ? 1 : q{}; + my $guess = Koha::CirculationRules->guess_article_requestable_itemtypes( + { + $category ? ( categorycode => $category ) : (), + } + ); + return ( $guess->{ $itemtype // q{} } || $guess->{'*'} ) ? 1 : q{}; } =head3 _library_limits @@ -176,8 +182,8 @@ sub may_article_request { sub _library_limits { return { - class => "ItemtypesBranch", - id => "itemtype", + class => "ItemtypesBranch", + id => "itemtype", library => "branchcode", }; } @@ -189,10 +195,10 @@ sub _library_limits { =cut sub parent { - my ( $self ) = @_; + my ($self) = @_; my $parent_rs = $self->_result->parent_type; return unless $parent_rs; - return Koha::ItemType->_new_from_dbic( $parent_rs ); + return Koha::ItemType->_new_from_dbic($parent_rs); } @@ -203,8 +209,23 @@ sub parent { =cut sub children_with_localization { - my ( $self ) = @_; - return Koha::ItemTypes->search_with_localization({ parent_type => $self->itemtype }); + my ($self) = @_; + return Koha::ItemTypes->search_with_localization( { parent_type => $self->itemtype } ); +} + +=head3 to_api_mapping + +This method returns the mapping for representing a Koha::ItemType object +on the API. + +=cut + +sub to_api_mapping { + return { 'itemtype' => 'item_type' }; +} + +sub from_api_mapping { + return { 'item_type' => 'itemtype' }; } =head3 type diff --git a/Koha/REST/V1/ItemTypes.pm b/Koha/REST/V1/ItemTypes.pm new file mode 100644 index 0000000000..91de0c4d0f --- /dev/null +++ b/Koha/REST/V1/ItemTypes.pm @@ -0,0 +1,48 @@ +package Koha::REST::V1::ItemTypes; + +# 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, see . + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Controller'; + +use Koha::ItemTypes; + +use Try::Tiny; + +=head1 API + +=head2 Methods + +=head3 list + +=cut + +sub list { + my $c = shift->openapi->valid_input or return; + + return try { + my $itemtypes_set = Koha::ItemTypes->new; + my $itemtypes = $c->objects->search( $itemtypes_set ); + + return $c->render( status => 200, openapi => $itemtypes ); + } catch { + $c->unhandled_exception($_); + }; + +} + +1; diff --git a/api/v1/swagger/definitions/item_type.yaml b/api/v1/swagger/definitions/item_type.yaml new file mode 100644 index 0000000000..6023869aa4 --- /dev/null +++ b/api/v1/swagger/definitions/item_type.yaml @@ -0,0 +1,96 @@ +--- +type: object +properties: + item_type: + type: string + description: Unique key, a code associated with the item type + readOnly: true + parent_type: + description: Unique key, a code associated with the parent item type + type: + - string + - "null" + description: + description: A plain text explanation of the item type + type: string + rentalcharge: + description: The amount charged when this item is checked out/issued + type: + - number + - "null" + rentalcharge_daily: + description: The amount charged for each day between checkout date and due date + type: + - number + - "null" + rentalcharge_daily_calendar: + description: Controls if the daily rental fee is calculated directly or using finesCalendar + type: boolean + rentalcharge_hourly: + description: The amount charged for each hour between checkout date and due date + type: + - number + - "null" + rentalcharge_hourly_calendar: + description: Controls if the hourly rental fee is calculated directly or using finesCalendar + type: boolean + defaultreplacecost: + description: Default replacement cost + type: + - number + - "null" + processfee: + description: Default text be recorded in the column note when the processing fee is applied + type: + - number + - "null" + notforloan: + description: Controls if the item is available for loan + type: + - boolean + - "null" + imageurl: + description: URL for the item type icon + type: + - string + - "null" + summary: + description: Information from the summary field, may include HTML + type: + - string + - "null" + checkinmsg: + description: Message that is displayed when an item with the given item type is checked in + type: + - string + - "null" + checkinmsgtype: + description: Type (CSS class) for the checkinmsg, can be 'alert' or 'message' + type: + - string + - "null" + sip_media_type: + description: SIP2 protocol media type for this item type + type: + - string + - "null" + hideinopac: + description: Hide the item type from the search options in OPAC + type: boolean + searchcategory: + description: Group this item type with others with the same value on OPAC search options + type: + - string + - "null" + automatic_checkin: + description: Controls if automatic checkin is enabled for items of this type + type: boolean + translated_descriptions: + description: Translations of description plain text + type: array + items: + $ref: "item_type_translated_description.yaml" + +additionalProperties: false +required: + - item_type diff --git a/api/v1/swagger/definitions/item_type_translated_description.yaml b/api/v1/swagger/definitions/item_type_translated_description.yaml new file mode 100644 index 0000000000..8cb95287a6 --- /dev/null +++ b/api/v1/swagger/definitions/item_type_translated_description.yaml @@ -0,0 +1,13 @@ +--- +type: object +properties: + lang: + description: Language identifier + type: string + translation: + description: Translated plain text + type: string +additionalProperties: false +required: + - lang + - translation diff --git a/api/v1/swagger/paths/item_types.yaml b/api/v1/swagger/paths/item_types.yaml new file mode 100644 index 0000000000..d359a40d62 --- /dev/null +++ b/api/v1/swagger/paths/item_types.yaml @@ -0,0 +1,59 @@ +--- +/item_types: + get: + x-mojo-to: ItemTypes#list + operationId: listItemTypes + tags: + - item_types + summary: List item types + produces: + - application/json + parameters: + - name: x-koha-embed + in: header + required: false + description: Embed list sent as a request header + type: array + items: + type: string + enum: + - translated_descriptions + collectionFormat: csv + - $ref: "../swagger.yaml#/parameters/match" + - $ref: "../swagger.yaml#/parameters/order_by" + - $ref: "../swagger.yaml#/parameters/page" + - $ref: "../swagger.yaml#/parameters/per_page" + - $ref: "../swagger.yaml#/parameters/q_param" + - $ref: "../swagger.yaml#/parameters/q_body" + responses: + 200: + description: A list of item types + schema: + items: + $ref: "../swagger.yaml#/definitions/item_type" + type: array + 400: + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + 403: + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + 404: + description: Resource not found + schema: + $ref: "../swagger.yaml#/definitions/error" + 500: + description: |- + Internal server error. Possible `error_code` attribute values: + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + 503: + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + catalogue: 1 diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index 7cebf43089..3d056e2700 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -110,6 +110,8 @@ definitions: $ref: ./definitions/item.yaml item_group: $ref: ./definitions/item_group.yaml + item_type: + $ref: ./definitions/item_type.yaml job: $ref: ./definitions/job.yaml library: @@ -367,6 +369,8 @@ paths: $ref: ./paths/import_batch_profiles.yaml#/~1import_batch_profiles "/import_batch_profiles/{import_batch_profile_id}": $ref: "./paths/import_batch_profiles.yaml#/~1import_batch_profiles~1{import_batch_profile_id}" + /item_types: + $ref: ./paths/item_types.yaml#/~1item_types /items: $ref: ./paths/items.yaml#/~1items "/items/{item_id}": @@ -1062,6 +1066,9 @@ tags: - description: "Manage items\n" name: items x-displayName: Items + - description: "Manage item types\n" + name: item_types + x-displayName: Item Types - description: "Manage jobs\n" name: jobs x-displayName: Jobs diff --git a/t/db_dependent/api/v1/item_types.t b/t/db_dependent/api/v1/item_types.t new file mode 100755 index 0000000000..d45208ccfb --- /dev/null +++ b/t/db_dependent/api/v1/item_types.t @@ -0,0 +1,142 @@ +#!/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, see . + +use Modern::Perl; + +use Test::More tests => 1; +use Test::Mojo; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +use Koha::ItemTypes; +use Koha::Database; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +my $t = Test::Mojo->new('Koha::REST::V1'); +t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); + +subtest 'list_item_types() tests' => sub { + + plan tests => 12; + + $schema->storage->txn_begin; + + my $item_type = $builder->build_object( + { + class => 'Koha::ItemTypes', + value => { + itemtype => 'TEST_IT', + parent_type => undef, + description => 'Test item type', + rentalcharge => 100.0, + rentalcharge_daily => 50., + rentalcharge_daily_calendar => 0, + rentalcharge_hourly => 0.60, + rentalcharge_hourly_calendar => 1, + defaultreplacecost => 1000.0, + processfee => 20.0, + notforloan => 0, + imageurl => 'https://upload.wikimedia.org/wikipedia/commons/1/1f/202208_test-tube-4.svg', + summary => 'An item type for testing', + checkinmsg => 'Checking in test', + checkinmsgtype => 'message', + sip_media_type => 'spt', + hideinopac => 1, + searchcategory => 'test search category', + automatic_checkin => 0, + } + } + ); + + my $en = $builder->build_object( + { + class => 'Koha::Localizations', + value => { + entity => 'itemtypes', + code => 'TEST_IT', + lang => 'en', + translation => 'English word "test"', + } + } + ); + my $sv = $builder->build_object( + { + class => 'Koha::Localizations', + value => { + entity => 'itemtypes', + code => 'TEST_IT', + lang => 'sv_SE', + translation => 'Swedish word "test"', + } + } + ); + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 2**2 } # catalogue flag = 2 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + ## Authorized user tests + # No category, 404 expected + $t->get_ok("//$userid:$password@/api/v1/item_types")->status_is(200)->json_has('/0'); + + for my $json ( @{ $t->tx->res->json } ) { + if ( $json->{item_type} eq 'TEST_IT' ) { + is( $json->{description}, 'Test item type' ); + ok( !exists $json->{translated_descriptions} ); + } + } + + $t->get_ok( "//$userid:$password@/api/v1/item_types" => { 'x-koha-embed' => 'translated_descriptions' } ) + ->status_is(200)->json_has('/0'); + + for my $json ( @{ $t->tx->res->json } ) { + if ( $json->{item_type} eq 'TEST_IT' ) { + is( $json->{description}, 'Test item type' ); + is_deeply( + $json->{translated_descriptions}, + [ + { lang => 'en', translation => 'English word "test"' }, + { lang => 'sv_SE', translation => 'Swedish word "test"' }, + ] + ); + } + } + + # Unauthorized access + $t->get_ok("//$unauth_userid:$password@/api/v1/item_types")->status_is(403); + + $schema->storage->txn_rollback; +}; -- 2.39.5