View | Details | Raw Unified | Return to bug 16825
Collapse All | Expand All

(-)a/Koha/REST/V1/Items.pm (+236 lines)
Line 0 Link Here
1
package Koha::REST::V1::Items;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
6
# terms of the GNU General Public License as published by the Free Software
7
# Foundation; either version 3 of the License, or (at your option) any later
8
# version.
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License along
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18
use Modern::Perl;
19
20
use Mojo::Base 'Mojolicious::Controller';
21
use Mojo::JSON;
22
23
use C4::Auth qw( haspermission );
24
use C4::Items qw( GetHiddenItemnumbers );
25
26
use Koha::Items;
27
28
use Try::Tiny;
29
30
sub get {
31
    my $c = shift->openapi->valid_input or return;
32
33
    my $item;
34
    try {
35
        $item = Koha::Items->find($c->validation->param('item_id'));
36
37
        # Hide non-public itemnotes if user has no staff access
38
        my $user = $c->stash('koha.user');
39
        unless ($user && haspermission($user->userid, 'catalogue')) {
40
            push my @items, $item->unblessed;
41
            my @hiddenitems = C4::Items::GetHiddenItemnumbers( ({items => (\@items)}) );
42
            my %hiddenitems = map { $_ => 1 } @hiddenitems;
43
44
            # Pretend it was not found as it's hidden from OPAC to regular users
45
            if ($hiddenitems{$item->itemnumber}) {
46
                return $c->render( status => 404,
47
                                   openapi => { error => 'Item not found'} ) ;
48
            }
49
50
            $item->set({ itemnotes_nonpublic => undef });
51
        }
52
53
        return $c->render( status => 200, openapi => _to_api( $item->TO_JSON ) );
54
    }
55
    catch {
56
        unless ( defined $item ) {
57
            return $c->render( status => 404,
58
                               openapi => { error => 'Item not found'} );
59
        }
60
        if ( $_->isa('DBIx::Class::Exception') ) {
61
            return $c->render( status  => 500,
62
                               openapi => { error => $_->{msg} } );
63
        }
64
        else {
65
            return $c->render( status => 500,
66
                openapi => { error => "Something went wrong, check the logs."} );
67
        }
68
    };
69
}
70
71
=head3 _to_api
72
73
Helper function that maps unblessed Koha::Hold objects into REST api
74
attribute names.
75
76
=cut
77
78
sub _to_api {
79
    my $item = shift;
80
81
    # Rename attributes
82
    foreach my $column ( keys %{ $Koha::REST::V1::Items::to_api_mapping } ) {
83
        my $mapped_column = $Koha::REST::V1::Items::to_api_mapping->{$column};
84
        if (    exists $item->{ $column }
85
             && defined $mapped_column )
86
        {
87
            # key != undef
88
            $item->{ $mapped_column } = delete $item->{ $column };
89
        }
90
        elsif (    exists $item->{ $column }
91
                && !defined $mapped_column )
92
        {
93
            # key == undef
94
            delete $item->{ $column };
95
        }
96
    }
97
98
    return $item;
99
}
100
101
=head3 _to_model
102
103
Helper function that maps REST api objects into Koha::Hold
104
attribute names.
105
106
=cut
107
108
sub _to_model {
109
    my $item = shift;
110
111
    foreach my $attribute ( keys %{ $Koha::REST::V1::Items::to_model_mapping } ) {
112
        my $mapped_attribute = $Koha::REST::V1::Items::to_model_mapping->{$attribute};
113
        if (    exists $item->{ $attribute }
114
             && defined $mapped_attribute )
115
        {
116
            # key => !undef
117
            $item->{ $mapped_attribute } = delete $item->{ $attribute };
118
        }
119
        elsif (    exists $item->{ $attribute }
120
                && !defined $mapped_attribute )
121
        {
122
            # key => undef / to be deleted
123
            delete $item->{ $attribute };
124
        }
125
    }
126
127
    return $item;
128
}
129
130
=head2 Global variables
131
132
=head3 $to_api_mapping
133
134
=cut
135
136
our $to_api_mapping = {
137
    itemnumber => 'item_id',
138
    biblionumber => 'biblio_id',
139
    biblioitemnumber => 'biblioitem_id',
140
    barcode => 'barcode',
141
    dateaccessioned => 'dateaccessioned',
142
    booksellerid => 'booksellerid',
143
    homebranch => 'homebranch',
144
    price => 'price',
145
    replacementprice => 'replacementprice',
146
    replacementpricedate => 'replacementpricedate',
147
    datelastborrowed => 'datelastborrowed',
148
    datelastseen => 'datelastseen',
149
    stack => 'stack',
150
    notforloan => 'notforloan',
151
    damaged => 'damaged',
152
    damaged_on => 'damaged_on',
153
    itemlost => 'itemlost',
154
    itemlost_on => 'itemlost_on',
155
    withdrawn => 'withdrawn',
156
    withdrawn_on => 'withdrawn_on',
157
    itemcallnumber => 'itemcallnumber',
158
    coded_location_qualifier => 'coded_location_qualifier',
159
    issues => 'issues',
160
    renewals => 'renewals',
161
    reserves => 'reserves',
162
    restricted => 'restricted',
163
    itemnotes => 'itemnotes',
164
    itemnotes_nonpublic => 'itemnotes_nonpublic',
165
    holdingbranch => 'holdingbranch',
166
    paidfor => 'paidfor',
167
    timestamp => 'timestamp',
168
    location => 'location',
169
    permanent_location => 'permanent_location',
170
    onloan => 'onloan',
171
    cn_source => 'cn_source',
172
    cn_sort => 'cn_sort',
173
    ccode => 'ccode',
174
    materials => 'materials',
175
    uri => 'uri',
176
    itype => 'itype',
177
    more_subfields_xml => 'more_subfields_xml',
178
    enumchron => 'enumchron',
179
    copynumber => 'copynumber',
180
    stocknumber => 'stocknumber',
181
    new_status => 'new_status'
182
};
183
184
=head3 $to_model_mapping
185
186
=cut
187
188
our $to_model_mapping = {
189
    item_id => 'itemnumber',
190
    biblio_id => 'biblionumber',
191
    biblioitem_id => 'biblioitemnumber',
192
    barcode => 'barcode',
193
    dateaccessioned => 'dateaccessioned',
194
    booksellerid => 'booksellerid',
195
    homebranch => 'homebranch',
196
    price => 'price',
197
    replacementprice => 'replacementprice',
198
    replacementpricedate => 'replacementpricedate',
199
    datelastborrowed => 'datelastborrowed',
200
    datelastseen => 'datelastseen',
201
    stack => 'stack',
202
    notforloan => 'notforloan',
203
    damaged => 'damaged',
204
    damaged_on => 'damaged_on',
205
    itemlost => 'itemlost',
206
    itemlost_on => 'itemlost_on',
207
    withdrawn => 'withdrawn',
208
    withdrawn_on => 'withdrawn_on',
209
    itemcallnumber => 'itemcallnumber',
210
    coded_location_qualifier => 'coded_location_qualifier',
211
    issues => 'issues',
212
    renewals => 'renewals',
213
    reserves => 'reserves',
214
    restricted => 'restricted',
215
    itemnotes => 'itemnotes',
216
    itemnotes_nonpublic => 'itemnotes_nonpublic',
217
    holdingbranch => 'holdingbranch',
218
    paidfor => 'paidfor',
219
    timestamp => 'timestamp',
220
    location => 'location',
221
    permanent_location => 'permanent_location',
222
    onloan => 'onloan',
223
    cn_source => 'cn_source',
224
    cn_sort => 'cn_sort',
225
    ccode => 'ccode',
226
    materials => 'materials',
227
    uri => 'uri',
228
    itype => 'itype',
229
    more_subfields_xml => 'more_subfields_xml',
230
    enumchron => 'enumchron',
231
    copynumber => 'copynumber',
232
    stocknumber => 'stocknumber',
233
    new_status => 'new_status'
234
};
235
236
1;
(-)a/api/v1/swagger/definitions.json (+3 lines)
Lines 23-28 Link Here
23
  "library": {
23
  "library": {
24
    "$ref": "definitions/library.json"
24
    "$ref": "definitions/library.json"
25
  },
25
  },
26
  "item": {
27
    "$ref": "definitions/item.json"
28
  },
26
  "patron": {
29
  "patron": {
27
    "$ref": "definitions/patron.json"
30
    "$ref": "definitions/patron.json"
28
  },
31
  },
(-)a/api/v1/swagger/definitions/item.json (+192 lines)
Line 0 Link Here
1
{
2
  "type": "object",
3
  "properties": {
4
    "item_id": {
5
      "$ref": "../x-primitives.json#/item_id"
6
    },
7
    "biblio_id": {
8
      "$ref": "../x-primitives.json#/biblio_id"
9
    },
10
    "biblioitem_id": {
11
      "$ref": "../x-primitives.json#/biblioitem_id"
12
    },
13
    "barcode": {
14
      "type": ["string", "null"],
15
      "description": "item barcode"
16
    },
17
    "dateaccessioned": {
18
      "type": ["string", "null"],
19
      "format": "date",
20
      "description": "date the item was acquired or added to Koha"
21
    },
22
    "booksellerid": {
23
      "type": ["string", "null"],
24
      "description": "where the item was purchased"
25
    },
26
    "homebranch": {
27
      "type": ["string", "null"],
28
      "description": "library that owns this item"
29
    },
30
    "price": {
31
      "type": ["number", "null"],
32
      "description": "purchase price"
33
    },
34
    "replacementprice": {
35
      "type": ["number", "null"],
36
      "description": "cost the library charges to replace the item if it has been marked lost"
37
    },
38
    "replacementpricedate": {
39
      "type": ["string", "null"],
40
      "format": "date",
41
      "description": "the date the price is effective from"
42
    },
43
    "datelastborrowed": {
44
      "type": ["string", "null"],
45
      "format": "date",
46
      "description": "the date the item was last checked out/issued"
47
    },
48
    "datelastseen": {
49
      "type": ["string", "null"],
50
      "format": "date",
51
      "description": "the date the item was last see (usually the last time the barcode was scanned or inventory was done)"
52
    },
53
    "stack": {
54
      "type": ["integer", "null"],
55
      "description": "?"
56
    },
57
    "notforloan": {
58
      "type": "integer",
59
      "description": "authorized value defining why this item is not for loan"
60
    },
61
    "damaged": {
62
      "type": "integer",
63
      "description": "authorized value defining this item as damaged"
64
    },
65
    "damaged_on": {
66
      "type": ["string", "null"],
67
      "description": "authorized value defining this item as damaged"
68
    },
69
    "itemlost": {
70
      "type": "integer",
71
      "description": "authorized value defining this item as lost"
72
    },
73
    "itemlost_on": {
74
      "type": ["string", "null"],
75
      "format": "date-time",
76
      "description": "the date and time an item was last marked as lost, NULL if not lost"
77
    },
78
    "withdrawn": {
79
      "type": "integer",
80
      "description": "authorized value defining this item as withdrawn"
81
    },
82
    "withdrawn_on": {
83
      "type": ["string", "null"],
84
      "format": "date-time",
85
      "description": "the date and time an item was last marked as withdrawn, NULL if not withdrawn"
86
    },
87
    "itemcallnumber": {
88
      "type": ["string", "null"],
89
      "description": "call number for this item"
90
    },
91
    "coded_location_qualifier": {
92
      "type": ["string", "null"],
93
      "description": "coded location qualifier"
94
    },
95
    "issues": {
96
      "type": ["integer", "null"],
97
      "description": "number of times this item has been checked out/issued"
98
    },
99
    "renewals": {
100
      "type": ["integer", "null"],
101
      "description": "number of times this item has been renewed"
102
    },
103
    "reserves": {
104
      "type": ["integer", "null"],
105
      "description": "number of times this item has been placed on hold/reserved"
106
    },
107
    "restricted": {
108
      "type": ["integer", "null"],
109
      "description": "authorized value defining use restrictions for this item"
110
    },
111
    "itemnotes": {
112
      "type": ["string", "null"],
113
      "description": "public notes on this item"
114
    },
115
    "itemnotes_nonpublic": {
116
      "type": ["string", "null"],
117
      "description": "non-public notes on this item"
118
    },
119
    "holdingbranch": {
120
      "type": ["string", "null"],
121
      "description": "library that is currently in possession item"
122
    },
123
    "paidfor": {
124
      "type": ["string", "null"],
125
      "description": "?"
126
    },
127
    "timestamp": {
128
      "type": "string",
129
      "format": "date-time",
130
      "description": "date and time this item was last altered"
131
    },
132
    "location": {
133
      "type": ["string", "null"],
134
      "description": "authorized value for the shelving location for this item"
135
    },
136
    "permanent_location": {
137
      "type": ["string", "null"],
138
      "description": "linked to the CART and PROC temporary locations feature, stores the permanent shelving location"
139
    },
140
    "onloan": {
141
      "type": ["string", "null"],
142
      "format": "date",
143
      "description": "defines if item is checked out (NULL for not checked out, and checkout date for checked out)"
144
    },
145
    "cn_source": {
146
      "type": ["string", "null"],
147
      "description": "classification source used on this item"
148
    },
149
    "cn_sort": {
150
      "type": ["string", "null"],
151
      "description": "?"
152
    },
153
    "ccode": {
154
      "type": ["string", "null"],
155
      "description": "authorized value for the collection code associated with this item"
156
    },
157
    "materials": {
158
      "type": ["string", "null"],
159
      "description": "materials specified"
160
    },
161
    "uri": {
162
      "type": ["string", "null"],
163
      "description": "URL for the item"
164
    },
165
    "itype": {
166
      "type": ["string", "null"],
167
      "description": "itemtype defining the type for this item"
168
    },
169
    "more_subfields_xml": {
170
      "type": ["string", "null"],
171
      "description": "additional 952 subfields in XML format"
172
    },
173
    "enumchron": {
174
      "type": ["string", "null"],
175
      "description": "serial enumeration/chronology for the item"
176
    },
177
    "copynumber": {
178
      "type": ["string", "null"],
179
      "description": "copy number"
180
    },
181
    "stocknumber": {
182
      "type": ["string", "null"],
183
      "description": "inventory number"
184
    },
185
    "new_status": {
186
      "type": ["string", "null"],
187
      "description": "'new' value, whatever free-text information."
188
    }
189
  },
190
  "additionalProperties": false,
191
  "required": ["item_id", "biblio_id", "biblioitem_id", "notforloan", "damaged", "itemlost", "withdrawn"]
192
}
(-)a/api/v1/swagger/parameters.json (+3 lines)
Lines 17-22 Link Here
17
  "library_id_pp": {
17
  "library_id_pp": {
18
    "$ref": "parameters/library.json#/library_id_pp"
18
    "$ref": "parameters/library.json#/library_id_pp"
19
  },
19
  },
20
  "item_id_pp": {
21
    "$ref": "parameters/item.json#/item_id_pp"
22
  },
20
  "vendoridPathParam": {
23
  "vendoridPathParam": {
21
    "$ref": "parameters/vendor.json#/vendoridPathParam"
24
    "$ref": "parameters/vendor.json#/vendoridPathParam"
22
  },
25
  },
(-)a/api/v1/swagger/parameters/item.json (+9 lines)
Line 0 Link Here
1
{
2
  "item_id_pp": {
3
    "name": "item_id",
4
    "in": "path",
5
    "description": "Internal item identifier",
6
    "required": true,
7
    "type": "integer"
8
  }
9
}
(-)a/api/v1/swagger/paths.json (+3 lines)
Lines 44-49 Link Here
44
  "/libraries/{library_id}": {
44
  "/libraries/{library_id}": {
45
    "$ref": "paths/libraries.json#/~1libraries~1{library_id}"
45
    "$ref": "paths/libraries.json#/~1libraries~1{library_id}"
46
  },
46
  },
47
  "/items/{item_id}": {
48
    "$ref": "paths/items.json#/~1items~1{item_id}"
49
  },
47
  "/patrons": {
50
  "/patrons": {
48
    "$ref": "paths/patrons.json#/~1patrons"
51
    "$ref": "paths/patrons.json#/~1patrons"
49
  },
52
  },
(-)a/api/v1/swagger/paths/items.json (+47 lines)
Line 0 Link Here
1
{
2
  "/items/{item_id}": {
3
    "get": {
4
      "x-mojo-to": "Items#get",
5
      "operationId": "getItem",
6
      "tags": ["items"],
7
      "parameters": [{
8
          "$ref": "../parameters.json#/item_id_pp"
9
        }
10
      ],
11
      "consumes": ["application/json"],
12
      "produces": ["application/json"],
13
      "responses": {
14
        "200": {
15
          "description": "An item",
16
          "schema": {
17
            "$ref": "../definitions.json#/item"
18
          }
19
        },
20
        "400": {
21
          "description": "Missing or wrong parameters",
22
          "schema": {
23
            "$ref": "../definitions.json#/error"
24
          }
25
        },
26
        "404": {
27
          "description": "Item not found",
28
          "schema": {
29
            "$ref": "../definitions.json#/error"
30
          }
31
        },
32
        "500": {
33
          "description": "Internal server error",
34
          "schema": {
35
            "$ref": "../definitions.json#/error"
36
          }
37
        },
38
        "503": {
39
          "description": "Under maintenance",
40
          "schema": {
41
            "$ref": "../definitions.json#/error"
42
          }
43
        }
44
      }
45
    }
46
  }
47
}
(-)a/api/v1/swagger/x-primitives.json (+8 lines)
Lines 3-8 Link Here
3
    "type": "integer",
3
    "type": "integer",
4
    "description": "Internal biblio identifier"
4
    "description": "Internal biblio identifier"
5
  },
5
  },
6
  "biblioitem_id": {
7
    "type": "integer",
8
    "description": "Internal biblioitem identifier"
9
  },
6
  "patron_id": {
10
  "patron_id": {
7
    "type": "integer",
11
    "type": "integer",
8
    "description": "Internal patron identifier"
12
    "description": "Internal patron identifier"
Lines 30-35 Link Here
30
    "type": ["string", "null"],
34
    "type": ["string", "null"],
31
    "description": "patron's first name"
35
    "description": "patron's first name"
32
  },
36
  },
37
  "item_id": {
38
    "type": "integer",
39
    "description": "internally assigned item identifier"
40
  },
33
  "phone": {
41
  "phone": {
34
    "type": ["string", "null"],
42
    "type": ["string", "null"],
35
    "description": "primary phone number for patron's primary address"
43
    "description": "primary phone number for patron's primary address"
(-)a/t/db_dependent/api/v1/items.t (-1 / +151 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/env perl
2
3
# Copyright 2016 Koha-Suomi
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
use Modern::Perl;
21
22
use Test::More tests => 4;
23
use Test::Mojo;
24
use Test::Warn;
25
26
use t::lib::TestBuilder;
27
use t::lib::Mocks;
28
29
use C4::Auth;
30
use Koha::Items;
31
use Koha::Database;
32
33
my $schema  = Koha::Database->new->schema;
34
my $builder = t::lib::TestBuilder->new();
35
36
# FIXME: sessionStorage defaults to mysql, but it seems to break transaction handling
37
# this affects the other REST api tests
38
t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' );
39
40
my $remote_address = '127.0.0.1';
41
my $t              = Test::Mojo->new('Koha::REST::V1');
42
43
subtest 'list() tests' => sub {
44
    plan tests => 2;
45
46
    # Not yet implemented
47
    my $tx = $t->ua->build_tx(GET => "/api/v1/items");
48
    $t->request_ok($tx)
49
      ->status_is(404);
50
};
51
52
subtest 'get() tests' => sub {
53
    plan tests => 8;
54
55
    $schema->storage->txn_begin;
56
57
    my $biblio = $builder->build({
58
        source => 'Biblio'
59
    });
60
    my $biblionumber = $biblio->{biblionumber};
61
    my $item = $builder->build({
62
        source => 'Item',
63
        value => {
64
            biblionumber => $biblionumber,
65
            withdrawn => 1, # tested with OpacHiddenItems
66
        }
67
    });
68
    my $itemnumber = $item->{itemnumber};
69
70
    my ($patron, $sessionid) = create_user_and_session({ authorized=>0 });
71
    my ($librarian, $lib_sessionid) = create_user_and_session({ authorized=>4 });
72
73
    my $nonExistentItemnumber = -14362719;
74
    my $tx = $t->ua->build_tx(GET => "/api/v1/items/$nonExistentItemnumber");
75
    $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
76
    $t->request_ok($tx)
77
      ->status_is(404);
78
79
    subtest 'Confirm effectiveness of OpacHiddenItems' => sub {
80
        plan tests => 7;
81
82
        t::lib::Mocks::mock_preference('OpacHiddenItems', '');
83
        $tx = $t->ua->build_tx(GET => "/api/v1/items/$itemnumber");
84
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
85
        $t->request_ok($tx)
86
          ->status_is(200)
87
          ->json_is('/item_id' => $itemnumber)
88
          ->json_is('/biblio_id' => $biblionumber)
89
          ->json_is('/itemnotes_nonpublic' => undef);
90
91
        t::lib::Mocks::mock_preference('OpacHiddenItems', 'withdrawn: [1]');
92
        $tx = $t->ua->build_tx(GET => "/api/v1/items/$itemnumber");
93
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
94
        $t->request_ok($tx)
95
          ->status_is(404);
96
    };
97
98
    $tx = $t->ua->build_tx(GET => "/api/v1/items/$itemnumber");
99
    $tx->req->cookies({name => 'CGISESSID', value => $lib_sessionid});
100
    $t->request_ok($tx)
101
      ->status_is(200)
102
      ->json_is('/item_id' => $itemnumber)
103
      ->json_is('/biblio_id' => $biblionumber)
104
      ->json_is('/itemnotes_nonpublic' => $item->{itemnotes_nonpublic});
105
106
    $schema->storage->txn_rollback;
107
};
108
109
subtest 'update() tests' => sub {
110
    plan tests => 2;
111
112
    # Not yet implemented
113
    my $tx = $t->ua->build_tx(PUT => "/api/v1/items/1");
114
    $t->request_ok($tx)
115
      ->status_is(404);
116
};
117
118
subtest 'delete() tests' => sub {
119
    plan tests => 2;
120
121
    # Not yet implemented
122
    my $tx = $t->ua->build_tx(DELETE => "/api/v1/items/1");
123
    $t->request_ok($tx)
124
      ->status_is(404);
125
};
126
127
sub create_user_and_session {
128
129
    my $args  = shift;
130
    my $flags = ( $args->{authorized} ) ? $args->{authorized} : 0;
131
    my $dbh   = C4::Context->dbh;
132
133
    my $user = $builder->build_object(
134
        {
135
            class => 'Koha::Patrons',
136
            value  => {
137
                flags => $flags
138
            }
139
        }
140
    );
141
142
    # Create a session for the authorized user
143
    my $session = C4::Auth::get_session('');
144
    $session->param( 'number',   $user->borrowernumber );
145
    $session->param( 'id',       $user->userid );
146
    $session->param( 'ip',       '127.0.0.1' );
147
    $session->param( 'lasttime', time() );
148
    $session->flush;
149
150
    return ( $user->{borrowernumber}, $session->id );
151
}

Return to bug 16825