Browse Source
This patch adds an endpoint in thi api in /api/v1/clubs/{club_id}/holds whith the verb POST that maps to Koha::REST::V1::Clubs::Holds#add controller. Classes for club_holds and club_holds_to_patron_holds new tables where also added. To test: 1) Reach SUCCESS.3 test point of previous patch with club that has no enrollments 2) Click on "Place Hold" SUCCESS => an alert should appear that you cannot place hold on a club without patrons 3) Reach SUCCESS.3 test point of previous patch with club that has enrollments 4) Click on "Place Hold" SUCCESS => holds priority list should appear with holds for every patron in club 5) Repeat steps 3 and 4. SUCCESS => new holds should appear in different order 6) Sign off Sponsored-by: Southeast Kansas Library - SEKLS Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.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
15 changed files with 729 additions and 1 deletions
@ -0,0 +1,142 @@ |
|||
package Koha::Club::Hold; |
|||
|
|||
# Copyright ByWater Solutions 2014 |
|||
# |
|||
# 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 Carp; |
|||
|
|||
use Koha::Database; |
|||
|
|||
use Koha::Club::Template::Fields; |
|||
|
|||
use base qw(Koha::Object); |
|||
use Koha::Exceptions::ClubHold; |
|||
use Koha::Club::Hold::PatronHold; |
|||
use Koha::Clubs; |
|||
|
|||
use List::Util 'shuffle'; |
|||
|
|||
=head1 NAME |
|||
|
|||
Koha::Club::Hold |
|||
|
|||
Represents a hold made for every member of club |
|||
|
|||
=head1 API |
|||
|
|||
=head2 Class Methods |
|||
|
|||
=cut |
|||
|
|||
=head3 add |
|||
|
|||
Class (static) method that returns a new Koha::Club::Hold instance |
|||
|
|||
=cut |
|||
|
|||
sub add { |
|||
my ( $params ) = @_; |
|||
|
|||
throw Koha::Exceptions::ClubHold unless $params->{club_id} && $params->{biblio_id}; |
|||
my $club = Koha::Clubs->find($params->{club_id}); |
|||
my @enrollments = $club->club_enrollments->as_list; |
|||
throw Koha::Exceptions::ClubHold::NoPatrons() unless scalar @enrollments; |
|||
|
|||
my $biblio = Koha::Biblios->find($params->{biblio_id}); |
|||
|
|||
my $club_params = { |
|||
club_id => $params->{club_id}, |
|||
biblio_id => $params->{biblio_id}, |
|||
item_id => $params->{item_id} |
|||
}; |
|||
|
|||
my $club_hold = Koha::Club::Hold->new($club_params)->store(); |
|||
|
|||
@enrollments = shuffle(@enrollments); |
|||
|
|||
foreach my $enrollment (@enrollments) { |
|||
my $patron_id = $enrollment->borrowernumber; |
|||
|
|||
my $can_place_hold |
|||
= $params->{item_id} |
|||
? C4::Reserves::CanItemBeReserved( $patron_id, $params->{club_id} ) |
|||
: C4::Reserves::CanBookBeReserved( $patron_id, $params->{biblio_id} ); |
|||
|
|||
unless ( $can_place_hold->{status} eq 'OK' ) { |
|||
warn "Patron(".$patron_id.") Hold cannot be placed. Reason: " . $can_place_hold->{status}; |
|||
Koha::Club::Hold::PatronHold->new({ |
|||
patron_id => $patron_id, |
|||
club_hold_id => $club_hold->id, |
|||
error_code => $can_place_hold->{status} |
|||
})->store(); |
|||
next; |
|||
} |
|||
|
|||
my $priority = C4::Reserves::CalculatePriority($params->{biblio_id}); |
|||
|
|||
my $hold_id = C4::Reserves::AddReserve( |
|||
$params->{pickup_library_id}, |
|||
$patron_id, |
|||
$params->{biblio_id}, |
|||
undef, # $bibitems param is unused |
|||
$priority, |
|||
undef, # hold date, we don't allow it currently |
|||
$params->{expiration_date}, |
|||
$params->{notes}, |
|||
$biblio->title, |
|||
$params->{item_id}, |
|||
undef, # TODO: Why not? |
|||
$params->{item_type} |
|||
); |
|||
if ($hold_id) { |
|||
Koha::Club::Hold::PatronHold->new({ |
|||
patron_id => $patron_id, |
|||
club_hold_id => $club_hold->id, |
|||
hold_id => $hold_id |
|||
})->store(); |
|||
} else { |
|||
warn "Could not create hold for Patron(".$patron_id.")"; |
|||
Koha::Club::Hold::PatronHold->new({ |
|||
patron_id => $patron_id, |
|||
club_hold_id => $club_hold->id, |
|||
error_message => "Could not create hold for Patron(".$patron_id.")" |
|||
})->store(); |
|||
} |
|||
|
|||
} |
|||
|
|||
return $club_hold; |
|||
|
|||
} |
|||
|
|||
=head3 type |
|||
|
|||
=cut |
|||
|
|||
sub _type { |
|||
return 'ClubHold'; |
|||
} |
|||
|
|||
=head1 AUTHOR |
|||
|
|||
Agustin Moyano <agustinmoyano@theke.io> |
|||
|
|||
=cut |
|||
|
|||
1; |
@ -0,0 +1,56 @@ |
|||
package Koha::Club::Hold::PatronHold; |
|||
|
|||
# Copyright ByWater Solutions 2014 |
|||
# |
|||
# 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 Carp; |
|||
|
|||
use Koha::Database; |
|||
|
|||
use Koha::Club::Template::Fields; |
|||
|
|||
use base qw(Koha::Object); |
|||
|
|||
=head1 NAME |
|||
|
|||
Koha::Club::Hold::PatronHold |
|||
|
|||
Represents an effective hold made for a patron, because of a club hold |
|||
|
|||
=head1 API |
|||
|
|||
=head2 Class Methods |
|||
|
|||
=cut |
|||
|
|||
=head3 type |
|||
|
|||
=cut |
|||
|
|||
sub _type { |
|||
return 'ClubHoldsToPatronHold'; |
|||
} |
|||
|
|||
=head1 AUTHOR |
|||
|
|||
Agustin Moyano <agustinmoyano@theke.io> |
|||
|
|||
=cut |
|||
|
|||
1; |
@ -0,0 +1,64 @@ |
|||
package Koha::Club::Hold::PatronHolds; |
|||
|
|||
# Copyright ByWater Solutions 2014 |
|||
# |
|||
# 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 Carp; |
|||
|
|||
use Koha::Database; |
|||
|
|||
use Koha::Club::Field; |
|||
|
|||
use base qw(Koha::Objects); |
|||
|
|||
=head1 NAME |
|||
|
|||
Koha::Club::Hold::PatronHolds |
|||
|
|||
Represents a collection of effective holds made by a club hold. |
|||
|
|||
=head1 API |
|||
|
|||
=head2 Class Methods |
|||
|
|||
=cut |
|||
|
|||
=head3 type |
|||
|
|||
=cut |
|||
|
|||
sub _type { |
|||
return 'ClubHoldsToPatronHold'; |
|||
} |
|||
|
|||
=head3 object_class |
|||
|
|||
=cut |
|||
|
|||
sub object_class { |
|||
return 'Koha::Club::Hold::PatronHold'; |
|||
} |
|||
|
|||
=head1 AUTHOR |
|||
|
|||
Agustin Moyano <agustinmoyano@theke.io> |
|||
|
|||
=cut |
|||
|
|||
1; |
@ -0,0 +1,64 @@ |
|||
package Koha::Club::Holds; |
|||
|
|||
# Copyright ByWater Solutions 2014 |
|||
# |
|||
# 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 Carp; |
|||
|
|||
use Koha::Database; |
|||
|
|||
use Koha::Club::Field; |
|||
|
|||
use base qw(Koha::Objects); |
|||
|
|||
=head1 NAME |
|||
|
|||
Koha::Club::Holds |
|||
|
|||
Represents a collection of club holds. |
|||
|
|||
=head1 API |
|||
|
|||
=head2 Class Methods |
|||
|
|||
=cut |
|||
|
|||
=head3 type |
|||
|
|||
=cut |
|||
|
|||
sub _type { |
|||
return 'ClubHold'; |
|||
} |
|||
|
|||
=head3 object_class |
|||
|
|||
=cut |
|||
|
|||
sub object_class { |
|||
return 'Koha::Club::Hold'; |
|||
} |
|||
|
|||
=head1 AUTHOR |
|||
|
|||
Agustin Moyano <agustinmoyano@theke.io> |
|||
|
|||
=cut |
|||
|
|||
1; |
@ -0,0 +1,15 @@ |
|||
package Koha::Exceptions::ClubHold; |
|||
|
|||
use Modern::Perl; |
|||
|
|||
use Exception::Class ( |
|||
'Koha::Exceptions::ClubHold' => { |
|||
description => "Something went wrong!", |
|||
}, |
|||
'Koha::Exceptions::ClubHold::NoPatrons' => { |
|||
isa => 'Koha::Exceptions::ClubHold', |
|||
description => "Cannot place a hold on a club without patrons.", |
|||
}, |
|||
); |
|||
|
|||
1; |
@ -0,0 +1,209 @@ |
|||
package Koha::REST::V1::Clubs::Holds; |
|||
|
|||
# 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; |
|||
use C4::Reserves; |
|||
|
|||
use Koha::Items; |
|||
use Koha::Patrons; |
|||
use Koha::Holds; |
|||
use Koha::Clubs; |
|||
use Koha::Club::Hold; |
|||
use Koha::DateUtils; |
|||
|
|||
use Scalar::Util qw(blessed); |
|||
use Try::Tiny; |
|||
use List::Util 'shuffle'; |
|||
|
|||
=head1 API |
|||
|
|||
=head2 Class methods |
|||
|
|||
=head3 add |
|||
|
|||
Method that handles adding a new Koha::Hold object |
|||
|
|||
=cut |
|||
|
|||
sub add { |
|||
my $c = shift->openapi->valid_input or return; |
|||
|
|||
return try { |
|||
my $body = $c->validation->param('body'); |
|||
my $club_id = $c->validation->param('club_id'); |
|||
|
|||
my $biblio; |
|||
|
|||
my $biblio_id = $body->{biblio_id}; |
|||
my $pickup_library_id = $body->{pickup_library_id}; |
|||
my $item_id = $body->{item_id}; |
|||
my $item_type = $body->{item_type}; |
|||
my $expiration_date = $body->{expiration_date}; |
|||
my $notes = $body->{notes}; |
|||
|
|||
if ( $item_id and $biblio_id ) { |
|||
|
|||
# check they are consistent |
|||
unless ( Koha::Items->search( { itemnumber => $item_id, biblionumber => $biblio_id } ) |
|||
->count > 0 ) |
|||
{ |
|||
return $c->render( |
|||
status => 400, |
|||
openapi => { error => "Item $item_id doesn't belong to biblio $biblio_id" } |
|||
); |
|||
} |
|||
else { |
|||
$biblio = Koha::Biblios->find($biblio_id); |
|||
} |
|||
} |
|||
elsif ($item_id) { |
|||
my $item = Koha::Items->find($item_id); |
|||
|
|||
unless ($item) { |
|||
return $c->render( |
|||
status => 404, |
|||
openapi => { error => "item_id not found." } |
|||
); |
|||
} |
|||
else { |
|||
$biblio = $item->biblio; |
|||
} |
|||
} |
|||
elsif ($biblio_id) { |
|||
$biblio = Koha::Biblios->find($biblio_id); |
|||
} |
|||
else { |
|||
return $c->render( |
|||
status => 400, |
|||
openapi => { error => "At least one of biblio_id, item_id should be given" } |
|||
); |
|||
} |
|||
|
|||
unless ($biblio) { |
|||
return $c->render( |
|||
status => 400, |
|||
openapi => "Biblio not found." |
|||
); |
|||
} |
|||
|
|||
# AddReserve expects date to be in syspref format |
|||
if ($expiration_date) { |
|||
$expiration_date = output_pref( dt_from_string( $expiration_date, 'rfc3339' ) ); |
|||
} |
|||
|
|||
my $club_hold = Koha::Club::Hold::add({ |
|||
club_id => $club_id, |
|||
biblio_id => $biblio->biblionumber, |
|||
item_id => $item_id, |
|||
pickup_library_id => $pickup_library_id, |
|||
expiration_date => $expiration_date, |
|||
notes => $notes, |
|||
item_type => $item_type |
|||
}); |
|||
|
|||
my $mapping = _to_api($club_hold->unblessed); |
|||
|
|||
return $c->render( status => 201, openapi => $mapping ); |
|||
} |
|||
catch { |
|||
if ( blessed $_ and $_->isa('Koha::Exceptions::Object') ) { |
|||
if ( $_->isa('Koha::Exceptions::Object::FKConstraint') ) { |
|||
my $broken_fk = $_->broken_fk; |
|||
|
|||
if ( grep { $_ eq $broken_fk } keys %{$Koha::REST::V1::Clubs::Holds::to_api_mapping} ) { |
|||
$c->render( |
|||
status => 404, |
|||
openapi => $Koha::REST::V1::Clubs::Holds::to_api_mapping->{$broken_fk} . ' not found.' |
|||
); |
|||
} |
|||
else { |
|||
return $c->render( |
|||
status => 500, |
|||
openapi => { error => "Uncaught exception: $_" } |
|||
); |
|||
} |
|||
} |
|||
else { |
|||
return $c->render( |
|||
status => 500, |
|||
openapi => { error => "$_" } |
|||
); |
|||
} |
|||
} |
|||
elsif (blessed $_ and $_->isa('Koha::Exceptions::ClubHold')) { |
|||
return $c->render( |
|||
status => 500, |
|||
openapi => { error => $_->description } |
|||
); |
|||
} |
|||
else { |
|||
return $c->render( |
|||
status => 500, |
|||
openapi => { error => "Something went wrong. check the logs." } |
|||
); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
=head3 _to_api |
|||
|
|||
Helper function that maps unblessed Koha::Club::Hold objects into REST api |
|||
attribute names. |
|||
|
|||
=cut |
|||
|
|||
sub _to_api { |
|||
my $club_hold = shift; |
|||
|
|||
# Rename attributes |
|||
foreach my $column ( keys %{ $Koha::REST::V1::Clubs::Holds::to_api_mapping } ) { |
|||
my $mapped_column = $Koha::REST::V1::Clubs::Holds::to_api_mapping->{$column}; |
|||
if ( exists $club_hold->{ $column } |
|||
&& defined $mapped_column ) |
|||
{ |
|||
# key != undef |
|||
$club_hold->{ $mapped_column } = delete $club_hold->{ $column }; |
|||
} |
|||
elsif ( exists $club_hold->{ $column } |
|||
&& !defined $mapped_column ) |
|||
{ |
|||
# key == undef |
|||
delete $club_hold->{ $column }; |
|||
} |
|||
} |
|||
|
|||
# Calculate the 'restricted' field |
|||
return $club_hold; |
|||
} |
|||
|
|||
=head3 $to_api_mapping |
|||
|
|||
=cut |
|||
|
|||
our $to_api_mapping = { |
|||
id => 'club_hold_id', |
|||
club_id => 'club_id', |
|||
biblio_id => 'biblio_id', |
|||
item_id => 'item_id' |
|||
}; |
|||
|
|||
|
|||
1; |
@ -0,0 +1,21 @@ |
|||
{ |
|||
"type": "object", |
|||
"properties": { |
|||
"club_hold_id": { |
|||
"type": "integer", |
|||
"description": "Internal club hold identifier" |
|||
}, |
|||
"club_id": { |
|||
"type": "integer", |
|||
"description": "Internal club identifier" |
|||
}, |
|||
"biblio_id": { |
|||
"type": "integer", |
|||
"description": "Internal biblio identifier" |
|||
}, |
|||
"item_id": { |
|||
"type": ["string", "null"], |
|||
"description": "Internal item identifier" |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,30 @@ |
|||
{ |
|||
"type": "object", |
|||
"properties": { |
|||
"club_hold_patron_hold_id": { |
|||
"type": "integer", |
|||
"description": "Internal club hold to patron hold identifier" |
|||
}, |
|||
"club_hold_id": { |
|||
"type": "integer", |
|||
"description": "Internal club hold identifier" |
|||
}, |
|||
"hold_id": { |
|||
"type": ["integer", "null"], |
|||
"description": "Internal hold identifier" |
|||
}, |
|||
"patron_id": { |
|||
"type": "integer", |
|||
"description": "Internal patron identifier" |
|||
}, |
|||
"error_code": { |
|||
"type": ["string", "null"], |
|||
"format": "date", |
|||
"description": "Code returned by CanItemBeReserved" |
|||
}, |
|||
"error_message": { |
|||
"type": ["string", "null"], |
|||
"description": "Generic error message" |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
{ |
|||
"type": "array", |
|||
"items": { |
|||
"$ref": "club_hold_patron_hold.json" |
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
{ |
|||
"type": "array", |
|||
"items": { |
|||
"$ref": "club_hold.json" |
|||
} |
|||
} |
@ -0,0 +1,9 @@ |
|||
{ |
|||
"club_id_pp": { |
|||
"name": "club_id", |
|||
"in": "path", |
|||
"description": "Internal club identifier", |
|||
"required": true, |
|||
"type": "integer" |
|||
} |
|||
} |
@ -0,0 +1,100 @@ |
|||
{ |
|||
"/clubs/{club_id}/holds": { |
|||
"post": { |
|||
"x-mojo-to": "Clubs::Holds#add", |
|||
"operationId": "addClubHold", |
|||
"tags": ["holds", "clubs"], |
|||
"parameters": [{ |
|||
"$ref": "../parameters.json#/club_id_pp" |
|||
}, { |
|||
"name": "body", |
|||
"in": "body", |
|||
"description": "A JSON object containing informations about the new hold", |
|||
"required": true, |
|||
"schema": { |
|||
"type": "object", |
|||
"properties": { |
|||
"biblio_id": { |
|||
"description": "Internal biblio identifier", |
|||
"type": [ "integer", "null" ] |
|||
}, |
|||
"item_id": { |
|||
"description": "Internal item identifier", |
|||
"type": [ "integer", "null" ] |
|||
}, |
|||
"pickup_library_id": { |
|||
"description": "Internal library identifier for the pickup library", |
|||
"type": "string" |
|||
}, |
|||
"expiration_date": { |
|||
"description": "Hold end date", |
|||
"type": ["string", "null"], |
|||
"format": "date" |
|||
}, |
|||
"notes": { |
|||
"description": "Notes related to this hold", |
|||
"type": [ "string", "null" ] |
|||
}, |
|||
"item_type": { |
|||
"description": "Limit hold on one itemtype (ignored for item-level holds)", |
|||
"type": [ "string", "null" ] |
|||
} |
|||
}, |
|||
"required": [ "pickup_library_id" ] |
|||
} |
|||
} |
|||
], |
|||
"consumes": ["application/json"], |
|||
"produces": ["application/json"], |
|||
"responses": { |
|||
"201": { |
|||
"description": "Created hold", |
|||
"schema": { |
|||
"$ref": "../definitions/club_hold.json" |
|||
} |
|||
}, |
|||
"400": { |
|||
"description": "Missing or wrong parameters", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
}, |
|||
"401": { |
|||
"description": "Authentication required", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
}, |
|||
"403": { |
|||
"description": "Hold not allowed", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
}, |
|||
"404": { |
|||
"description": "Hold 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": { |
|||
"reserveforothers": "1" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
Loading…
Reference in new issue