Bug 33328: Rename x-marc-schema => x-record-schema
[koha.git] / Koha / REST / V1 / Biblios.pm
1 package Koha::REST::V1::Biblios;
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::Biblios;
23 use Koha::Ratings;
24 use Koha::RecordProcessor;
25 use C4::Biblio qw( DelBiblio AddBiblio ModBiblio );
26 use C4::Search qw( FindDuplicate );
27
28 use List::MoreUtils qw( any );
29 use MARC::Record::MiJ;
30
31 use Try::Tiny qw( catch try );
32
33 =head1 API
34
35 =head2 Methods
36
37 =head3 get
38
39 Controller function that handles retrieving a single biblio object
40
41 =cut
42
43 sub get {
44     my $c = shift->openapi->valid_input or return;
45
46     my $attributes;
47     $attributes = { prefetch => [ 'metadata' ] } # don't prefetch metadata if not needed
48         unless $c->req->headers->accept =~ m/application\/json/;
49
50     my $biblio = Koha::Biblios->find( { biblionumber => $c->validation->param('biblio_id') }, $attributes );
51
52     unless ( $biblio ) {
53         return $c->render(
54             status  => 404,
55             openapi => {
56                 error => "Object not found."
57             }
58         );
59     }
60
61     return try {
62
63         if ( $c->req->headers->accept =~ m/application\/json/ ) {
64             return $c->render(
65                 status => 200,
66                 json   => $biblio->to_api
67             );
68         }
69         else {
70             my $metadata = $biblio->metadata;
71             my $record   = $metadata->record;
72             my $schema   = $metadata->schema // C4::Context->preference("marcflavour");
73
74             $c->respond_to(
75                 marcxml => {
76                     status => 200,
77                     format => 'marcxml',
78                     text   => $record->as_xml_record($schema),
79                 },
80                 mij => {
81                     status => 200,
82                     format => 'mij',
83                     data   => $record->to_mij
84                 },
85                 marc => {
86                     status => 200,
87                     format => 'marc',
88                     text   => $record->as_usmarc
89                 },
90                 txt => {
91                     status => 200,
92                     format => 'text/plain',
93                     text   => $record->as_formatted
94                 },
95                 any => {
96                     status  => 406,
97                     openapi => [
98                         "application/json",
99                         "application/marcxml+xml",
100                         "application/marc-in-json",
101                         "application/marc",
102                         "text/plain"
103                     ]
104                 }
105             );
106         }
107     }
108     catch {
109         $c->unhandled_exception($_);
110     };
111 }
112
113 =head3 delete
114
115 Controller function that handles deleting a biblio object
116
117 =cut
118
119 sub delete {
120     my $c = shift->openapi->valid_input or return;
121
122     my $biblio = Koha::Biblios->find( $c->validation->param('biblio_id') );
123
124     if ( not defined $biblio ) {
125         return $c->render(
126             status  => 404,
127             openapi => { error => "Object not found" }
128         );
129     }
130
131     return try {
132         my $error = DelBiblio( $biblio->id );
133
134         if ($error) {
135             return $c->render(
136                 status  => 409,
137                 openapi => { error => $error }
138             );
139         }
140         else {
141             return $c->render( status => 204, openapi => "" );
142         }
143     }
144     catch {
145         $c->unhandled_exception($_);
146     };
147 }
148
149 =head3 get_public
150
151 Controller function that handles retrieving a single biblio object
152
153 =cut
154
155 sub get_public {
156     my $c = shift->openapi->valid_input or return;
157
158     my $biblio = Koha::Biblios->find(
159         { biblionumber => $c->validation->param('biblio_id') },
160         { prefetch     => ['metadata'] } );
161
162     unless ($biblio) {
163         return $c->render(
164             status  => 404,
165             openapi => {
166                 error => "Object not found."
167             }
168         );
169     }
170
171     return try {
172
173         my $metadata = $biblio->metadata;
174         my $record   = $metadata->record;
175
176         my $opachiddenitems_rules = C4::Context->yaml_preference('OpacHiddenItems');
177         my $patron = $c->stash('koha.user');
178
179         # Check if the biblio should be hidden for unprivileged access
180         # unless there's a logged in user, and there's an exception for it's
181         # category
182         unless ( $patron and $patron->category->override_hidden_items ) {
183             if ( $biblio->hidden_in_opac({ rules => $opachiddenitems_rules }) )
184             {
185                 return $c->render(
186                     status  => 404,
187                     openapi => {
188                         error => "Object not found."
189                     }
190                 );
191             }
192         }
193
194         my $schema = $metadata->schema // C4::Context->preference("marcflavour");
195
196         my $record_processor = Koha::RecordProcessor->new({
197             filters => 'ViewPolicy',
198             options => {
199                 interface => 'opac',
200                 frameworkcode => $biblio->frameworkcode
201             }
202         });
203         # Apply framework's filtering to MARC::Record object
204         $record_processor->process($record);
205
206         $c->respond_to(
207             marcxml => {
208                 status => 200,
209                 format => 'marcxml',
210                 text   => $record->as_xml_record($schema),
211             },
212             mij => {
213                 status => 200,
214                 format => 'mij',
215                 data   => $record->to_mij
216             },
217             marc => {
218                 status => 200,
219                 format => 'marc',
220                 text   => $record->as_usmarc
221             },
222             txt => {
223                 status => 200,
224                 format => 'text/plain',
225                 text   => $record->as_formatted
226             },
227             any => {
228                 status  => 406,
229                 openapi => [
230                     "application/marcxml+xml",
231                     "application/marc-in-json",
232                     "application/marc",
233                     "text/plain"
234                 ]
235             }
236         );
237     }
238     catch {
239         $c->unhandled_exception($_);
240     };
241 }
242
243 =head3 get_items
244
245 Controller function that handles retrieving biblio's items
246
247 =cut
248
249 sub get_items {
250     my $c = shift->openapi->valid_input or return;
251
252     my $biblio = Koha::Biblios->find( { biblionumber => $c->validation->param('biblio_id') }, { prefetch => ['items'] } );
253
254     unless ( $biblio ) {
255         return $c->render(
256             status  => 404,
257             openapi => {
258                 error => "Object not found."
259             }
260         );
261     }
262
263     return try {
264
265         my $items_rs = $biblio->items;
266         my $items    = $c->objects->search( $items_rs );
267         return $c->render(
268             status  => 200,
269             openapi => $items
270         );
271     }
272     catch {
273         $c->unhandled_exception($_);
274     };
275 }
276
277 =head3 get_checkouts
278
279 List Koha::Checkout objects
280
281 =cut
282
283 sub get_checkouts {
284     my $c = shift->openapi->valid_input or return;
285
286     my $checked_in = delete $c->validation->output->{checked_in};
287
288     try {
289         my $biblio = Koha::Biblios->find( $c->validation->param('biblio_id') );
290
291         unless ($biblio) {
292             return $c->render(
293                 status  => 404,
294                 openapi => { error => 'Object not found' }
295             );
296         }
297
298         my $checkouts =
299           ($checked_in)
300           ? $c->objects->search( $biblio->old_checkouts )
301           : $c->objects->search( $biblio->current_checkouts );
302
303         return $c->render(
304             status  => 200,
305             openapi => $checkouts
306         );
307     }
308     catch {
309         $c->unhandled_exception($_);
310     };
311 }
312
313 =head3 pickup_locations
314
315 Method that returns the possible pickup_locations for a given biblio
316 used for building the dropdown selector
317
318 =cut
319
320 sub pickup_locations {
321     my $c = shift->openapi->valid_input or return;
322
323     my $biblio_id = $c->validation->param('biblio_id');
324     my $biblio = Koha::Biblios->find( $biblio_id );
325
326     unless ($biblio) {
327         return $c->render(
328             status  => 404,
329             openapi => { error => "Biblio not found" }
330         );
331     }
332
333     my $patron_id = delete $c->validation->output->{patron_id};
334     my $patron    = Koha::Patrons->find( $patron_id );
335
336     unless ($patron) {
337         return $c->render(
338             status  => 400,
339             openapi => { error => "Patron not found" }
340         );
341     }
342
343     return try {
344
345         my $pl_set = $biblio->pickup_locations( { patron => $patron } );
346
347         my @response = ();
348         if ( C4::Context->preference('AllowHoldPolicyOverride') ) {
349
350             my $libraries_rs = Koha::Libraries->search( { pickup_location => 1 } );
351             my $libraries    = $c->objects->search($libraries_rs);
352
353             @response = map {
354                 my $library = $_;
355                 $library->{needs_override} = (
356                     any { $_->branchcode eq $library->{library_id} }
357                     @{$pl_set->as_list}
358                   )
359                   ? Mojo::JSON->false
360                   : Mojo::JSON->true;
361                 $library;
362             } @{$libraries};
363         }
364         else {
365
366             my $pickup_locations = $c->objects->search($pl_set);
367             @response = map { $_->{needs_override} = Mojo::JSON->false; $_; } @{$pickup_locations};
368         }
369
370         return $c->render(
371             status  => 200,
372             openapi => \@response
373         );
374     }
375     catch {
376         $c->unhandled_exception($_);
377     };
378 }
379
380 =head3 get_items_public
381
382 Controller function that handles retrieving biblio's items, for unprivileged
383 access.
384
385 =cut
386
387 sub get_items_public {
388     my $c = shift->openapi->valid_input or return;
389
390     my $biblio = Koha::Biblios->find( { biblionumber => $c->validation->param('biblio_id') }, { prefetch => ['items'] } );
391
392     unless ( $biblio ) {
393         return $c->render(
394             status  => 404,
395             openapi => {
396                 error => "Object not found."
397             }
398         );
399     }
400
401     return try {
402
403         my $patron = $c->stash('koha.user');
404
405         my $items_rs = $biblio->items->filter_by_visible_in_opac({ patron => $patron });
406         my $items    = $c->objects->search( $items_rs );
407         return $c->render(
408             status  => 200,
409             openapi => $items
410         );
411     }
412     catch {
413         $c->unhandled_exception($_);
414     };
415 }
416
417 =head3 set_rating
418
419 Set rating for the logged in user
420
421 =cut
422
423
424 sub set_rating {
425     my $c = shift->openapi->valid_input or return;
426
427     my $biblio = Koha::Biblios->find( $c->validation->param('biblio_id') );
428
429     unless ($biblio) {
430         return $c->render(
431             status  => 404,
432             openapi => {
433                 error => "Object not found."
434             }
435         );
436     }
437
438     my $patron = $c->stash('koha.user');
439     unless ($patron) {
440         return $c->render(
441             status => 403,
442             openapi =>
443                 { error => "Cannot rate. Reason: must be logged-in" }
444         );
445     }
446
447     my $body   = $c->validation->param('body');
448     my $rating_value = $body->{rating};
449
450     return try {
451
452         my $rating = Koha::Ratings->find(
453             {
454                 biblionumber   => $biblio->biblionumber,
455                 borrowernumber => $patron->borrowernumber,
456             }
457         );
458         $rating->delete if $rating;
459
460         if ( $rating_value ) { # Cannot set to 0 from the UI
461             $rating = Koha::Rating->new(
462                 {
463                     biblionumber   => $biblio->biblionumber,
464                     borrowernumber => $patron->borrowernumber,
465                     rating_value   => $rating_value,
466                 }
467             )->store;
468         };
469         my $ratings =
470           Koha::Ratings->search( { biblionumber => $biblio->biblionumber } );
471         my $average = $ratings->get_avg_rating;
472
473         return $c->render(
474             status  => 200,
475             openapi => {
476                 rating  => $rating && $rating->in_storage ? $rating->rating_value : undef,
477                 average => $average,
478                 count   => $ratings->count
479             },
480         );
481     }
482     catch {
483         $c->unhandled_exception($_);
484     };
485 }
486
487 =head3 add
488
489 Controller function that handles creating a biblio object
490
491 =cut
492
493 sub add {
494     my $c = shift->openapi->valid_input or return;
495
496     try {
497         my $headers = $c->req->headers;
498
499         my $flavour = $headers->header('x-record-schema');
500         $flavour //= C4::Context->preference('marcflavour');
501
502         my $record;
503
504         my $frameworkcode = $headers->header('x-framework-id');
505         my $content_type  = $headers->content_type;
506
507         if ( $content_type =~ m/application\/marcxml\+xml/ ) {
508             $record = MARC::Record->new_from_xml( $c->req->body, 'UTF-8', $flavour );
509         }
510         elsif ( $content_type =~ m/application\/marc-in-json/ ) {
511             $record = MARC::Record->new_from_mij_structure( $c->req->json );
512         }
513         elsif ( $content_type =~ m/application\/marc/ ) {
514             $record = MARC::Record->new_from_usmarc( $c->req->body );
515         }
516         else {
517             return $c->render(
518                 status  => 406,
519                 openapi => [
520                     "application/marcxml+xml",
521                     "application/marc-in-json",
522                     "application/marc"
523                 ]
524             );
525         }
526
527         my ( $duplicatebiblionumber, $duplicatetitle );
528             ( $duplicatebiblionumber, $duplicatetitle ) = FindDuplicate($record);
529
530         my $confirm_not_duplicate = $headers->header('x-confirm-not-duplicate');
531
532         return $c->render(
533             status  => 400,
534             openapi => {
535                 error => "Duplicate biblio $duplicatebiblionumber",
536             }
537         ) unless !$duplicatebiblionumber || $confirm_not_duplicate;
538
539         my ( $biblionumber, $oldbibitemnum );
540             ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
541
542         $c->render(
543             status  => 200,
544             openapi => { id => $biblionumber }
545         );
546     }
547     catch {
548         $c->unhandled_exception($_);
549     };
550 }
551
552 =head3 update
553
554 Controller function that handles modifying an biblio object
555
556 =cut
557
558 sub update {
559     my $c = shift->openapi->valid_input or return;
560
561     my $biblio_id = $c->param('biblio_id');
562     my $biblio    = Koha::Biblios->find($biblio_id);
563
564     if ( ! defined $biblio ) {
565         return $c->render(
566             status  => 404,
567             openapi => { error => "Object not found" }
568         );
569     }
570
571     try {
572         my $headers = $c->req->headers;
573
574         my $flavour = $headers->header('x-record-schema');
575         $flavour //= C4::Context->preference('marcflavour');
576
577         my $frameworkcode = $headers->header('x-framework-id') || $biblio->frameworkcode;
578
579         my $content_type = $headers->content_type;
580
581         my $record;
582
583         if ( $content_type =~ m/application\/marcxml\+xml/ ) {
584             $record = MARC::Record->new_from_xml( $c->req->body, 'UTF-8', $flavour );
585         }
586         elsif ( $content_type =~ m/application\/marc-in-json/ ) {
587             $record = MARC::Record->new_from_mij_structure( $c->req->json );
588         }
589         elsif ( $content_type =~ m/application\/marc/ ) {
590             $record = MARC::Record->new_from_usmarc( $c->req->body );
591         }
592         else {
593             return $c->render(
594                 status  => 406,
595                 openapi => [
596                     "application/json",
597                     "application/marcxml+xml",
598                     "application/marc-in-json",
599                     "application/marc"
600                 ]
601             );
602         }
603
604         ModBiblio( $record, $biblio_id, $frameworkcode );
605
606         $c->render(
607             status  => 200,
608             openapi => { id => $biblio_id }
609         );
610     }
611     catch {
612         $c->unhandled_exception($_);
613     };
614 }
615
616 =head3 list
617
618 Controller function that handles retrieving a single biblio object
619
620 =cut
621
622 sub list {
623     my $c = shift->openapi->valid_input or return;
624
625     my $attributes;
626     $attributes =
627       { prefetch => ['metadata'] }    # don't prefetch metadata if not needed
628       unless $c->req->headers->accept =~ m/application\/json/;
629
630     my $biblios = $c->objects->search_rs( Koha::Biblios->new );
631
632     return try {
633
634         if ( $c->req->headers->accept =~ m/application\/json(;.*)?$/ ) {
635             return $c->render(
636                 status => 200,
637                 json   => $c->objects->to_api( $biblios ),
638             );
639         }
640         elsif (
641             $c->req->headers->accept =~ m/application\/marcxml\+xml(;.*)?$/ )
642         {
643             $c->res->headers->add( 'Content-Type', 'application/marcxml+xml' );
644             return $c->render(
645                 status => 200,
646                 text   => $biblios->print_collection('marcxml')
647             );
648         }
649         elsif (
650             $c->req->headers->accept =~ m/application\/marc-in-json(;.*)?$/ )
651         {
652             $c->res->headers->add( 'Content-Type', 'application/marc-in-json' );
653             return $c->render(
654                 status => 200,
655                 data   => $biblios->print_collection('mij')
656             );
657         }
658         elsif ( $c->req->headers->accept =~ m/application\/marc(;.*)?$/ ) {
659             $c->res->headers->add( 'Content-Type', 'application/marc' );
660             return $c->render(
661                 status => 200,
662                 text   => $biblios->print_collection('marc')
663             );
664         }
665         elsif ( $c->req->headers->accept =~ m/text\/plain(;.*)?$/ ) {
666             return $c->render(
667                 status => 200,
668                 text   => $biblios->print_collection('txt')
669             );
670         }
671         else {
672             return $c->render(
673                 status  => 406,
674                 openapi => [
675                     "application/json",         "application/marcxml+xml",
676                     "application/marc-in-json", "application/marc",
677                     "text/plain"
678                 ]
679             );
680         }
681     }
682     catch {
683         $c->unhandled_exception($_);
684     };
685 }
686
687 1;