Bug 23634: (QA follow-up) Our PUT is really a PATCH
[koha.git] / Koha / REST / V1 / Patrons.pm
1 package Koha::REST::V1::Patrons;
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18 use Modern::Perl;
19
20 use Mojo::Base 'Mojolicious::Controller';
21
22 use Koha::DateUtils;
23 use Koha::Patrons;
24
25 use Scalar::Util qw(blessed);
26 use Try::Tiny;
27
28 =head1 NAME
29
30 Koha::REST::V1::Patrons
31
32 =head1 API
33
34 =head2 Methods
35
36 =head3 list
37
38 Controller function that handles listing Koha::Patron objects
39
40 =cut
41
42 sub list {
43     my $c = shift->openapi->valid_input or return;
44
45     return try {
46
47         my $patrons_rs = Koha::Patrons->new;
48         my $args = $c->validation->output;
49         my $attributes = {};
50
51         # Extract reserved params
52         my ( $filtered_params, $reserved_params ) = $c->extract_reserved_params($args);
53
54         my $restricted = delete $filtered_params->{restricted};
55
56         # Merge sorting into query attributes
57         $c->dbic_merge_sorting(
58             {
59                 attributes => $attributes,
60                 params     => $reserved_params,
61                 result_set => $patrons_rs
62             }
63         );
64
65         # Merge pagination into query attributes
66         $c->dbic_merge_pagination(
67             {
68                 filter => $attributes,
69                 params => $reserved_params
70             }
71         );
72
73         if ( defined $filtered_params ) {
74
75             # Apply the mapping function to the passed params
76             $filtered_params = $patrons_rs->attributes_from_api($filtered_params);
77             $filtered_params = $c->build_query_params( $filtered_params, $reserved_params );
78         }
79
80         # translate 'restricted' => 'debarred'
81         $filtered_params->{debarred} = { '!=' => undef }
82           if $restricted;
83
84         my $patrons = $patrons_rs->search( $filtered_params, $attributes );
85         if ( $patrons_rs->is_paged ) {
86             $c->add_pagination_headers(
87                 {
88                     total  => $patrons->pager->total_entries,
89                     params => $args,
90                 }
91             );
92         }
93
94         return $c->render( status => 200, openapi => $patrons->to_api );
95     }
96     catch {
97         $c->unhandled_exception($_);
98     };
99 }
100
101
102 =head3 get
103
104 Controller function that handles retrieving a single Koha::Patron object
105
106 =cut
107
108 sub get {
109     my $c = shift->openapi->valid_input or return;
110
111     return try {
112         my $patron_id = $c->validation->param('patron_id');
113         my $patron    = Koha::Patrons->find($patron_id);
114
115         unless ($patron) {
116             return $c->render( status => 404, openapi => { error => "Patron not found." } );
117         }
118
119         return $c->render( status => 200, openapi => $patron->to_api );
120     }
121     catch {
122         $c->unhandled_exception($_);
123     };
124 }
125
126 =head3 add
127
128 Controller function that handles adding a new Koha::Patron object
129
130 =cut
131
132 sub add {
133     my $c = shift->openapi->valid_input or return;
134
135     return try {
136
137         my $patron = Koha::Patron->new_from_api( $c->validation->param('body') )->store;
138
139         $c->res->headers->location( $c->req->url->to_string . '/' . $patron->borrowernumber );
140         return $c->render(
141             status  => 201,
142             openapi => $patron->to_api
143         );
144     }
145     catch {
146
147         my $to_api_mapping = Koha::Patron->new->to_api_mapping;
148
149         unless ( blessed $_ && $_->can('rethrow') ) {
150             return $c->render(
151                 status  => 500,
152                 openapi => { error => "Something went wrong, check Koha logs for details." }
153             );
154         }
155         if ( $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
156             return $c->render(
157                 status  => 409,
158                 openapi => { error => $_->error, conflict => $_->duplicate_id }
159             );
160         }
161         elsif ( $_->isa('Koha::Exceptions::Object::FKConstraint') ) {
162             return $c->render(
163                 status  => 400,
164                 openapi => {
165                           error => "Given "
166                         . $to_api_mapping->{ $_->broken_fk }
167                         . " does not exist"
168                 }
169             );
170         }
171         elsif ( $_->isa('Koha::Exceptions::BadParameter') ) {
172             return $c->render(
173                 status  => 400,
174                 openapi => {
175                           error => "Given "
176                         . $to_api_mapping->{ $_->parameter }
177                         . " does not exist"
178                 }
179             );
180         }
181         else {
182             $c->unhandled_exception($_);
183         }
184     };
185 }
186
187
188 =head3 update
189
190 Controller function that handles updating a Koha::Patron object
191
192 =cut
193
194 sub update {
195     my $c = shift->openapi->valid_input or return;
196
197     my $patron_id = $c->validation->param('patron_id');
198     my $patron    = Koha::Patrons->find( $patron_id );
199
200     unless ($patron) {
201          return $c->render(
202              status  => 404,
203              openapi => { error => "Patron not found" }
204          );
205      }
206
207     return try {
208         my $body = $c->validation->param('body');
209         my $user = $c->stash('koha.user');
210
211         if (
212                 $patron->is_superlibrarian
213             and !$user->is_superlibrarian
214             and (  exists $body->{email}
215                 or exists $body->{secondary_email}
216                 or exists $body->{altaddress_email} )
217           )
218         {
219             foreach my $email_field ( qw(email secondary_email altaddress_email) ) {
220                 my $exists_email = exists $body->{$email_field};
221                 next unless $exists_email;
222
223                 # exists, verify if we are asked to change it
224                 my $put_email      = $body->{$email_field};
225                 # As of writing this patch, 'email' is the only unmapped field
226                 # (i.e. it preserves its name, hence this fallback)
227                 my $db_email_field = $patron->to_api_mapping->{$email_field} // 'email';
228                 my $db_email       = $patron->$db_email_field;
229
230                 return $c->render(
231                     status  => 403,
232                     openapi => { error => "Not enough privileges to change a superlibrarian's email" }
233                   )
234                   unless ( !defined $put_email and !defined $db_email )
235                   or (  defined $put_email
236                     and defined $db_email
237                     and $put_email eq $db_email );
238             }
239         }
240
241         $patron->set_from_api($c->validation->param('body'))->store;
242         $patron->discard_changes;
243         return $c->render( status => 200, openapi => $patron->to_api );
244     }
245     catch {
246         unless ( blessed $_ && $_->can('rethrow') ) {
247             return $c->render(
248                 status  => 500,
249                 openapi => {
250                     error => "Something went wrong, check Koha logs for details."
251                 }
252             );
253         }
254         if ( $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
255             return $c->render(
256                 status  => 409,
257                 openapi => { error => $_->error, conflict => $_->duplicate_id }
258             );
259         }
260         elsif ( $_->isa('Koha::Exceptions::Object::FKConstraint') ) {
261             return $c->render(
262                 status  => 400,
263                 openapi => { error => "Given " .
264                             $patron->to_api_mapping->{$_->broken_fk}
265                             . " does not exist" }
266             );
267         }
268         elsif ( $_->isa('Koha::Exceptions::MissingParameter') ) {
269             return $c->render(
270                 status  => 400,
271                 openapi => {
272                     error      => "Missing mandatory parameter(s)",
273                     parameters => $_->parameter
274                 }
275             );
276         }
277         elsif ( $_->isa('Koha::Exceptions::BadParameter') ) {
278             return $c->render(
279                 status  => 400,
280                 openapi => {
281                     error      => "Invalid parameter(s)",
282                     parameters => $_->parameter
283                 }
284             );
285         }
286         elsif ( $_->isa('Koha::Exceptions::NoChanges') ) {
287             return $c->render(
288                 status  => 204,
289                 openapi => { error => "No changes have been made" }
290             );
291         }
292         else {
293             $c->unhandled_exception($_);
294         }
295     };
296 }
297
298 =head3 delete
299
300 Controller function that handles deleting a Koha::Patron object
301
302 =cut
303
304 sub delete {
305     my $c = shift->openapi->valid_input or return;
306
307     my $patron;
308
309     return try {
310         $patron = Koha::Patrons->find( $c->validation->param('patron_id') );
311
312         # check if loans, reservations, debarrment, etc. before deletion!
313         $patron->delete;
314         return $c->render(
315             status  => 204,
316             openapi => q{}
317         );
318     }
319     catch {
320         unless ($patron) {
321             return $c->render(
322                 status  => 404,
323                 openapi => { error => "Patron not found" }
324             );
325         }
326         else {
327             $c->unhandled_exception($_);
328         }
329     };
330 }
331
332 =head3 guarantors_can_see_charges
333
334 Method for setting whether guarantors can see the patron's charges.
335
336 =cut
337
338 sub guarantors_can_see_charges {
339     my $c = shift->openapi->valid_input or return;
340
341     return try {
342         if ( C4::Context->preference('AllowPatronToSetFinesVisibilityForGuarantor') ) {
343             my $patron = $c->stash( 'koha.user' );
344             my $privacy_setting = ($c->req->json->{allowed}) ? 1 : 0;
345
346             $patron->privacy_guarantor_fines( $privacy_setting )->store;
347
348             return $c->render(
349                 status  => 200,
350                 openapi => {}
351             );
352         }
353         else {
354             return $c->render(
355                 status  => 403,
356                 openapi => {
357                     error =>
358                       'The current configuration doesn\'t allow the requested action.'
359                 }
360             );
361         }
362     }
363     catch {
364         $c->unhandled_exception($_);
365     };
366 }
367
368 =head3 guarantors_can_see_checkouts
369
370 Method for setting whether guarantors can see the patron's checkouts.
371
372 =cut
373
374 sub guarantors_can_see_checkouts {
375     my $c = shift->openapi->valid_input or return;
376
377     return try {
378         if ( C4::Context->preference('AllowPatronToSetCheckoutsVisibilityForGuarantor') ) {
379             my $patron = $c->stash( 'koha.user' );
380             my $privacy_setting = ( $c->req->json->{allowed} ) ? 1 : 0;
381
382             $patron->privacy_guarantor_checkouts( $privacy_setting )->store;
383
384             return $c->render(
385                 status  => 200,
386                 openapi => {}
387             );
388         }
389         else {
390             return $c->render(
391                 status  => 403,
392                 openapi => {
393                     error =>
394                       'The current configuration doesn\'t allow the requested action.'
395                 }
396             );
397         }
398     }
399     catch {
400         $c->unhandled_exception($_);
401     };
402 }
403
404 1;