Browse Source

Bug 19618: Add api endpoint for club holds

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
Agustin Moyano 3 years ago
committed by Martin Renvoize
parent
commit
a902efb83b
Signed by: martin.renvoize GPG Key ID: 422B469130441A0F
  1. 142
      Koha/Club/Hold.pm
  2. 56
      Koha/Club/Hold/PatronHold.pm
  3. 64
      Koha/Club/Hold/PatronHolds.pm
  4. 64
      Koha/Club/Holds.pm
  5. 15
      Koha/Exceptions/ClubHold.pm
  6. 209
      Koha/REST/V1/Clubs/Holds.pm
  7. 21
      api/v1/swagger/definitions/club_hold.json
  8. 30
      api/v1/swagger/definitions/club_hold_patron_hold.json
  9. 6
      api/v1/swagger/definitions/club_hold_patron_holds.json
  10. 6
      api/v1/swagger/definitions/club_holds.json
  11. 3
      api/v1/swagger/parameters.json
  12. 9
      api/v1/swagger/parameters/club.json
  13. 3
      api/v1/swagger/paths.json
  14. 100
      api/v1/swagger/paths/clubs.json
  15. 2
      koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt

142
Koha/Club/Hold.pm

@ -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;

56
Koha/Club/Hold/PatronHold.pm

@ -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;

64
Koha/Club/Hold/PatronHolds.pm

@ -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;

64
Koha/Club/Holds.pm

@ -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;

15
Koha/Exceptions/ClubHold.pm

@ -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;

209
Koha/REST/V1/Clubs/Holds.pm

@ -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;

21
api/v1/swagger/definitions/club_hold.json

@ -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"
}
}
}

30
api/v1/swagger/definitions/club_hold_patron_hold.json

@ -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"
}
}
}

6
api/v1/swagger/definitions/club_hold_patron_holds.json

@ -0,0 +1,6 @@
{
"type": "array",
"items": {
"$ref": "club_hold_patron_hold.json"
}
}

6
api/v1/swagger/definitions/club_holds.json

@ -0,0 +1,6 @@
{
"type": "array",
"items": {
"$ref": "club_hold.json"
}
}

3
api/v1/swagger/parameters.json

@ -14,6 +14,9 @@
"hold_id_pp": {
"$ref": "parameters/hold.json#/hold_id_pp"
},
"club_id_pp": {
"$ref": "parameters/club.json#/club_id_pp"
},
"library_id_pp": {
"$ref": "parameters/library.json#/library_id_pp"
},

9
api/v1/swagger/parameters/club.json

@ -0,0 +1,9 @@
{
"club_id_pp": {
"name": "club_id",
"in": "path",
"description": "Internal club identifier",
"required": true,
"type": "integer"
}
}

3
api/v1/swagger/paths.json

@ -29,6 +29,9 @@
"/biblios/{biblio_id}": {
"$ref": "paths/biblios.json#/~1biblios~1{biblio_id}"
},
"/clubs/{club_id}/holds": {
"$ref": "paths/clubs.json#/~1clubs~1{club_id}~1holds"
},
"/holds": {
"$ref": "paths/holds.json#/~1holds"
},

100
api/v1/swagger/paths/clubs.json

@ -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"
}
}
}
}
}

2
koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt

@ -88,7 +88,7 @@
</div>
[% END %]
<fieldset class="brief">
<label>Seach Patrons or clubs</label>
<label>Search Patrons or clubs</label>
<div id="circ_holds_select">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation"><a href="#holds_patronsearch_pane" aria-controls="holds_patronsearch_pane" role="tab" data-toggle="tab">Patrons</a></li>

Loading…
Cancel
Save