1 package OpenILS::Application::Search::Biblio;
2 use base qw/OpenILS::Application/;
3 use strict; use warnings;
6 use OpenSRF::Utils::JSON;
7 use OpenILS::Utils::Fieldmapper;
8 use OpenILS::Utils::ModsParser;
9 use OpenSRF::Utils::SettingsClient;
10 use OpenILS::Utils::CStoreEditor q/:funcs/;
11 use OpenSRF::Utils::Cache;
16 use OpenSRF::Utils::Logger qw/:logger/;
18 use Time::HiRes qw(time sleep);
19 use OpenSRF::EX qw(:try);
20 use Digest::MD5 qw(md5_hex);
26 $Data::Dumper::Indent = 0;
28 use OpenILS::Const qw/:const/;
30 use OpenILS::Application::AppUtils;
31 my $apputils = "OpenILS::Application::AppUtils";
34 my $pfx = "open-ils.search_";
42 $cache = OpenSRF::Utils::Cache->new('global');
43 my $sclient = OpenSRF::Utils::SettingsClient->new();
44 $cache_timeout = $sclient->config_value(
45 "apps", "open-ils.search", "app_settings", "cache_timeout" ) || 300;
47 $superpage_size = $sclient->config_value(
48 "apps", "open-ils.search", "app_settings", "superpage_size" ) || 500;
50 $max_superpages = $sclient->config_value(
51 "apps", "open-ils.search", "app_settings", "max_superpages" ) || 20;
53 $logger->info("Search cache timeout is $cache_timeout, ".
54 " superpage_size is $superpage_size, max_superpages is $max_superpages");
59 # ---------------------------------------------------------------------------
60 # takes a list of record id's and turns the docs into friendly
61 # mods structures. Creates one MODS structure for each doc id.
62 # ---------------------------------------------------------------------------
63 sub _records_to_mods {
69 my $session = OpenSRF::AppSession->create("open-ils.cstore");
70 my $request = $session->request(
71 "open-ils.cstore.direct.biblio.record_entry.search", { id => \@ids } );
73 while( my $resp = $request->recv ) {
74 my $content = $resp->content;
75 next if $content->id == OILS_PRECAT_RECORD;
76 my $u = OpenILS::Utils::ModsParser->new(); # FIXME: we really need a new parser for each object?
77 $u->start_mods_batch( $content->marc );
78 my $mods = $u->finish_mods_batch();
79 $mods->doc_id($content->id());
80 $mods->tcn($content->tcn_value);
84 $session->disconnect();
88 __PACKAGE__->register_method(
89 method => "record_id_to_mods",
90 api_name => "open-ils.search.biblio.record.mods.retrieve",
93 desc => "Provide ID, we provide the MODS object with copy count. "
94 . "Note: this method does NOT take an array of IDs like mods_slim.retrieve", # FIXME: do it here too
96 { desc => 'Record ID', type => 'number' }
99 desc => 'MODS object', type => 'object'
104 # converts a record into a mods object with copy counts attached
105 sub record_id_to_mods {
107 my( $self, $client, $org_id, $id ) = @_;
109 my $mods_list = _records_to_mods( $id );
110 my $mods_obj = $mods_list->[0];
111 my $cmethod = $self->method_lookup("open-ils.search.biblio.record.copy_count");
112 my ($count) = $cmethod->run($org_id, $id);
113 $mods_obj->copy_count($count);
120 __PACKAGE__->register_method(
121 method => "record_id_to_mods_slim",
122 api_name => "open-ils.search.biblio.record.mods_slim.retrieve",
126 desc => "Provide ID(s), we provide the MODS",
128 { desc => 'Record ID or array of IDs' }
131 desc => 'MODS object(s), event on error'
136 # converts a record into a mods object with NO copy counts attached
137 sub record_id_to_mods_slim {
138 my( $self, $client, $id ) = @_;
139 return undef unless defined $id;
141 if(ref($id) and ref($id) == 'ARRAY') {
142 return _records_to_mods( @$id );
144 my $mods_list = _records_to_mods( $id );
145 my $mods_obj = $mods_list->[0];
146 return OpenILS::Event->new('BIBLIO_RECORD_ENTRY_NOT_FOUND') unless $mods_obj;
152 __PACKAGE__->register_method(
153 method => "record_id_to_mods_slim_batch",
154 api_name => "open-ils.search.biblio.record.mods_slim.batch.retrieve",
157 sub record_id_to_mods_slim_batch {
158 my($self, $conn, $id_list) = @_;
159 $conn->respond(_records_to_mods($_)->[0]) for @$id_list;
164 # Returns the number of copies attached to a record based on org location
165 __PACKAGE__->register_method(
166 method => "record_id_to_copy_count",
167 api_name => "open-ils.search.biblio.record.copy_count",
169 desc => q/Returns a copy summary for the given record for the context org
170 unit and all ancestor org units/,
172 {desc => 'Context org unit id', type => 'number'},
173 {desc => 'Record ID', type => 'number'}
176 desc => q/summary object per org unit in the set, where the set
177 includes the context org unit and all parent org units.
178 Object includes the keys "transcendant", "count", "org_unit", "depth",
179 "unshadow", "available". Each is a count, except "org_unit" which is
180 the context org unit and "depth" which is the depth of the context org unit
187 __PACKAGE__->register_method(
188 method => "record_id_to_copy_count",
189 api_name => "open-ils.search.biblio.record.copy_count.staff",
192 desc => q/Returns a copy summary for the given record for the context org
193 unit and all ancestor org units/,
195 {desc => 'Context org unit id', type => 'number'},
196 {desc => 'Record ID', type => 'number'}
199 desc => q/summary object per org unit in the set, where the set
200 includes the context org unit and all parent org units.
201 Object includes the keys "transcendant", "count", "org_unit", "depth",
202 "unshadow", "available". Each is a count, except "org_unit" which is
203 the context org unit and "depth" which is the depth of the context org unit
210 __PACKAGE__->register_method(
211 method => "record_id_to_copy_count",
212 api_name => "open-ils.search.biblio.metarecord.copy_count",
214 desc => q/Returns a copy summary for the given record for the context org
215 unit and all ancestor org units/,
217 {desc => 'Context org unit id', type => 'number'},
218 {desc => 'Record ID', type => 'number'}
221 desc => q/summary object per org unit in the set, where the set
222 includes the context org unit and all parent org units.
223 Object includes the keys "transcendant", "count", "org_unit", "depth",
224 "unshadow", "available". Each is a count, except "org_unit" which is
225 the context org unit and "depth" which is the depth of the context org unit
232 __PACKAGE__->register_method(
233 method => "record_id_to_copy_count",
234 api_name => "open-ils.search.biblio.metarecord.copy_count.staff",
236 desc => q/Returns a copy summary for the given record for the context org
237 unit and all ancestor org units/,
239 {desc => 'Context org unit id', type => 'number'},
240 {desc => 'Record ID', type => 'number'}
243 desc => q/summary object per org unit in the set, where the set
244 includes the context org unit and all parent org units.
245 Object includes the keys "transcendant", "count", "org_unit", "depth",
246 "unshadow", "available". Each is a count, except "org_unit" which is
247 the context org unit and "depth" which is the depth of the context org
248 unit. "depth" is always -1 when the count from a lasso search is
249 performed, since depth doesn't mean anything in a lasso context.
256 sub record_id_to_copy_count {
257 my( $self, $client, $org_id, $record_id ) = @_;
259 return [] unless $record_id;
261 my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
262 my $staff = $self->api_name =~ /staff/ ? 't' : 'f';
264 my $data = $U->cstorereq(
265 "open-ils.cstore.json_query.atomic",
266 { from => ['asset.' . $key . '_copy_count' => $org_id => $record_id => $staff] }
270 for my $d ( @$data ) { # fix up the key name change required by stored-proc version
271 $$d{count} = delete $$d{visible};
275 return [ sort { $a->{depth} <=> $b->{depth} } @count ];
278 __PACKAGE__->register_method(
279 method => "record_has_holdable_copy",
280 api_name => "open-ils.search.biblio.record.has_holdable_copy",
282 desc => q/Returns a boolean indicating if a record has any holdable copies./,
284 {desc => 'Record ID', type => 'number'}
287 desc => q/bool indicating if the record has any holdable copies/,
293 __PACKAGE__->register_method(
294 method => "record_has_holdable_copy",
295 api_name => "open-ils.search.biblio.metarecord.has_holdable_copy",
297 desc => q/Returns a boolean indicating if a record has any holdable copies./,
299 {desc => 'Record ID', type => 'number'}
302 desc => q/bool indicating if the record has any holdable copies/,
308 sub record_has_holdable_copy {
309 my($self, $client, $record_id ) = @_;
311 return 0 unless $record_id;
313 my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
315 my $data = $U->cstorereq(
316 "open-ils.cstore.json_query.atomic",
317 { from => ['asset.' . $key . '_has_holdable_copy' => $record_id ] }
320 return ${@$data[0]}{'asset.' . $key . '_has_holdable_copy'} eq 't';
324 __PACKAGE__->register_method(
325 method => "biblio_search_tcn",
326 api_name => "open-ils.search.biblio.tcn",
329 desc => "Retrieve related record ID(s) given a TCN",
331 { desc => 'TCN', type => 'string' },
332 { desc => 'Flag indicating to include deleted records', type => 'string' }
335 desc => 'Results object like: { "count": $i, "ids": [...] }',
342 sub biblio_search_tcn {
344 my( $self, $client, $tcn, $include_deleted ) = @_;
346 $tcn =~ s/^\s+|\s+$//og;
348 my $e = new_editor();
349 my $search = {tcn_value => $tcn};
350 $search->{deleted} = 'f' unless $include_deleted;
351 my $recs = $e->search_biblio_record_entry( $search, {idlist =>1} );
353 return { count => scalar(@$recs), ids => $recs };
357 # --------------------------------------------------------------------------------
359 __PACKAGE__->register_method(
360 method => "biblio_barcode_to_copy",
361 api_name => "open-ils.search.asset.copy.find_by_barcode",
363 sub biblio_barcode_to_copy {
364 my( $self, $client, $barcode ) = @_;
365 my( $copy, $evt ) = $U->fetch_copy_by_barcode($barcode);
370 __PACKAGE__->register_method(
371 method => "biblio_id_to_copy",
372 api_name => "open-ils.search.asset.copy.batch.retrieve",
374 sub biblio_id_to_copy {
375 my( $self, $client, $ids ) = @_;
376 $logger->info("Fetching copies @$ids");
377 return $U->cstorereq(
378 "open-ils.cstore.direct.asset.copy.search.atomic", { id => $ids } );
382 __PACKAGE__->register_method(
383 method => "biblio_id_to_uris",
384 api_name=> "open-ils.search.asset.uri.retrieve_by_bib",
388 @param BibID Which bib record contains the URIs
389 @param OrgID Where to look for URIs
390 @param OrgDepth Range adjustment for OrgID
391 @return A stream or list of 'auri' objects
395 sub biblio_id_to_uris {
396 my( $self, $client, $bib, $org, $depth ) = @_;
397 die "Org ID required" unless defined($org);
398 die "Bib ID required" unless defined($bib);
401 push @params, $depth if (defined $depth);
403 my $ids = $U->cstorereq( "open-ils.cstore.json_query.atomic",
404 { select => { auri => [ 'id' ] },
408 field => 'call_number',
414 filter => { active => 't' }
425 select => { aou => [ { column => 'id', transform => 'actor.org_unit_descendants', params => \@params, result_field => 'id' } ] },
427 where => { id => $org },
437 my $uris = $U->cstorereq(
438 "open-ils.cstore.direct.asset.uri.search.atomic",
439 { id => [ map { (values %$_) } @$ids ] }
442 $client->respond($_) for (@$uris);
448 __PACKAGE__->register_method(
449 method => "copy_retrieve",
450 api_name => "open-ils.search.asset.copy.retrieve",
453 desc => 'Retrieve a copy object based on the Copy ID',
455 { desc => 'Copy ID', type => 'number'}
458 desc => 'Copy object, event on error'
464 my( $self, $client, $cid ) = @_;
465 my( $copy, $evt ) = $U->fetch_copy($cid);
466 return $evt || $copy;
469 __PACKAGE__->register_method(
470 method => "volume_retrieve",
471 api_name => "open-ils.search.asset.call_number.retrieve"
473 sub volume_retrieve {
474 my( $self, $client, $vid ) = @_;
475 my $e = new_editor();
476 my $vol = $e->retrieve_asset_call_number($vid) or return $e->event;
480 __PACKAGE__->register_method(
481 method => "fleshed_copy_retrieve_batch",
482 api_name => "open-ils.search.asset.copy.fleshed.batch.retrieve",
486 sub fleshed_copy_retrieve_batch {
487 my( $self, $client, $ids ) = @_;
488 $logger->info("Fetching fleshed copies @$ids");
489 return $U->cstorereq(
490 "open-ils.cstore.direct.asset.copy.search.atomic",
493 flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries parts / ] }
498 __PACKAGE__->register_method(
499 method => "fleshed_copy_retrieve",
500 api_name => "open-ils.search.asset.copy.fleshed.retrieve",
503 sub fleshed_copy_retrieve {
504 my( $self, $client, $id ) = @_;
505 my( $c, $e) = $U->fetch_fleshed_copy($id);
510 __PACKAGE__->register_method(
511 method => 'fleshed_by_barcode',
512 api_name => "open-ils.search.asset.copy.fleshed2.find_by_barcode",
515 sub fleshed_by_barcode {
516 my( $self, $conn, $barcode ) = @_;
517 my $e = new_editor();
518 my $copyid = $e->search_asset_copy(
519 {barcode => $barcode, deleted => 'f'}, {idlist=>1})->[0]
521 return fleshed_copy_retrieve2( $self, $conn, $copyid);
525 __PACKAGE__->register_method(
526 method => "fleshed_copy_retrieve2",
527 api_name => "open-ils.search.asset.copy.fleshed2.retrieve",
531 sub fleshed_copy_retrieve2 {
532 my( $self, $client, $id ) = @_;
533 my $e = new_editor();
534 my $copy = $e->retrieve_asset_copy(
541 qw/ location status stat_cat_entry_copy_maps notes age_protect parts peer_record_maps /
543 ascecm => [qw/ stat_cat stat_cat_entry /],
547 ) or return $e->event;
549 # For backwards compatibility
550 #$copy->stat_cat_entries($copy->stat_cat_entry_copy_maps);
552 if( $copy->status->id == OILS_COPY_STATUS_CHECKED_OUT ) {
554 $e->search_action_circulation(
556 { target_copy => $copy->id },
558 order_by => { circ => 'xact_start desc' },
570 __PACKAGE__->register_method(
571 method => 'flesh_copy_custom',
572 api_name => 'open-ils.search.asset.copy.fleshed.custom',
576 sub flesh_copy_custom {
577 my( $self, $conn, $copyid, $fields ) = @_;
578 my $e = new_editor();
579 my $copy = $e->retrieve_asset_copy(
589 ) or return $e->event;
594 __PACKAGE__->register_method(
595 method => "biblio_barcode_to_title",
596 api_name => "open-ils.search.biblio.find_by_barcode",
599 sub biblio_barcode_to_title {
600 my( $self, $client, $barcode ) = @_;
602 my $title = $apputils->simple_scalar_request(
604 "open-ils.storage.biblio.record_entry.retrieve_by_barcode", $barcode );
606 return { ids => [ $title->id ], count => 1 } if $title;
607 return { count => 0 };
610 __PACKAGE__->register_method(
611 method => 'title_id_by_item_barcode',
612 api_name => 'open-ils.search.bib_id.by_barcode',
615 desc => 'Retrieve bib record id associated with the copy identified by the given barcode',
617 { desc => 'Item barcode', type => 'string' }
620 desc => 'Bib record id.'
625 __PACKAGE__->register_method(
626 method => 'title_id_by_item_barcode',
627 api_name => 'open-ils.search.multi_home.bib_ids.by_barcode',
630 desc => 'Retrieve bib record ids associated with the copy identified by the given barcode. This includes peer bibs for Multi-Home items.',
632 { desc => 'Item barcode', type => 'string' }
635 desc => 'Array of bib record ids. First element is the native bib for the item.'
641 sub title_id_by_item_barcode {
642 my( $self, $conn, $barcode ) = @_;
643 my $e = new_editor();
644 my $copies = $e->search_asset_copy(
646 { deleted => 'f', barcode => $barcode },
650 acp => [ 'call_number' ],
657 return $e->event unless @$copies;
659 if( $self->api_name =~ /multi_home/ ) {
660 my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
662 { target_copy => $$copies[0]->id }
665 my @temp = map { $_->peer_record } @{ $multi_home_list };
666 unshift @temp, $$copies[0]->call_number->record->id;
669 return $$copies[0]->call_number->record->id;
673 __PACKAGE__->register_method(
674 method => 'find_peer_bibs',
675 api_name => 'open-ils.search.peer_bibs.test',
678 desc => 'Tests to see if the specified record is a peer record.',
680 { desc => 'Biblio record entry Id', type => 'number' }
683 desc => 'True if specified id can be found in biblio.peer_bib_copy_map.peer_record.',
689 __PACKAGE__->register_method(
690 method => 'find_peer_bibs',
691 api_name => 'open-ils.search.peer_bibs',
694 desc => 'Return acps and mvrs for multi-home items linked to specified peer record.',
696 { desc => 'Biblio record entry Id', type => 'number' }
699 desc => '{ records => Array of mvrs, items => array of acps }',
706 my( $self, $client, $doc_id ) = @_;
707 my $e = new_editor();
709 my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
711 { peer_record => $doc_id },
715 bpbcm => [ 'target_copy', 'peer_type' ],
716 acp => [ 'call_number', 'location', 'status', 'peer_record_maps' ]
722 if ($self->api_name =~ /test/) {
723 return scalar( @{$multi_home_list} ) > 0 ? 1 : 0;
726 if (scalar(@{$multi_home_list})==0) {
730 # create a unique hash of the primary record MVRs for foreign copies
731 # XXX PLEASE let's change to unAPI2 (supports foreign copies) in the TT opac?!?
733 ($_->target_copy->call_number->record, _records_to_mods( $_->target_copy->call_number->record )->[0])
736 # set the foreign_copy_maps field to an empty array
737 map { $rec_hash{$_}->foreign_copy_maps([]) } keys( %rec_hash );
739 # push the maps onto the correct MVRs
740 for (@$multi_home_list) {
742 @{$rec_hash{ $_->target_copy->call_number->record }->foreign_copy_maps()},
747 return [sort {$a->title cmp $b->title} values(%rec_hash)];
750 __PACKAGE__->register_method(
751 method => "biblio_copy_to_mods",
752 api_name => "open-ils.search.biblio.copy.mods.retrieve",
755 # takes a copy object and returns it fleshed mods object
756 sub biblio_copy_to_mods {
757 my( $self, $client, $copy ) = @_;
759 my $volume = $U->cstorereq(
760 "open-ils.cstore.direct.asset.call_number.retrieve",
761 $copy->call_number() );
763 my $mods = _records_to_mods($volume->record());
764 $mods = shift @$mods;
765 $volume->copies([$copy]);
766 push @{$mods->call_numbers()}, $volume;
774 OpenILS::Application::Search::Biblio
780 =head3 open-ils.search.biblio.multiclass.query (arghash, query, docache)
782 For arghash and docache, see B<open-ils.search.biblio.multiclass>.
784 The query argument is a string, but built like a hash with key: value pairs.
785 Recognized search keys include:
787 keyword (kw) - search keyword(s) *
788 author (au) - search author(s) *
789 name (au) - same as author *
790 title (ti) - search title *
791 subject (su) - search subject *
792 series (se) - search series *
793 lang - limit by language (specify multiple langs with lang:l1 lang:l2 ...)
794 site - search at specified org unit, corresponds to actor.org_unit.shortname
795 pref_ou - extend search to specified org unit, corresponds to actor.org_unit.shortname
796 sort - sort type (title, author, pubdate)
797 dir - sort direction (asc, desc)
798 available - if set to anything other than "false" or "0", limits to available items
800 * Searching keyword, author, title, subject, and series supports additional search
801 subclasses, specified with a "|". For example, C<title|proper:gone with the wind>.
803 For more, see B<config.metabib_field>.
807 foreach (qw/open-ils.search.biblio.multiclass.query
808 open-ils.search.biblio.multiclass.query.staff
809 open-ils.search.metabib.multiclass.query
810 open-ils.search.metabib.multiclass.query.staff/)
812 __PACKAGE__->register_method(
814 method => 'multiclass_query',
816 desc => 'Perform a search query. The .staff version of the call includes otherwise hidden hits.',
818 {name => 'arghash', desc => 'Arg hash (see open-ils.search.biblio.multiclass)', type => 'object'},
819 {name => 'query', desc => 'Raw human-readable query (see perldoc '. __PACKAGE__ .')', type => 'string'},
820 {name => 'docache', desc => 'Flag for caching (see open-ils.search.biblio.multiclass)', type => 'object'},
823 desc => 'Search results from query, like: { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
824 type => 'object', # TODO: update as miker's new elements are included
830 sub multiclass_query {
831 # arghash only really supports limit/offset anymore
832 my($self, $conn, $arghash, $query, $docache) = @_;
836 $query =~ s/^\s+//go;
837 $query =~ s/\s+/ /go;
838 $arghash->{query} = $query
841 $logger->debug("initial search query => $query") if $query;
843 (my $method = $self->api_name) =~ s/\.query/.staged/o;
844 return $self->method_lookup($method)->dispatch($arghash, $docache);
848 __PACKAGE__->register_method(
849 method => 'cat_search_z_style_wrapper',
850 api_name => 'open-ils.search.biblio.zstyle',
852 signature => q/@see open-ils.search.biblio.multiclass/
855 __PACKAGE__->register_method(
856 method => 'cat_search_z_style_wrapper',
857 api_name => 'open-ils.search.biblio.zstyle.staff',
859 signature => q/@see open-ils.search.biblio.multiclass/
862 sub cat_search_z_style_wrapper {
865 my $authtoken = shift;
868 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
870 my $ou = $cstore->request(
871 'open-ils.cstore.direct.actor.org_unit.search',
872 { parent_ou => undef }
875 my $result = { service => 'native-evergreen-catalog', records => [] };
876 my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
878 $$searchhash{searches}{title}{term} = $$args{search}{title} if $$args{search}{title};
879 $$searchhash{searches}{author}{term} = $$args{search}{author} if $$args{search}{author};
880 $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
881 $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
882 $$searchhash{searches}{'identifier|isbn'}{term} = $$args{search}{isbn} if $$args{search}{isbn};
883 $$searchhash{searches}{'identifier|issn'}{term} = $$args{search}{issn} if $$args{search}{issn};
884 $$searchhash{searches}{'identifier|upc'}{term} = $$args{search}{upc} if $$args{search}{upc};
886 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
887 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
888 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
889 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
891 my $method = 'open-ils.search.biblio.multiclass.staged';
892 $method .= '.staff' if $self->api_name =~ /staff$/;
894 my ($list) = $self->method_lookup($method)->run( $searchhash );
896 if ($list->{count} > 0 and @{$list->{ids}}) {
897 $result->{count} = $list->{count};
899 my $records = $cstore->request(
900 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
901 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
904 for my $rec ( @$records ) {
906 my $u = OpenILS::Utils::ModsParser->new();
907 $u->start_mods_batch( $rec->marc );
908 my $mods = $u->finish_mods_batch();
910 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
916 $cstore->disconnect();
920 # ----------------------------------------------------------------------------
921 # These are the main OPAC search methods
922 # ----------------------------------------------------------------------------
924 __PACKAGE__->register_method(
925 method => 'the_quest_for_knowledge',
926 api_name => 'open-ils.search.biblio.multiclass',
928 desc => "Performs a multi class biblio or metabib search",
931 desc => "A search hash with keys: "
932 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
933 . "See perldoc " . __PACKAGE__ . " for more detail",
937 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
942 desc => 'An object of the form: '
943 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
948 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
950 The search-hash argument can have the following elements:
952 searches: { "$class" : "$value", ...} [REQUIRED]
953 org_unit: The org id to focus the search at
954 depth : The org depth
955 limit : The search limit default: 10
956 offset : The search offset default: 0
957 format : The MARC format
958 sort : What field to sort the results on? [ author | title | pubdate ]
959 sort_dir: What direction do we sort? [ asc | desc ]
960 tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
961 will be tagged with an additional value ("1") as the last value in the record ID array for
962 each record. Requires the 'authtoken'
963 authtoken : Authentication token string; When actions are performed that require a user login
964 (e.g. tagging circulated records), the authentication token is required
966 The searches element is required, must have a hashref value, and the hashref must contain at least one
967 of the following classes as a key:
975 The value paired with a key is the associated search string.
977 The docache argument enables/disables searching and saving results in cache (default OFF).
979 The return object, if successful, will look like:
981 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
985 __PACKAGE__->register_method(
986 method => 'the_quest_for_knowledge',
987 api_name => 'open-ils.search.biblio.multiclass.staff',
988 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
990 __PACKAGE__->register_method(
991 method => 'the_quest_for_knowledge',
992 api_name => 'open-ils.search.metabib.multiclass',
993 signature => q/@see open-ils.search.biblio.multiclass/
995 __PACKAGE__->register_method(
996 method => 'the_quest_for_knowledge',
997 api_name => 'open-ils.search.metabib.multiclass.staff',
998 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1001 sub the_quest_for_knowledge {
1002 my( $self, $conn, $searchhash, $docache ) = @_;
1004 return { count => 0 } unless $searchhash and
1005 ref $searchhash->{searches} eq 'HASH';
1007 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
1011 if($self->api_name =~ /metabib/) {
1013 $method =~ s/biblio/metabib/o;
1016 # do some simple sanity checking
1017 if(!$searchhash->{searches} or
1018 ( !grep { /^(?:title|author|subject|series|keyword|identifier\|is[bs]n)/ } keys %{$searchhash->{searches}} ) ) {
1019 return { count => 0 };
1022 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
1023 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
1024 my $end = $offset + $limit - 1;
1026 my $maxlimit = 5000;
1027 $searchhash->{offset} = 0; # possible user value overwritten in hash
1028 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
1030 return { count => 0 } if $offset > $maxlimit;
1033 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
1034 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
1035 my $ckey = $pfx . md5_hex($method . $s);
1037 $logger->info("bib search for: $s");
1039 $searchhash->{limit} -= $offset;
1043 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1047 $method .= ".staff" if($self->api_name =~ /staff/);
1048 $method .= ".atomic";
1050 for (keys %$searchhash) {
1051 delete $$searchhash{$_}
1052 unless defined $$searchhash{$_};
1055 $result = $U->storagereq( $method, %$searchhash );
1059 $docache = 0; # results came FROM cache, so we don't write back
1062 return {count => 0} unless ($result && $$result[0]);
1066 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1069 # If we didn't get this data from the cache, put it into the cache
1070 # then return the correct offset of records
1071 $logger->debug("putting search cache $ckey\n");
1072 put_cache($ckey, $count, \@recs);
1076 # if we have the full set of data, trim out
1077 # the requested chunk based on limit and offset
1079 for ($offset..$end) {
1080 last unless $recs[$_];
1081 push(@t, $recs[$_]);
1086 return { ids => \@recs, count => $count };
1090 __PACKAGE__->register_method(
1091 method => 'staged_search',
1092 api_name => 'open-ils.search.biblio.multiclass.staged',
1094 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1095 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1098 desc => "A search hash with keys: "
1099 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1100 . "See perldoc " . __PACKAGE__ . " for more detail",
1104 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1109 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1110 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1115 __PACKAGE__->register_method(
1116 method => 'staged_search',
1117 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1118 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1120 __PACKAGE__->register_method(
1121 method => 'staged_search',
1122 api_name => 'open-ils.search.metabib.multiclass.staged',
1123 signature => q/@see open-ils.search.biblio.multiclass.staged/
1125 __PACKAGE__->register_method(
1126 method => 'staged_search',
1127 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1128 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1131 my $estimation_strategy;
1133 my($self, $conn, $search_hash, $docache) = @_;
1135 my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1137 my $method = $IAmMetabib?
1138 'open-ils.storage.metabib.multiclass.staged.search_fts':
1139 'open-ils.storage.biblio.multiclass.staged.search_fts';
1141 $method .= '.staff' if $self->api_name =~ /staff$/;
1142 $method .= '.atomic';
1144 if (!$search_hash->{query}) {
1145 return {count => 0} unless (
1147 $search_hash->{searches} and
1148 scalar( keys %{$search_hash->{searches}} ));
1151 my $search_duration;
1152 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1153 my $user_limit = $search_hash->{limit} || 10;
1154 my $ignore_facet_classes = $search_hash->{ignore_facet_classes};
1155 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1156 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1159 # we're grabbing results on a per-superpage basis, which means the
1160 # limit and offset should coincide with superpage boundaries
1161 $search_hash->{offset} = 0;
1162 $search_hash->{limit} = $superpage_size;
1164 # force a well-known check_limit
1165 $search_hash->{check_limit} = $superpage_size;
1166 # restrict total tested to superpage size * number of superpages
1167 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1169 # Set the configured estimation strategy, defaults to 'inclusion'.
1170 unless ($estimation_strategy) {
1171 $estimation_strategy = OpenSRF::Utils::SettingsClient
1174 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1177 $search_hash->{estimation_strategy} = $estimation_strategy;
1179 # pull any existing results from the cache
1180 my $key = search_cache_key($method, $search_hash);
1181 my $facet_key = $key.'_facets';
1182 my $cache_data = $cache->get_cache($key) || {};
1184 # First, we want to make sure that someone else isn't currently trying to perform exactly
1185 # this same search. The point is to allow just one instance of a search to fill the needs
1186 # of all concurrent, identical searches. This will avoid spammy searches killing the
1187 # database without requiring admins to start locking some IP addresses out entirely.
1189 # There's still a tiny race condition where 2 might run, but without sigificantly more code
1190 # and complexity, this is close to the best we can do.
1192 if ($cache_data->{running}) { # someone is already doing the search...
1193 my $stop_looping = time() + $cache_timeout;
1194 while ( sleep(1) and time() < $stop_looping ) { # sleep for a second ... maybe they'll finish
1195 $cache_data = $cache->get_cache($key) || {};
1196 last if (!$cache_data->{running});
1198 } elsif (!$cache_data->{0}) { # we're the first ... let's give it a try
1199 $cache->put_cache($key, { running => $$ }, $cache_timeout / 3);
1202 # keep retrieving results until we find enough to
1203 # fulfill the user-specified limit and offset
1204 my $all_results = [];
1205 my $page; # current superpage
1206 my $current_page_summary = {};
1207 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1210 for($page = 0; $page < $max_superpages; $page++) {
1212 my $data = $cache_data->{$page};
1216 $logger->debug("staged search: analyzing superpage $page");
1219 # this window of results is already cached
1220 $logger->debug("staged search: found cached results");
1221 $summary = $data->{summary};
1222 $results = $data->{results};
1225 # retrieve the window of results from the database
1226 $logger->debug("staged search: fetching results from the database");
1227 $search_hash->{skip_check} = $page * $superpage_size;
1228 $search_hash->{return_query} = $page == 0 ? 1 : 0;
1231 $results = $U->storagereq($method, %$search_hash);
1232 $search_duration = time - $start;
1233 $summary = shift(@$results) if $results;
1236 $logger->info("search timed out: duration=$search_duration: params=".
1237 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1238 return {count => 0};
1241 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1243 # Create backwards-compatible result structures
1245 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}, $_->{rel}, $_->{record}]} @$results];
1247 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}]} @$results];
1250 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1251 $results = [grep {defined $_->[0]} @$results];
1252 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1255 tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib)
1256 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1258 $current_page_summary = $summary;
1260 # add the new set of results to the set under construction
1261 push(@$all_results, @$results);
1263 my $current_count = scalar(@$all_results);
1265 if ($page == 0) { # all summaries are the same, just get the first
1266 for (keys %$summary) {
1267 $global_summary->{$_} = $summary->{$_};
1271 # we've found all the possible hits
1272 last if $current_count == $summary->{visible};
1274 # we've found enough results to satisfy the requested limit/offset
1275 last if $current_count >= ($user_limit + $user_offset);
1277 # we've scanned all possible hits
1278 last if($summary->{checked} < $superpage_size);
1281 # Let other backends grab our data now that we're done.
1282 $cache_data = $cache->get_cache($key);
1283 if ($$cache_data{running} and $$cache_data{running} == $$) {
1284 delete $$cache_data{running};
1285 $cache->put_cache($key, $cache_data, $cache_timeout);
1288 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1290 $conn->respond_complete(
1292 global_summary => $global_summary,
1293 count => $global_summary->{visible},
1294 core_limit => $search_hash->{core_limit},
1296 superpage_size => $search_hash->{check_limit},
1297 superpage_summary => $current_page_summary,
1298 facet_key => $facet_key,
1303 $logger->info("Completed canonicalized search is: $$global_summary{canonicalized_query}");
1305 return cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1308 sub fetch_display_fields {
1311 my $highlight_map = shift;
1315 $conn->respond_complete;
1319 my $hl_map_string = "";
1320 if (ref($highlight_map) =~ /HASH/) {
1321 for my $tsq (keys %$highlight_map) {
1322 my $field_list = join(',', @{$$highlight_map{$tsq}});
1323 $hl_map_string .= ' || ' if $hl_map_string;
1324 $hl_map_string .= "hstore(($tsq)\:\:TEXT,'$field_list')";
1328 my $e = new_editor();
1330 for my $record ( @records ) {
1331 next unless ($record && $hl_map_string);
1334 {from => ['search.highlight_display_fields', $record, $hl_map_string]}
1341 __PACKAGE__->register_method(
1342 method => 'fetch_display_fields',
1343 api_name => 'open-ils.search.fetch.metabib.display_field.highlight',
1348 sub tag_circulated_records {
1349 my ($auth, $results, $metabib) = @_;
1350 my $e = new_editor(authtoken => $auth);
1351 return $results unless $e->checkauth;
1354 select => { acn => [{ column => 'record', alias => 'tagme' }] },
1355 from => { auch => { acp => { join => 'acn' }} },
1356 where => { usr => $e->requestor->id },
1362 select => { mmrsm => [{ column => 'metarecord', alias => 'tagme' }] },
1364 where => { source => { in => $query } },
1369 # Give me the distinct set of bib records that exist in the user's visible circulation history
1370 my $circ_recs = $e->json_query( $query );
1372 # if the record appears in the circ history, push a 1 onto
1373 # the rec array structure to indicate truthiness
1374 for my $rec (@$results) {
1375 push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1381 # creates a unique token to represent the query in the cache
1382 sub search_cache_key {
1384 my $search_hash = shift;
1386 for my $key (sort keys %$search_hash) {
1387 push(@sorted, ($key => $$search_hash{$key}))
1388 unless $key eq 'limit' or
1390 $key eq 'skip_check';
1392 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1393 return $pfx . md5_hex($method . $s);
1396 sub retrieve_cached_facets {
1402 return undef unless ($key and $key =~ /_facets$/);
1405 local $SIG{ALRM} = sub {die};
1406 alarm(10); # we'll sleep for as much as 10s
1408 die if $cache->get_cache($key . '_COMPLETE');
1409 } while (sleep(0.05));
1414 my $blob = $cache->get_cache($key) || {};
1418 for my $f ( keys %$blob ) {
1419 my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1420 @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1421 for my $s ( @sorted ) {
1422 my ($k) = keys(%$s);
1423 my ($v) = values(%$s);
1424 $$facets{$f}{$k} = $v;
1434 __PACKAGE__->register_method(
1435 method => "retrieve_cached_facets",
1436 api_name => "open-ils.search.facet_cache.retrieve",
1438 desc => 'Returns facet data derived from a specific search based on a key '.
1439 'generated by open-ils.search.biblio.multiclass.staged and friends.',
1442 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1447 desc => 'Two level hash of facet values. Top level key is the facet id defined on the config.metabib_field table. '.
1448 'Second level key is a string facet value. Datum attached to each facet value is the number of distinct records, '.
1449 'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1450 'facet retrieval. These counts are calculated for all superpages that have been checked for visibility.',
1458 # add facets for this search to the facet cache
1459 my($key, $results, $metabib, $ignore) = @_;
1460 my $data = $cache->get_cache($key);
1463 return undef unless (@$results);
1465 my $facets_function = $metabib ? 'search.facets_for_metarecord_set'
1466 : 'search.facets_for_record_set';
1467 my $results_str = '{' . join(',', @$results) . '}';
1468 my $ignore_str = ref($ignore) ? '{' . join(',', @$ignore) . '}'
1471 from => [ $facets_function, $ignore_str, $results_str ]
1474 my $facets = OpenILS::Utils::CStoreEditor->new->json_query($query, {substream => 1});
1476 for my $facet (@$facets) {
1477 next unless ($facet->{value});
1478 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1481 $logger->info("facet compilation: cached with key=$key");
1483 $cache->put_cache($key, $data, $cache_timeout);
1484 $cache->put_cache($key.'_COMPLETE', 1, $cache_timeout);
1487 sub cache_staged_search_page {
1488 # puts this set of results into the cache
1489 my($key, $page, $summary, $results) = @_;
1490 my $data = $cache->get_cache($key);
1493 summary => $summary,
1497 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1498 ($summary->{estimated_hit_count} || "none") .
1499 ", visible=" . ($summary->{visible} || "none")
1502 $cache->put_cache($key, $data, $cache_timeout);
1510 my $start = $offset;
1511 my $end = $offset + $limit - 1;
1513 $logger->debug("searching cache for $key : $start..$end\n");
1515 return undef unless $cache;
1516 my $data = $cache->get_cache($key);
1518 return undef unless $data;
1520 my $count = $data->[0];
1523 return undef unless $offset < $count;
1526 for( my $i = $offset; $i <= $end; $i++ ) {
1527 last unless my $d = $$data[$i];
1528 push( @result, $d );
1531 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1538 my( $key, $count, $data ) = @_;
1539 return undef unless $cache;
1540 $logger->debug("search_cache putting ".
1541 scalar(@$data)." items at key $key with timeout $cache_timeout");
1542 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1546 __PACKAGE__->register_method(
1547 method => "biblio_mrid_to_modsbatch_batch",
1548 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1551 sub biblio_mrid_to_modsbatch_batch {
1552 my( $self, $client, $mrids) = @_;
1553 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1555 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1556 for my $id (@$mrids) {
1557 next unless defined $id;
1558 my ($m) = $method->run($id);
1565 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1566 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1568 __PACKAGE__->register_method(
1569 method => "biblio_mrid_to_modsbatch",
1572 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1573 . "As usual, the .staff version of this method will include otherwise hidden records.",
1575 { desc => 'Metarecord ID', type => 'number' },
1576 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1579 desc => 'MVR Object, event on error',
1585 sub biblio_mrid_to_modsbatch {
1586 my( $self, $client, $mrid, $args) = @_;
1588 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1590 my ($mr, $evt) = _grab_metarecord($mrid);
1591 return $evt unless $mr;
1593 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1594 biblio_mrid_make_modsbatch($self, $client, $mr);
1596 return $mvr unless ref($args);
1598 # Here we find the lead record appropriate for the given filters
1599 # and use that for the title and author of the metarecord
1600 my $format = $$args{format};
1601 my $org = $$args{org};
1602 my $depth = $$args{depth};
1604 return $mvr unless $format or $org or $depth;
1606 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1607 $method = "$method.staff" if $self->api_name =~ /staff/o;
1609 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1611 if( my $mods = $U->record_to_mvr($rec) ) {
1613 $mvr->title( $mods->title );
1614 $mvr->author($mods->author);
1615 $logger->debug("mods_slim updating title and ".
1616 "author in mvr with ".$mods->title." : ".$mods->author);
1622 # converts a metarecord to an mvr
1625 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1626 return Fieldmapper::metabib::virtual_record->new($perl);
1629 # checks to see if a metarecord has mods, if so returns true;
1631 __PACKAGE__->register_method(
1632 method => "biblio_mrid_check_mvr",
1633 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1634 notes => "Takes a metarecord ID or a metarecord object and returns true "
1635 . "if the metarecord already has an mvr associated with it."
1638 sub biblio_mrid_check_mvr {
1639 my( $self, $client, $mrid ) = @_;
1643 if(ref($mrid)) { $mr = $mrid; }
1644 else { ($mr, $evt) = _grab_metarecord($mrid); }
1645 return $evt if $evt;
1647 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1649 return _mr_to_mvr($mr) if $mr->mods();
1653 sub _grab_metarecord {
1655 my $e = new_editor();
1656 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1661 __PACKAGE__->register_method(
1662 method => "biblio_mrid_make_modsbatch",
1663 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1664 notes => "Takes either a metarecord ID or a metarecord object. "
1665 . "Forces the creations of an mvr for the given metarecord. "
1666 . "The created mvr is returned."
1669 sub biblio_mrid_make_modsbatch {
1670 my( $self, $client, $mrid ) = @_;
1672 my $e = new_editor();
1679 $mr = $e->retrieve_metabib_metarecord($mrid)
1680 or return $e->event;
1683 my $masterid = $mr->master_record;
1684 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1686 my $ids = $U->storagereq(
1687 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1688 return undef unless @$ids;
1690 my $master = $e->retrieve_biblio_record_entry($masterid)
1691 or return $e->event;
1693 # start the mods batch
1694 my $u = OpenILS::Utils::ModsParser->new();
1695 $u->start_mods_batch( $master->marc );
1697 # grab all of the sub-records and shove them into the batch
1698 my @ids = grep { $_ ne $masterid } @$ids;
1699 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1704 my $r = $e->retrieve_biblio_record_entry($i);
1705 push( @$subrecs, $r ) if $r;
1710 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1711 $u->push_mods_batch( $_->marc ) if $_->marc;
1715 # finish up and send to the client
1716 my $mods = $u->finish_mods_batch();
1717 $mods->doc_id($mrid);
1718 $client->respond_complete($mods);
1721 # now update the mods string in the db
1722 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1725 $e = new_editor(xact => 1);
1726 $e->update_metabib_metarecord($mr)
1727 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1734 # converts a mr id into a list of record ids
1736 foreach (qw/open-ils.search.biblio.metarecord_to_records
1737 open-ils.search.biblio.metarecord_to_records.staff/)
1739 __PACKAGE__->register_method(
1740 method => "biblio_mrid_to_record_ids",
1743 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1744 . "As usual, the .staff version of this method will include otherwise hidden records.",
1746 { desc => 'Metarecord ID', type => 'number' },
1747 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1750 desc => 'Results object like {count => $i, ids =>[...]}',
1758 sub biblio_mrid_to_record_ids {
1759 my( $self, $client, $mrid, $args ) = @_;
1761 my $format = $$args{format};
1762 my $org = $$args{org};
1763 my $depth = $$args{depth};
1765 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1766 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1767 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1769 return { count => scalar(@$recs), ids => $recs };
1773 __PACKAGE__->register_method(
1774 method => "biblio_record_to_marc_html",
1775 api_name => "open-ils.search.biblio.record.html"
1778 __PACKAGE__->register_method(
1779 method => "biblio_record_to_marc_html",
1780 api_name => "open-ils.search.authority.to_html"
1783 # Persistent parsers and setting objects
1784 my $parser = XML::LibXML->new();
1785 my $xslt = XML::LibXSLT->new();
1787 my $slim_marc_sheet;
1788 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1790 sub biblio_record_to_marc_html {
1791 my($self, $client, $recordid, $slim, $marcxml) = @_;
1794 my $dir = $settings_client->config_value("dirs", "xsl");
1797 unless($slim_marc_sheet) {
1798 my $xsl = $settings_client->config_value(
1799 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1801 $xsl = $parser->parse_file("$dir/$xsl");
1802 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1805 $sheet = $slim_marc_sheet;
1809 unless($marc_sheet) {
1810 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1811 my $xsl = $settings_client->config_value(
1812 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1813 $xsl = $parser->parse_file("$dir/$xsl");
1814 $marc_sheet = $xslt->parse_stylesheet($xsl);
1816 $sheet = $marc_sheet;
1821 my $e = new_editor();
1822 if($self->api_name =~ /authority/) {
1823 $record = $e->retrieve_authority_record_entry($recordid)
1824 or return $e->event;
1826 $record = $e->retrieve_biblio_record_entry($recordid)
1827 or return $e->event;
1829 $marcxml = $record->marc;
1832 my $xmldoc = $parser->parse_string($marcxml);
1833 my $html = $sheet->transform($xmldoc);
1834 return $html->documentElement->toString();
1837 __PACKAGE__->register_method(
1838 method => "send_event_email_output",
1839 api_name => "open-ils.search.biblio.record.email.send_output",
1841 sub send_event_email_output {
1842 my($self, $client, $auth, $event_id, $capkey, $capanswer) = @_;
1843 return undef unless $event_id;
1845 my $captcha_pass = 0;
1848 $real_answer = $cache->get_cache(md5_hex($capkey));
1849 $captcha_pass++ if ($real_answer eq $capanswer);
1852 my $e = new_editor(authtoken => $auth);
1853 return $e->die_event unless $captcha_pass || $e->checkauth;
1855 my $event = $e->retrieve_action_trigger_event([$event_id,{flesh => 1, flesh_fields => { atev => ['template_output']}}]);
1856 return undef unless ($event and $event->template_output);
1858 my $smtp = OpenSRF::Utils::SettingsClient
1860 ->config_value('email_notify', 'smtp_server');
1862 my $sender = Email::Send->new({mailer => 'SMTP'});
1863 $sender->mailer_args([Host => $smtp]);
1868 my $email = Email::Simple->new($event->template_output->data);
1870 for my $hfield (qw/From To Subject Bcc Cc Reply-To Sender/) {
1871 my @headers = $email->header($hfield);
1872 $email->header_set($hfield => map { encode("MIME-Header", $_) } @headers) if ($headers[0]);
1875 $email->header_set('MIME-Version' => '1.0');
1876 $email->header_set('Content-Type' => "text/plain; charset=UTF-8");
1877 $email->header_set('Content-Transfer-Encoding' => '8bit');
1880 $stat = $sender->send($email);
1881 } catch Error with {
1882 $err = $stat = shift;
1883 $logger->error("resend_at_email: Email failed with error: $err");
1889 __PACKAGE__->register_method(
1890 method => "format_biblio_record_entry",
1891 api_name => "open-ils.search.biblio.record.print.preview",
1894 __PACKAGE__->register_method(
1895 method => "format_biblio_record_entry",
1896 api_name => "open-ils.search.biblio.record.email.preview",
1899 __PACKAGE__->register_method(
1900 method => "format_biblio_record_entry",
1901 api_name => "open-ils.search.biblio.record.print",
1903 desc => 'Returns a printable version of the specified bib record',
1905 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1906 { desc => 'Context library for holdings, if applicable' => 'number' },
1907 { desc => 'Sort order, if applicable' => 'string' },
1908 { desc => 'Definition Group Member id' => 'number' },
1911 desc => q/An action_trigger.event object or error event./,
1916 __PACKAGE__->register_method(
1917 method => "format_biblio_record_entry",
1918 api_name => "open-ils.search.biblio.record.email",
1920 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1922 { desc => 'Authentication token', type => 'string'},
1923 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1924 { desc => 'Context library for holdings, if applicable' => 'number' },
1925 { desc => 'Sort order, if applicable' => 'string' },
1926 { desc => 'Sort direction, if applicable' => 'string' },
1927 { desc => 'Definition Group Member id' => 'number' },
1928 { desc => 'Whether to bypass auth due to captcha' => 'bool' },
1929 { desc => 'Email address, if none for the user' => 'string' },
1930 { desc => 'Subject, if customized' => 'string' },
1933 desc => q/Undefined on success, otherwise an error event./,
1939 sub format_biblio_record_entry {
1940 my($self, $conn, $arg1, $arg2, $arg3, $arg4, $arg5, $arg6, $captcha_pass, $email, $subject) = @_;
1942 my $for_print = ($self->api_name =~ /print/);
1943 my $for_email = ($self->api_name =~ /email/);
1944 my $preview = ($self->api_name =~ /preview/);
1946 my $e; my $auth; my $bib_id; my $context_org; my $holdings_context; my $bib_sort; my $group_member; my $type = 'brief'; my $sort_dir;
1950 $context_org = $arg2 || $U->get_org_tree->id;
1951 $holdings_context = $context_org;
1952 $bib_sort = $arg3 || 'author';
1953 $sort_dir = $arg4 || 'ascending';
1954 $group_member = $arg5;
1955 $e = new_editor(xact => 1);
1956 } elsif ($for_email) {
1959 $bib_sort = $arg4 || 'author';
1960 $sort_dir = $arg5 || 'ascending';
1961 $group_member = $arg6;
1962 $e = new_editor(authtoken => $auth, xact => 1);
1963 return $e->die_event unless $captcha_pass || $e->checkauth;
1964 $holdings_context = $arg3 || $U->get_org_tree->id;
1965 $context_org = $e->requestor ? $e->requestor->home_ou : $arg3;
1966 $email ||= $e->requestor ? $e->requestor->email : '';
1969 if ($group_member) {
1970 $group_member = $e->retrieve_action_trigger_event_def_group_member($group_member);
1971 if ($group_member and $U->is_true($group_member->holdings)) {
1976 $holdings_context = $e->retrieve_actor_org_unit($holdings_context);
1979 if (ref $bib_id ne 'ARRAY') {
1980 $bib_ids = [ $bib_id ];
1985 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1986 $bucket->btype('temp');
1987 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1989 $bucket->owner($e->requestor || 1)
1993 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1995 for my $id (@$bib_ids) {
1997 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1999 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
2000 $bucket_entry->target_biblio_record_entry($bib);
2001 $bucket_entry->bucket($bucket_obj->id);
2002 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
2010 subject => $subject,
2011 context_org => $holdings_context->shortname,
2012 sort_by => $bib_sort,
2013 sort_dir => $sort_dir,
2019 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org, undef, [ $usr_data ]);
2021 } elsif ($for_email) {
2023 return $U->fire_object_event(undef, 'biblio.format.record_entry.email', [ $bucket ], $context_org, undef, [ $usr_data ])
2026 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, $usr_data, 1);
2033 __PACKAGE__->register_method(
2034 method => "retrieve_all_copy_statuses",
2035 api_name => "open-ils.search.config.copy_status.retrieve.all"
2038 sub retrieve_all_copy_statuses {
2039 my( $self, $client ) = @_;
2040 return new_editor()->retrieve_all_config_copy_status();
2044 __PACKAGE__->register_method(
2045 method => "copy_counts_per_org",
2046 api_name => "open-ils.search.biblio.copy_counts.retrieve"
2049 __PACKAGE__->register_method(
2050 method => "copy_counts_per_org",
2051 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
2054 sub copy_counts_per_org {
2055 my( $self, $client, $record_id ) = @_;
2057 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
2059 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
2060 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
2062 my $counts = $apputils->simple_scalar_request(
2063 "open-ils.storage", $method, $record_id );
2065 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
2070 __PACKAGE__->register_method(
2071 method => "copy_count_summary",
2072 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
2073 notes => "returns an array of these: "
2074 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2075 . "where statusx is a copy status name. The statuses are sorted by ID.",
2079 sub copy_count_summary {
2080 my( $self, $client, $rid, $org, $depth ) = @_;
2083 my $data = $U->storagereq(
2084 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
2087 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2089 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2093 __PACKAGE__->register_method(
2094 method => "copy_location_count_summary",
2095 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
2096 notes => "returns an array of these: "
2097 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
2098 . "where statusx is a copy status name. The statuses are sorted by ID.",
2101 sub copy_location_count_summary {
2102 my( $self, $client, $rid, $org, $depth ) = @_;
2105 my $data = $U->storagereq(
2106 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2109 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2111 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2113 || $a->[4] cmp $b->[4]
2117 __PACKAGE__->register_method(
2118 method => "copy_count_location_summary",
2119 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2120 notes => "returns an array of these: "
2121 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2122 . "where statusx is a copy status name. The statuses are sorted by ID."
2125 sub copy_count_location_summary {
2126 my( $self, $client, $rid, $org, $depth ) = @_;
2129 my $data = $U->storagereq(
2130 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2132 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2134 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2139 foreach (qw/open-ils.search.biblio.marc
2140 open-ils.search.biblio.marc.staff/)
2142 __PACKAGE__->register_method(
2143 method => "marc_search",
2146 desc => 'Fetch biblio IDs based on MARC record criteria. '
2147 . 'As usual, the .staff version of the search includes otherwise hidden records',
2150 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2151 'See perldoc ' . __PACKAGE__ . ' for more detail.',
2154 {desc => 'timeout (optional)', type => 'number'}
2157 desc => 'Results object like: { "count": $i, "ids": [...] }',
2164 =head3 open-ils.search.biblio.marc (arghash, timeout)
2166 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
2168 searches: complex query object (required)
2169 org_unit: The org ID to focus the search at
2170 depth : The org depth
2171 limit : integer search limit default: 10
2172 offset : integer search offset default: 0
2173 sort : What field to sort the results on? [ author | title | pubdate ]
2174 sort_dir: In what direction do we sort? [ asc | desc ]
2176 Additional keys to refine search criteria:
2179 language : Language (code)
2180 lit_form : Literary form
2181 item_form: Item form
2182 item_type: Item type
2183 format : The MARC format
2185 Please note that the specific strings to be used in the "addtional keys" will be entirely
2186 dependent on your loaded data.
2188 All keys except "searches" are optional.
2189 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
2191 For example, an arg hash might look like:
2213 The arghash is eventually passed to the SRF call:
2214 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2216 Presently, search uses the cache unconditionally.
2220 # FIXME: that example above isn't actually tested.
2221 # FIXME: sort and limit added. item_type not tested yet.
2222 # TODO: docache option?
2224 my( $self, $conn, $args, $timeout ) = @_;
2226 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2227 $method .= ".staff" if $self->api_name =~ /staff/;
2228 $method .= ".atomic";
2230 my $limit = $args->{limit} || 10;
2231 my $offset = $args->{offset} || 0;
2233 # allow caller to pass in a call timeout since MARC searches
2234 # can take longer than the default 60-second timeout.
2235 # Default to 2 mins. Arbitrarily cap at 5 mins.
2236 $timeout = 120 if !$timeout or $timeout > 300;
2239 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2240 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2242 my $recs = search_cache($ckey, $offset, $limit);
2246 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2247 my $req = $ses->request($method, %$args);
2248 my $resp = $req->recv($timeout);
2250 if($resp and $recs = $resp->content) {
2251 put_cache($ckey, scalar(@$recs), $recs);
2260 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2261 my @recs = map { $_->[0] } @$recs;
2263 return { ids => \@recs, count => $count };
2267 foreach my $isbn_method (qw/
2268 open-ils.search.biblio.isbn
2269 open-ils.search.biblio.isbn.staff
2271 __PACKAGE__->register_method(
2272 method => "biblio_search_isbn",
2273 api_name => $isbn_method,
2275 desc => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2277 {desc => 'ISBN', type => 'string'}
2280 desc => 'Results object like: { "count": $i, "ids": [...] }',
2287 sub biblio_search_isbn {
2288 my( $self, $client, $isbn ) = @_;
2289 $logger->debug("Searching ISBN $isbn");
2290 # the previous implementation of this method was essentially unlimited,
2291 # so we will set our limit very high and let multiclass.query provide any
2293 # XXX: if making this unlimited is deemed important, we might consider
2294 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2295 # which is functionally deprecated at this point, or a custom call to
2296 # 'open-ils.storage.biblio.multiclass.search_fts'
2298 my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2299 if ($self->api_name =~ m/.staff$/) {
2300 $isbn_method .= '.staff';
2303 my $method = $self->method_lookup($isbn_method);
2304 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2305 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2306 return { ids => \@recs, count => $search_result->{'count'} };
2309 __PACKAGE__->register_method(
2310 method => "biblio_search_isbn_batch",
2311 api_name => "open-ils.search.biblio.isbn_list",
2314 # XXX: see biblio_search_isbn() for note concerning 'limit'
2315 sub biblio_search_isbn_batch {
2316 my( $self, $client, $isbn_list ) = @_;
2317 $logger->debug("Searching ISBNs @$isbn_list");
2318 my @recs = (); my %rec_set = ();
2319 my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2320 foreach my $isbn ( @$isbn_list ) {
2321 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2322 my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2323 foreach my $rec (@recs_subset) {
2324 if (! $rec_set{ $rec }) {
2325 $rec_set{ $rec } = 1;
2330 return { ids => \@recs, count => scalar(@recs) };
2333 foreach my $issn_method (qw/
2334 open-ils.search.biblio.issn
2335 open-ils.search.biblio.issn.staff
2337 __PACKAGE__->register_method(
2338 method => "biblio_search_issn",
2339 api_name => $issn_method,
2341 desc => 'Retrieve biblio IDs for a given ISSN',
2343 {desc => 'ISBN', type => 'string'}
2346 desc => 'Results object like: { "count": $i, "ids": [...] }',
2353 sub biblio_search_issn {
2354 my( $self, $client, $issn ) = @_;
2355 $logger->debug("Searching ISSN $issn");
2356 # the previous implementation of this method was essentially unlimited,
2357 # so we will set our limit very high and let multiclass.query provide any
2359 # XXX: if making this unlimited is deemed important, we might consider
2360 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2361 # which is functionally deprecated at this point, or a custom call to
2362 # 'open-ils.storage.biblio.multiclass.search_fts'
2364 my $issn_method = 'open-ils.search.biblio.multiclass.query';
2365 if ($self->api_name =~ m/.staff$/) {
2366 $issn_method .= '.staff';
2369 my $method = $self->method_lookup($issn_method);
2370 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2371 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2372 return { ids => \@recs, count => $search_result->{'count'} };
2376 __PACKAGE__->register_method(
2377 method => "fetch_mods_by_copy",
2378 api_name => "open-ils.search.biblio.mods_from_copy",
2381 desc => 'Retrieve MODS record given an attached copy ID',
2383 { desc => 'Copy ID', type => 'number' }
2386 desc => 'MODS record, event on error or uncataloged item'
2391 sub fetch_mods_by_copy {
2392 my( $self, $client, $copyid ) = @_;
2393 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2394 return $evt if $evt;
2395 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2396 return $apputils->record_to_mvr($record);
2400 # -------------------------------------------------------------------------------------
2402 __PACKAGE__->register_method(
2403 method => "cn_browse",
2404 api_name => "open-ils.search.callnumber.browse.target",
2405 notes => "Starts a callnumber browse"
2408 __PACKAGE__->register_method(
2409 method => "cn_browse",
2410 api_name => "open-ils.search.callnumber.browse.page_up",
2411 notes => "Returns the previous page of callnumbers",
2414 __PACKAGE__->register_method(
2415 method => "cn_browse",
2416 api_name => "open-ils.search.callnumber.browse.page_down",
2417 notes => "Returns the next page of callnumbers",
2421 # RETURNS array of arrays like so: label, owning_lib, record, id
2423 my( $self, $client, @params ) = @_;
2426 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2427 if( $self->api_name =~ /target/ );
2428 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2429 if( $self->api_name =~ /page_up/ );
2430 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2431 if( $self->api_name =~ /page_down/ );
2433 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2435 # -------------------------------------------------------------------------------------
2437 __PACKAGE__->register_method(
2438 method => "fetch_cn",
2439 api_name => "open-ils.search.callnumber.retrieve",
2441 notes => "retrieves a callnumber based on ID",
2445 my( $self, $client, $id ) = @_;
2447 my $e = new_editor();
2448 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2449 return $evt if $evt;
2453 __PACKAGE__->register_method(
2454 method => "fetch_fleshed_cn",
2455 api_name => "open-ils.search.callnumber.fleshed.retrieve",
2457 notes => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2460 sub fetch_fleshed_cn {
2461 my( $self, $client, $id ) = @_;
2463 my $e = new_editor();
2464 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2465 return $evt if $evt;
2470 __PACKAGE__->register_method(
2471 method => "fetch_copy_by_cn",
2472 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2474 Returns an array of copy ID's by callnumber ID
2475 @param cnid The callnumber ID
2476 @return An array of copy IDs
2480 sub fetch_copy_by_cn {
2481 my( $self, $conn, $cnid ) = @_;
2482 return $U->cstorereq(
2483 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2484 { call_number => $cnid, deleted => 'f' } );
2487 __PACKAGE__->register_method(
2488 method => 'fetch_cn_by_info',
2489 api_name => 'open-ils.search.call_number.retrieve_by_info',
2491 @param label The callnumber label
2492 @param record The record the cn is attached to
2493 @param org The owning library of the cn
2494 @return The callnumber object
2499 sub fetch_cn_by_info {
2500 my( $self, $conn, $label, $record, $org ) = @_;
2501 return $U->cstorereq(
2502 'open-ils.cstore.direct.asset.call_number.search',
2503 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2508 __PACKAGE__->register_method(
2509 method => 'bib_extras',
2510 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2513 __PACKAGE__->register_method(
2514 method => 'bib_extras',
2515 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2516 ctype => 'item_form'
2518 __PACKAGE__->register_method(
2519 method => 'bib_extras',
2520 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2521 ctype => 'item_type',
2523 __PACKAGE__->register_method(
2524 method => 'bib_extras',
2525 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2526 ctype => 'bib_level'
2528 __PACKAGE__->register_method(
2529 method => 'bib_extras',
2530 api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2536 $logger->warn("deprecation warning: " .$self->api_name);
2538 my $e = new_editor();
2540 my $ctype = $self->{ctype};
2541 my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2544 for my $ccvm (@$ccvms) {
2545 my $obj = "Fieldmapper::config::${ctype}_map"->new;
2546 $obj->value($ccvm->value);
2547 $obj->code($ccvm->code);
2548 $obj->description($ccvm->description) if $obj->can('description');
2557 __PACKAGE__->register_method(
2558 method => 'fetch_slim_record',
2559 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2561 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2563 { desc => 'Array of Record IDs', type => 'array' }
2566 desc => 'Array of biblio records, event on error'
2571 sub fetch_slim_record {
2572 my( $self, $conn, $ids ) = @_;
2574 my $editor = new_editor();
2577 return $editor->event unless
2578 my $r = $editor->retrieve_biblio_record_entry($_);
2585 __PACKAGE__->register_method(
2586 method => 'rec_hold_parts',
2587 api_name => 'open-ils.search.biblio.record_hold_parts',
2589 Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2593 sub rec_hold_parts {
2594 my( $self, $conn, $args ) = @_;
2596 my $rec = $$args{record};
2597 my $mrec = $$args{metarecord};
2598 my $pickup_lib = $$args{pickup_lib};
2599 my $e = new_editor();
2602 select => {bmp => ['id', 'label']},
2607 select => {'acpm' => ['part']},
2608 from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2610 '+acp' => {'deleted' => 'f'},
2611 '+bre' => {id => $rec}
2618 order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2621 if(defined $pickup_lib) {
2622 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2623 if($hard_boundary) {
2624 my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2625 $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2629 return $e->json_query($query);
2635 __PACKAGE__->register_method(
2636 method => 'rec_to_mr_rec_descriptors',
2637 api_name => 'open-ils.search.metabib.record_to_descriptors',
2639 specialized method...
2640 Given a biblio record id or a metarecord id,
2641 this returns a list of metabib.record_descriptor
2642 objects that live within the same metarecord
2643 @param args Object of args including:
2647 sub rec_to_mr_rec_descriptors {
2648 my( $self, $conn, $args ) = @_;
2650 my $rec = $$args{record};
2651 my $mrec = $$args{metarecord};
2652 my $item_forms = $$args{item_forms};
2653 my $item_types = $$args{item_types};
2654 my $item_lang = $$args{item_lang};
2655 my $pickup_lib = $$args{pickup_lib};
2657 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2659 my $e = new_editor();
2663 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2664 return $e->event unless @$map;
2665 $mrec = $$map[0]->metarecord;
2668 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2669 return $e->event unless @$recs;
2671 my @recs = map { $_->source } @$recs;
2672 my $search = { record => \@recs };
2673 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2674 $search->{item_type} = $item_types if $item_types and @$item_types;
2675 $search->{item_lang} = $item_lang if $item_lang;
2677 my $desc = $e->search_metabib_record_descriptor($search);
2681 select => { 'bre' => ['id'] },
2686 'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2692 '+bre' => { id => \@recs },
2697 "+ccs" => { holdable => 't' },
2698 "+acpl" => { holdable => 't', deleted => 'f' }
2702 if ($hard_boundary) { # 0 (or "top") is the same as no setting
2703 my $orgs = $e->json_query(
2704 { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2705 ) or return $e->die_event;
2707 $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2710 my $good_records = $e->json_query($query) or return $e->die_event;
2713 for my $d (@$desc) {
2714 if ( grep { $d->record == $_->{id} } @$good_records ) {
2721 return { metarecord => $mrec, descriptors => $desc };
2725 __PACKAGE__->register_method(
2726 method => 'fetch_age_protect',
2727 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2730 sub fetch_age_protect {
2731 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2735 __PACKAGE__->register_method(
2736 method => 'copies_by_cn_label',
2737 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2740 __PACKAGE__->register_method(
2741 method => 'copies_by_cn_label',
2742 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2745 sub copies_by_cn_label {
2746 my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2747 my $e = new_editor();
2748 my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2749 my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2750 my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2751 return [] unless @$cns;
2753 # show all non-deleted copies in the staff client ...
2754 if ($self->api_name =~ /staff$/o) {
2755 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2758 # ... otherwise, grab the copies ...
2759 my $copies = $e->search_asset_copy(
2760 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2761 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2765 # ... and test for location and status visibility
2766 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2769 __PACKAGE__->register_method(
2770 method => 'bib_copies',
2771 api_name => 'open-ils.search.bib.copies',
2774 __PACKAGE__->register_method(
2775 method => 'bib_copies',
2776 api_name => 'open-ils.search.bib.copies.staff',
2781 my ($self, $client, $rec_id, $org, $depth, $limit, $offset, $pref_ou) = @_;
2782 my $is_staff = ($self->api_name =~ /staff/);
2784 my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
2785 my $req = $cstore->request(
2786 'open-ils.cstore.json_query', mk_copy_query(
2787 $rec_id, $org, $depth, $limit, $offset, $pref_ou, $is_staff));
2790 while ($resp = $req->recv) {
2791 $client->respond($resp->content);
2797 # TODO: this comes almost directly from WWW/EGCatLoader/Record.pm
2803 my $copy_limit = shift;
2804 my $copy_offset = shift;
2805 my $pref_ou = shift;
2806 my $is_staff = shift;
2808 my $query = $U->basic_opac_copy_query(
2809 $rec_id, undef, undef, $copy_limit, $copy_offset, $is_staff
2812 if ($org) { # TODO: root org test
2813 # no need to add the org join filter if we're not actually filtering
2814 $query->{from}->{acp}->[1] = { aou => {
2820 select => {aou => [{
2822 transform => 'actor.org_unit_descendants',
2823 result_field => 'id',
2827 where => {id => $org}
2834 # Unsure if we want these in the shared function, leaving here for now
2835 unshift(@{$query->{order_by}},
2836 { class => "aou", field => 'id',
2837 transform => 'evergreen.rank_ou', params => [$org, $pref_ou]
2840 push(@{$query->{order_by}},
2841 { class => "acp", field => 'id',
2842 transform => 'evergreen.rank_cp'
2850 __PACKAGE__->register_method(
2851 method => 'catalog_record_summary',
2852 api_name => 'open-ils.search.biblio.record.catalog_summary',
2854 max_bundle_count => 1,
2856 desc => 'Stream of record data suitable for catalog display',
2858 {desc => 'Context org unit ID', type => 'number'},
2859 {desc => 'Array of Record IDs', type => 'array'}
2863 Stream of record summary objects including id, record,
2864 hold_count, copy_counts, display (metabib display
2865 fields), attributes (metabib record attrs), plus
2866 metabib_id and metabib_records for the metabib variant.
2871 __PACKAGE__->register_method(
2872 method => 'catalog_record_summary',
2873 api_name => 'open-ils.search.biblio.record.catalog_summary.staff',
2875 max_bundle_count => 1,
2876 signature => q/see open-ils.search.biblio.record.catalog_summary/
2878 __PACKAGE__->register_method(
2879 method => 'catalog_record_summary',
2880 api_name => 'open-ils.search.biblio.metabib.catalog_summary',
2882 max_bundle_count => 1,
2883 signature => q/see open-ils.search.biblio.record.catalog_summary/
2886 __PACKAGE__->register_method(
2887 method => 'catalog_record_summary',
2888 api_name => 'open-ils.search.biblio.metabib.catalog_summary.staff',
2890 max_bundle_count => 1,
2891 signature => q/see open-ils.search.biblio.record.catalog_summary/
2895 sub catalog_record_summary {
2896 my ($self, $client, $org_id, $record_ids) = @_;
2897 my $e = new_editor();
2899 my $is_meta = ($self->api_name =~ /metabib/);
2900 my $is_staff = ($self->api_name =~ /staff/);
2902 my $holds_method = $is_meta ?
2903 'open-ils.circ.mmr.holds.count' :
2904 'open-ils.circ.bre.holds.count';
2906 my $copy_method = $is_meta ?
2907 'open-ils.search.biblio.metarecord.copy_count':
2908 'open-ils.search.biblio.record.copy_count';
2910 $copy_method .= '.staff' if $is_staff;
2912 $copy_method = $self->method_lookup($copy_method); # local method
2914 for my $rec_id (@$record_ids) {
2916 my $response = $is_meta ?
2917 get_one_metarecord_summary($e, $rec_id) :
2918 get_one_record_summary($e, $rec_id);
2920 ($response->{copy_counts}) = $copy_method->run($org_id, $rec_id);
2922 $response->{hold_count} =
2923 $U->simplereq('open-ils.circ', $holds_method, $rec_id);
2925 $client->respond($response);
2931 # Start with a bib summary and augment the data with additional
2932 # metarecord content.
2933 sub get_one_metarecord_summary {
2934 my ($e, $rec_id) = @_;
2936 my $meta = $e->retrieve_metabib_metarecord($rec_id) or return {};
2937 my $maps = $e->search_metabib_metarecord_source_map({metarecord => $rec_id});
2939 my $bre_id = $meta->master_record;
2941 my $response = get_one_record_summary($e, $bre_id);
2943 $response->{metabib_id} = $rec_id;
2944 $response->{metabib_records} = [map {$_->source} @$maps];
2946 my @other_bibs = map {$_->source} grep {$_->source != $bre_id} @$maps;
2948 # Augment the record attributes with those of all of the records
2949 # linked to this metarecord.
2951 my $attrs = $e->search_metabib_record_attr_flat({id => \@other_bibs});
2953 my $attributes = $response->{attributes};
2955 for my $attr (@$attrs) {
2956 $attributes->{$attr->attr} = [] unless $attributes->{$attr->attr};
2957 push(@{$attributes->{$attr->attr}}, $attr->value) # avoid dupes
2958 unless grep {$_ eq $attr->value} @{$attributes->{$attr->attr}};
2965 sub get_one_record_summary {
2966 my ($e, $rec_id) = @_;
2968 my $bre = $e->retrieve_biblio_record_entry([$rec_id, {
2971 bre => [qw/compressed_display_entries mattrs creator editor/]
2975 # Compressed display fields are pachaged as JSON
2977 $display->{$_->name} = OpenSRF::Utils::JSON->JSON2perl($_->value)
2978 foreach @{$bre->compressed_display_entries};
2980 # Create an object of 'mraf' attributes.
2981 # Any attribute can be multi so dedupe and array-ify all of them.
2982 my $attributes = {};
2983 for my $attr (@{$bre->mattrs}) {
2984 $attributes->{$attr->attr} = {} unless $attributes->{$attr->attr};
2985 $attributes->{$attr->attr}->{$attr->value} = 1; # avoid dupes
2987 $attributes->{$_} = [keys %{$attributes->{$_}}] for keys %$attributes;
2992 $bre->clear_compressed_display_entries;
2997 display => $display,
2998 attributes => $attributes