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;
14 use OpenSRF::Utils::Logger qw/:logger/;
17 use OpenSRF::Utils::JSON;
19 use Time::HiRes qw(time);
20 use OpenSRF::EX qw(:try);
21 use Digest::MD5 qw(md5_hex);
27 $Data::Dumper::Indent = 0;
29 use OpenILS::Const qw/:const/;
31 use OpenILS::Application::AppUtils;
32 my $apputils = "OpenILS::Application::AppUtils";
35 my $pfx = "open-ils.search_";
43 $cache = OpenSRF::Utils::Cache->new('global');
44 my $sclient = OpenSRF::Utils::SettingsClient->new();
45 $cache_timeout = $sclient->config_value(
46 "apps", "open-ils.search", "app_settings", "cache_timeout" ) || 300;
48 $superpage_size = $sclient->config_value(
49 "apps", "open-ils.search", "app_settings", "superpage_size" ) || 500;
51 $max_superpages = $sclient->config_value(
52 "apps", "open-ils.search", "app_settings", "max_superpages" ) || 20;
54 $logger->info("Search cache timeout is $cache_timeout, ".
55 " superpage_size is $superpage_size, max_superpages is $max_superpages");
60 # ---------------------------------------------------------------------------
61 # takes a list of record id's and turns the docs into friendly
62 # mods structures. Creates one MODS structure for each doc id.
63 # ---------------------------------------------------------------------------
64 sub _records_to_mods {
70 my $session = OpenSRF::AppSession->create("open-ils.cstore");
71 my $request = $session->request(
72 "open-ils.cstore.direct.biblio.record_entry.search", { id => \@ids } );
74 while( my $resp = $request->recv ) {
75 my $content = $resp->content;
76 next if $content->id == OILS_PRECAT_RECORD;
77 my $u = OpenILS::Utils::ModsParser->new(); # FIXME: we really need a new parser for each object?
78 $u->start_mods_batch( $content->marc );
79 my $mods = $u->finish_mods_batch();
80 $mods->doc_id($content->id());
81 $mods->tcn($content->tcn_value);
85 $session->disconnect();
89 __PACKAGE__->register_method(
90 method => "record_id_to_mods",
91 api_name => "open-ils.search.biblio.record.mods.retrieve",
94 desc => "Provide ID, we provide the MODS object with copy count. "
95 . "Note: this method does NOT take an array of IDs like mods_slim.retrieve", # FIXME: do it here too
97 { desc => 'Record ID', type => 'number' }
100 desc => 'MODS object', type => 'object'
105 # converts a record into a mods object with copy counts attached
106 sub record_id_to_mods {
108 my( $self, $client, $org_id, $id ) = @_;
110 my $mods_list = _records_to_mods( $id );
111 my $mods_obj = $mods_list->[0];
112 my $cmethod = $self->method_lookup("open-ils.search.biblio.record.copy_count");
113 my ($count) = $cmethod->run($org_id, $id);
114 $mods_obj->copy_count($count);
121 __PACKAGE__->register_method(
122 method => "record_id_to_mods_slim",
123 api_name => "open-ils.search.biblio.record.mods_slim.retrieve",
127 desc => "Provide ID(s), we provide the MODS",
129 { desc => 'Record ID or array of IDs' }
132 desc => 'MODS object(s), event on error'
137 # converts a record into a mods object with NO copy counts attached
138 sub record_id_to_mods_slim {
139 my( $self, $client, $id ) = @_;
140 return undef unless defined $id;
142 if(ref($id) and ref($id) == 'ARRAY') {
143 return _records_to_mods( @$id );
145 my $mods_list = _records_to_mods( $id );
146 my $mods_obj = $mods_list->[0];
147 return OpenILS::Event->new('BIBLIO_RECORD_ENTRY_NOT_FOUND') unless $mods_obj;
153 __PACKAGE__->register_method(
154 method => "record_id_to_mods_slim_batch",
155 api_name => "open-ils.search.biblio.record.mods_slim.batch.retrieve",
158 sub record_id_to_mods_slim_batch {
159 my($self, $conn, $id_list) = @_;
160 $conn->respond(_records_to_mods($_)->[0]) for @$id_list;
165 # Returns the number of copies attached to a record based on org location
166 __PACKAGE__->register_method(
167 method => "record_id_to_copy_count",
168 api_name => "open-ils.search.biblio.record.copy_count",
170 desc => q/Returns a copy summary for the given record for the context org
171 unit and all ancestor org units/,
173 {desc => 'Context org unit id', type => 'number'},
174 {desc => 'Record ID', type => 'number'}
177 desc => q/summary object per org unit in the set, where the set
178 includes the context org unit and all parent org units.
179 Object includes the keys "transcendant", "count", "org_unit", "depth",
180 "unshadow", "available". Each is a count, except "org_unit" which is
181 the context org unit and "depth" which is the depth of the context org unit
188 __PACKAGE__->register_method(
189 method => "record_id_to_copy_count",
190 api_name => "open-ils.search.biblio.record.copy_count.staff",
193 desc => q/Returns a copy summary for the given record for the context org
194 unit and all ancestor org units/,
196 {desc => 'Context org unit id', type => 'number'},
197 {desc => 'Record ID', type => 'number'}
200 desc => q/summary object per org unit in the set, where the set
201 includes the context org unit and all parent org units.
202 Object includes the keys "transcendant", "count", "org_unit", "depth",
203 "unshadow", "available". Each is a count, except "org_unit" which is
204 the context org unit and "depth" which is the depth of the context org unit
211 __PACKAGE__->register_method(
212 method => "record_id_to_copy_count",
213 api_name => "open-ils.search.biblio.metarecord.copy_count",
215 desc => q/Returns a copy summary for the given record for the context org
216 unit and all ancestor org units/,
218 {desc => 'Context org unit id', type => 'number'},
219 {desc => 'Record ID', type => 'number'}
222 desc => q/summary object per org unit in the set, where the set
223 includes the context org unit and all parent org units.
224 Object includes the keys "transcendant", "count", "org_unit", "depth",
225 "unshadow", "available". Each is a count, except "org_unit" which is
226 the context org unit and "depth" which is the depth of the context org unit
233 __PACKAGE__->register_method(
234 method => "record_id_to_copy_count",
235 api_name => "open-ils.search.biblio.metarecord.copy_count.staff",
237 desc => q/Returns a copy summary for the given record for the context org
238 unit and all ancestor org units/,
240 {desc => 'Context org unit id', type => 'number'},
241 {desc => 'Record ID', type => 'number'}
244 desc => q/summary object per org unit in the set, where the set
245 includes the context org unit and all parent org units.
246 Object includes the keys "transcendant", "count", "org_unit", "depth",
247 "unshadow", "available". Each is a count, except "org_unit" which is
248 the context org unit and "depth" which is the depth of the context org
249 unit. "depth" is always -1 when the count from a lasso search is
250 performed, since depth doesn't mean anything in a lasso context.
257 sub record_id_to_copy_count {
258 my( $self, $client, $org_id, $record_id ) = @_;
260 return [] unless $record_id;
262 my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
263 my $staff = $self->api_name =~ /staff/ ? 't' : 'f';
265 my $data = $U->cstorereq(
266 "open-ils.cstore.json_query.atomic",
267 { from => ['asset.' . $key . '_copy_count' => $org_id => $record_id => $staff] }
271 for my $d ( @$data ) { # fix up the key name change required by stored-proc version
272 $$d{count} = delete $$d{visible};
276 return [ sort { $a->{depth} <=> $b->{depth} } @count ];
279 __PACKAGE__->register_method(
280 method => "record_has_holdable_copy",
281 api_name => "open-ils.search.biblio.record.has_holdable_copy",
283 desc => q/Returns a boolean indicating if a record has any holdable copies./,
285 {desc => 'Record ID', type => 'number'}
288 desc => q/bool indicating if the record has any holdable copies/,
294 __PACKAGE__->register_method(
295 method => "record_has_holdable_copy",
296 api_name => "open-ils.search.biblio.metarecord.has_holdable_copy",
298 desc => q/Returns a boolean indicating if a record has any holdable copies./,
300 {desc => 'Record ID', type => 'number'}
303 desc => q/bool indicating if the record has any holdable copies/,
309 sub record_has_holdable_copy {
310 my($self, $client, $record_id ) = @_;
312 return 0 unless $record_id;
314 my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
316 my $data = $U->cstorereq(
317 "open-ils.cstore.json_query.atomic",
318 { from => ['asset.' . $key . '_has_holdable_copy' => $record_id ] }
321 return ${@$data[0]}{'asset.' . $key . '_has_holdable_copy'} eq 't';
325 __PACKAGE__->register_method(
326 method => "biblio_search_tcn",
327 api_name => "open-ils.search.biblio.tcn",
330 desc => "Retrieve related record ID(s) given a TCN",
332 { desc => 'TCN', type => 'string' },
333 { desc => 'Flag indicating to include deleted records', type => 'string' }
336 desc => 'Results object like: { "count": $i, "ids": [...] }',
343 sub biblio_search_tcn {
345 my( $self, $client, $tcn, $include_deleted ) = @_;
347 $tcn =~ s/^\s+|\s+$//og;
349 my $e = new_editor();
350 my $search = {tcn_value => $tcn};
351 $search->{deleted} = 'f' unless $include_deleted;
352 my $recs = $e->search_biblio_record_entry( $search, {idlist =>1} );
354 return { count => scalar(@$recs), ids => $recs };
358 # --------------------------------------------------------------------------------
360 __PACKAGE__->register_method(
361 method => "biblio_barcode_to_copy",
362 api_name => "open-ils.search.asset.copy.find_by_barcode",
364 sub biblio_barcode_to_copy {
365 my( $self, $client, $barcode ) = @_;
366 my( $copy, $evt ) = $U->fetch_copy_by_barcode($barcode);
371 __PACKAGE__->register_method(
372 method => "biblio_id_to_copy",
373 api_name => "open-ils.search.asset.copy.batch.retrieve",
375 sub biblio_id_to_copy {
376 my( $self, $client, $ids ) = @_;
377 $logger->info("Fetching copies @$ids");
378 return $U->cstorereq(
379 "open-ils.cstore.direct.asset.copy.search.atomic", { id => $ids } );
383 __PACKAGE__->register_method(
384 method => "biblio_id_to_uris",
385 api_name=> "open-ils.search.asset.uri.retrieve_by_bib",
389 @param BibID Which bib record contains the URIs
390 @param OrgID Where to look for URIs
391 @param OrgDepth Range adjustment for OrgID
392 @return A stream or list of 'auri' objects
396 sub biblio_id_to_uris {
397 my( $self, $client, $bib, $org, $depth ) = @_;
398 die "Org ID required" unless defined($org);
399 die "Bib ID required" unless defined($bib);
402 push @params, $depth if (defined $depth);
404 my $ids = $U->cstorereq( "open-ils.cstore.json_query.atomic",
405 { select => { auri => [ 'id' ] },
409 field => 'call_number',
415 filter => { active => 't' }
426 select => { aou => [ { column => 'id', transform => 'actor.org_unit_descendants', params => \@params, result_field => 'id' } ] },
428 where => { id => $org },
438 my $uris = $U->cstorereq(
439 "open-ils.cstore.direct.asset.uri.search.atomic",
440 { id => [ map { (values %$_) } @$ids ] }
443 $client->respond($_) for (@$uris);
449 __PACKAGE__->register_method(
450 method => "copy_retrieve",
451 api_name => "open-ils.search.asset.copy.retrieve",
454 desc => 'Retrieve a copy object based on the Copy ID',
456 { desc => 'Copy ID', type => 'number'}
459 desc => 'Copy object, event on error'
465 my( $self, $client, $cid ) = @_;
466 my( $copy, $evt ) = $U->fetch_copy($cid);
467 return $evt || $copy;
470 __PACKAGE__->register_method(
471 method => "volume_retrieve",
472 api_name => "open-ils.search.asset.call_number.retrieve"
474 sub volume_retrieve {
475 my( $self, $client, $vid ) = @_;
476 my $e = new_editor();
477 my $vol = $e->retrieve_asset_call_number($vid) or return $e->event;
481 __PACKAGE__->register_method(
482 method => "fleshed_copy_retrieve_batch",
483 api_name => "open-ils.search.asset.copy.fleshed.batch.retrieve",
487 sub fleshed_copy_retrieve_batch {
488 my( $self, $client, $ids ) = @_;
489 $logger->info("Fetching fleshed copies @$ids");
490 return $U->cstorereq(
491 "open-ils.cstore.direct.asset.copy.search.atomic",
494 flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries parts / ] }
499 __PACKAGE__->register_method(
500 method => "fleshed_copy_retrieve",
501 api_name => "open-ils.search.asset.copy.fleshed.retrieve",
504 sub fleshed_copy_retrieve {
505 my( $self, $client, $id ) = @_;
506 my( $c, $e) = $U->fetch_fleshed_copy($id);
511 __PACKAGE__->register_method(
512 method => 'fleshed_by_barcode',
513 api_name => "open-ils.search.asset.copy.fleshed2.find_by_barcode",
516 sub fleshed_by_barcode {
517 my( $self, $conn, $barcode ) = @_;
518 my $e = new_editor();
519 my $copyid = $e->search_asset_copy(
520 {barcode => $barcode, deleted => 'f'}, {idlist=>1})->[0]
522 return fleshed_copy_retrieve2( $self, $conn, $copyid);
526 __PACKAGE__->register_method(
527 method => "fleshed_copy_retrieve2",
528 api_name => "open-ils.search.asset.copy.fleshed2.retrieve",
532 sub fleshed_copy_retrieve2 {
533 my( $self, $client, $id ) = @_;
534 my $e = new_editor();
535 my $copy = $e->retrieve_asset_copy(
542 qw/ location status stat_cat_entry_copy_maps notes age_protect parts peer_record_maps /
544 ascecm => [qw/ stat_cat stat_cat_entry /],
548 ) or return $e->event;
550 # For backwards compatibility
551 #$copy->stat_cat_entries($copy->stat_cat_entry_copy_maps);
553 if( $copy->status->id == OILS_COPY_STATUS_CHECKED_OUT ) {
555 $e->search_action_circulation(
557 { target_copy => $copy->id },
559 order_by => { circ => 'xact_start desc' },
571 __PACKAGE__->register_method(
572 method => 'flesh_copy_custom',
573 api_name => 'open-ils.search.asset.copy.fleshed.custom',
577 sub flesh_copy_custom {
578 my( $self, $conn, $copyid, $fields ) = @_;
579 my $e = new_editor();
580 my $copy = $e->retrieve_asset_copy(
590 ) or return $e->event;
595 __PACKAGE__->register_method(
596 method => "biblio_barcode_to_title",
597 api_name => "open-ils.search.biblio.find_by_barcode",
600 sub biblio_barcode_to_title {
601 my( $self, $client, $barcode ) = @_;
603 my $title = $apputils->simple_scalar_request(
605 "open-ils.storage.biblio.record_entry.retrieve_by_barcode", $barcode );
607 return { ids => [ $title->id ], count => 1 } if $title;
608 return { count => 0 };
611 __PACKAGE__->register_method(
612 method => 'title_id_by_item_barcode',
613 api_name => 'open-ils.search.bib_id.by_barcode',
616 desc => 'Retrieve bib record id associated with the copy identified by the given barcode',
618 { desc => 'Item barcode', type => 'string' }
621 desc => 'Bib record id.'
626 __PACKAGE__->register_method(
627 method => 'title_id_by_item_barcode',
628 api_name => 'open-ils.search.multi_home.bib_ids.by_barcode',
631 desc => 'Retrieve bib record ids associated with the copy identified by the given barcode. This includes peer bibs for Multi-Home items.',
633 { desc => 'Item barcode', type => 'string' }
636 desc => 'Array of bib record ids. First element is the native bib for the item.'
642 sub title_id_by_item_barcode {
643 my( $self, $conn, $barcode ) = @_;
644 my $e = new_editor();
645 my $copies = $e->search_asset_copy(
647 { deleted => 'f', barcode => $barcode },
651 acp => [ 'call_number' ],
658 return $e->event unless @$copies;
660 if( $self->api_name =~ /multi_home/ ) {
661 my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
663 { target_copy => $$copies[0]->id }
666 my @temp = map { $_->peer_record } @{ $multi_home_list };
667 unshift @temp, $$copies[0]->call_number->record->id;
670 return $$copies[0]->call_number->record->id;
674 __PACKAGE__->register_method(
675 method => 'find_peer_bibs',
676 api_name => 'open-ils.search.peer_bibs.test',
679 desc => 'Tests to see if the specified record is a peer record.',
681 { desc => 'Biblio record entry Id', type => 'number' }
684 desc => 'True if specified id can be found in biblio.peer_bib_copy_map.peer_record.',
690 __PACKAGE__->register_method(
691 method => 'find_peer_bibs',
692 api_name => 'open-ils.search.peer_bibs',
695 desc => 'Return acps and mvrs for multi-home items linked to specified peer record.',
697 { desc => 'Biblio record entry Id', type => 'number' }
700 desc => '{ records => Array of mvrs, items => array of acps }',
707 my( $self, $client, $doc_id ) = @_;
708 my $e = new_editor();
710 my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
712 { peer_record => $doc_id },
716 bpbcm => [ 'target_copy', 'peer_type' ],
717 acp => [ 'call_number', 'location', 'status', 'peer_record_maps' ]
723 if ($self->api_name =~ /test/) {
724 return scalar( @{$multi_home_list} ) > 0 ? 1 : 0;
727 if (scalar(@{$multi_home_list})==0) {
731 # create a unique hash of the primary record MVRs for foreign copies
732 # XXX PLEASE let's change to unAPI2 (supports foreign copies) in the TT opac?!?
734 ($_->target_copy->call_number->record, _records_to_mods( $_->target_copy->call_number->record )->[0])
737 # set the foreign_copy_maps field to an empty array
738 map { $rec_hash{$_}->foreign_copy_maps([]) } keys( %rec_hash );
740 # push the maps onto the correct MVRs
741 for (@$multi_home_list) {
743 @{$rec_hash{ $_->target_copy->call_number->record }->foreign_copy_maps()},
748 return [sort {$a->title cmp $b->title} values(%rec_hash)];
751 __PACKAGE__->register_method(
752 method => "biblio_copy_to_mods",
753 api_name => "open-ils.search.biblio.copy.mods.retrieve",
756 # takes a copy object and returns it fleshed mods object
757 sub biblio_copy_to_mods {
758 my( $self, $client, $copy ) = @_;
760 my $volume = $U->cstorereq(
761 "open-ils.cstore.direct.asset.call_number.retrieve",
762 $copy->call_number() );
764 my $mods = _records_to_mods($volume->record());
765 $mods = shift @$mods;
766 $volume->copies([$copy]);
767 push @{$mods->call_numbers()}, $volume;
775 OpenILS::Application::Search::Biblio
781 =head3 open-ils.search.biblio.multiclass.query (arghash, query, docache)
783 For arghash and docache, see B<open-ils.search.biblio.multiclass>.
785 The query argument is a string, but built like a hash with key: value pairs.
786 Recognized search keys include:
788 keyword (kw) - search keyword(s) *
789 author (au) - search author(s) *
790 name (au) - same as author *
791 title (ti) - search title *
792 subject (su) - search subject *
793 series (se) - search series *
794 lang - limit by language (specify multiple langs with lang:l1 lang:l2 ...)
795 site - search at specified org unit, corresponds to actor.org_unit.shortname
796 pref_ou - extend search to specified org unit, corresponds to actor.org_unit.shortname
797 sort - sort type (title, author, pubdate)
798 dir - sort direction (asc, desc)
799 available - if set to anything other than "false" or "0", limits to available items
801 * Searching keyword, author, title, subject, and series supports additional search
802 subclasses, specified with a "|". For example, C<title|proper:gone with the wind>.
804 For more, see B<config.metabib_field>.
808 foreach (qw/open-ils.search.biblio.multiclass.query
809 open-ils.search.biblio.multiclass.query.staff
810 open-ils.search.metabib.multiclass.query
811 open-ils.search.metabib.multiclass.query.staff/)
813 __PACKAGE__->register_method(
815 method => 'multiclass_query',
817 desc => 'Perform a search query. The .staff version of the call includes otherwise hidden hits.',
819 {name => 'arghash', desc => 'Arg hash (see open-ils.search.biblio.multiclass)', type => 'object'},
820 {name => 'query', desc => 'Raw human-readable query (see perldoc '. __PACKAGE__ .')', type => 'string'},
821 {name => 'docache', desc => 'Flag for caching (see open-ils.search.biblio.multiclass)', type => 'object'},
824 desc => 'Search results from query, like: { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
825 type => 'object', # TODO: update as miker's new elements are included
831 sub multiclass_query {
832 my($self, $conn, $arghash, $query, $docache) = @_;
834 $logger->debug("initial search query => $query");
835 my $orig_query = $query;
838 $query =~ s/^\s+//go;
840 # convert convenience classes (e.g. kw for keyword) to the full class name
841 # ensure that the convenience class isn't part of a word (e.g. 'playhouse')
842 $query =~ s/(^|\s)kw(:|\|)/$1keyword$2/go;
843 $query =~ s/(^|\s)ti(:|\|)/$1title$2/go;
844 $query =~ s/(^|\s)au(:|\|)/$1author$2/go;
845 $query =~ s/(^|\s)su(:|\|)/$1subject$2/go;
846 $query =~ s/(^|\s)se(:|\|)/$1series$2/go;
847 $query =~ s/(^|\s)name(:|\|)/$1author$2/og;
849 $logger->debug("cleansed query string => $query");
852 my $simple_class_re = qr/((?:\w+(?:\|\w+)?):[^:]+?)$/;
853 my $class_list_re = qr/(?:keyword|title|author|subject|series)/;
854 my $modifier_list_re = qr/(?:site|dir|sort|lang|available|preflib)/;
857 while ($query =~ s/$simple_class_re//so) {
860 my $where = index($qpart,':');
861 my $type = substr($qpart, 0, $where++);
862 my $value = substr($qpart, $where);
864 if ($type !~ /^(?:$class_list_re|$modifier_list_re)/o) {
865 $tmp_value = "$qpart $tmp_value";
869 if ($type =~ /$class_list_re/o ) {
870 $value .= $tmp_value;
874 next unless $type and $value;
876 $value =~ s/^\s*//og;
877 $value =~ s/\s*$//og;
878 $type = 'sort_dir' if $type eq 'dir';
880 if($type eq 'site') {
881 # 'site' is the org shortname. when using this, we also want
882 # to search at the requested org's depth
883 my $e = new_editor();
884 if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
885 $arghash->{org_unit} = $org->id if $org;
886 $arghash->{depth} = $e->retrieve_actor_org_unit_type($org->ou_type)->depth;
888 $logger->warn("'site:' query used on invalid org shortname: $value ... ignoring");
890 } elsif($type eq 'pref_ou') {
891 # 'pref_ou' is the preferred org shortname.
892 my $e = new_editor();
893 if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
894 $arghash->{pref_ou} = $org->id if $org;
896 $logger->warn("'pref_ou:' query used on invalid org shortname: $value ... ignoring");
899 } elsif($type eq 'available') {
901 $arghash->{available} = 1 unless $value eq 'false' or $value eq '0';
903 } elsif($type eq 'lang') {
904 # collect languages into an array of languages
905 $arghash->{language} = [] unless $arghash->{language};
906 push(@{$arghash->{language}}, $value);
908 } elsif($type =~ /^sort/o) {
909 # sort and sort_dir modifiers
910 $arghash->{$type} = $value;
913 # append the search term to the term under construction
914 $search->{$type} = {} unless $search->{$type};
915 $search->{$type}->{term} =
916 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $value" : $value;
920 $query .= " $tmp_value";
921 $query =~ s/\s+/ /go;
922 $query =~ s/^\s+//go;
923 $query =~ s/\s+$//go;
925 my $type = $arghash->{default_class} || 'keyword';
926 $type = ($type eq '-') ? 'keyword' : $type;
927 $type = ($type !~ /^(title|author|keyword|subject|series)(?:\|\w+)?$/o) ? 'keyword' : $type;
930 # This is the front part of the string before any special tokens were
931 # parsed OR colon-separated strings that do not denote a class.
932 # Add this data to the default search class
933 $search->{$type} = {} unless $search->{$type};
934 $search->{$type}->{term} =
935 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $query" : $query;
937 my $real_search = $arghash->{searches} = { $type => { term => $orig_query } };
939 # capture the original limit because the search method alters the limit internally
940 my $ol = $arghash->{limit};
942 my $sclient = OpenSRF::Utils::SettingsClient->new;
944 (my $method = $self->api_name) =~ s/\.query//o;
946 $method =~ s/multiclass/multiclass.staged/
947 if $sclient->config_value(apps => 'open-ils.search',
948 app_settings => 'use_staged_search') =~ /true/i;
950 # XXX This stops the session locale from doing the right thing.
951 # XXX Revisit this and have it translate to a lang instead of a locale.
952 #$arghash->{preferred_language} = $U->get_org_locale($arghash->{org_unit})
953 # unless $arghash->{preferred_language};
955 $method = $self->method_lookup($method);
956 my ($data) = $method->run($arghash, $docache);
958 $arghash->{searches} = $search if (!$data->{complex_query});
960 $arghash->{limit} = $ol if $ol;
961 $data->{compiled_search} = $arghash;
962 $data->{query} = $orig_query;
964 $logger->info("compiled search is " . OpenSRF::Utils::JSON->perl2JSON($arghash));
969 __PACKAGE__->register_method(
970 method => 'cat_search_z_style_wrapper',
971 api_name => 'open-ils.search.biblio.zstyle',
973 signature => q/@see open-ils.search.biblio.multiclass/
976 __PACKAGE__->register_method(
977 method => 'cat_search_z_style_wrapper',
978 api_name => 'open-ils.search.biblio.zstyle.staff',
980 signature => q/@see open-ils.search.biblio.multiclass/
983 sub cat_search_z_style_wrapper {
986 my $authtoken = shift;
989 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
991 my $ou = $cstore->request(
992 'open-ils.cstore.direct.actor.org_unit.search',
993 { parent_ou => undef }
996 my $result = { service => 'native-evergreen-catalog', records => [] };
997 my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
999 $$searchhash{searches}{title}{term} = $$args{search}{title} if $$args{search}{title};
1000 $$searchhash{searches}{author}{term} = $$args{search}{author} if $$args{search}{author};
1001 $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
1002 $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
1003 $$searchhash{searches}{'identifier|isbn'}{term} = $$args{search}{isbn} if $$args{search}{isbn};
1004 $$searchhash{searches}{'identifier|issn'}{term} = $$args{search}{issn} if $$args{search}{issn};
1006 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
1007 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
1008 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
1009 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
1011 my $list = the_quest_for_knowledge( $self, $client, $searchhash );
1013 if ($list->{count} > 0 and @{$list->{ids}}) {
1014 $result->{count} = $list->{count};
1016 my $records = $cstore->request(
1017 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
1018 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
1021 for my $rec ( @$records ) {
1023 my $u = OpenILS::Utils::ModsParser->new();
1024 $u->start_mods_batch( $rec->marc );
1025 my $mods = $u->finish_mods_batch();
1027 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
1033 $cstore->disconnect();
1037 # ----------------------------------------------------------------------------
1038 # These are the main OPAC search methods
1039 # ----------------------------------------------------------------------------
1041 __PACKAGE__->register_method(
1042 method => 'the_quest_for_knowledge',
1043 api_name => 'open-ils.search.biblio.multiclass',
1045 desc => "Performs a multi class biblio or metabib search",
1048 desc => "A search hash with keys: "
1049 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
1050 . "See perldoc " . __PACKAGE__ . " for more detail",
1054 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
1059 desc => 'An object of the form: '
1060 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
1065 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
1067 The search-hash argument can have the following elements:
1069 searches: { "$class" : "$value", ...} [REQUIRED]
1070 org_unit: The org id to focus the search at
1071 depth : The org depth
1072 limit : The search limit default: 10
1073 offset : The search offset default: 0
1074 format : The MARC format
1075 sort : What field to sort the results on? [ author | title | pubdate ]
1076 sort_dir: What direction do we sort? [ asc | desc ]
1077 tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
1078 will be tagged with an additional value ("1") as the last value in the record ID array for
1079 each record. Requires the 'authtoken'
1080 authtoken : Authentication token string; When actions are performed that require a user login
1081 (e.g. tagging circulated records), the authentication token is required
1083 The searches element is required, must have a hashref value, and the hashref must contain at least one
1084 of the following classes as a key:
1092 The value paired with a key is the associated search string.
1094 The docache argument enables/disables searching and saving results in cache (default OFF).
1096 The return object, if successful, will look like:
1098 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
1102 __PACKAGE__->register_method(
1103 method => 'the_quest_for_knowledge',
1104 api_name => 'open-ils.search.biblio.multiclass.staff',
1105 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1107 __PACKAGE__->register_method(
1108 method => 'the_quest_for_knowledge',
1109 api_name => 'open-ils.search.metabib.multiclass',
1110 signature => q/@see open-ils.search.biblio.multiclass/
1112 __PACKAGE__->register_method(
1113 method => 'the_quest_for_knowledge',
1114 api_name => 'open-ils.search.metabib.multiclass.staff',
1115 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1118 sub the_quest_for_knowledge {
1119 my( $self, $conn, $searchhash, $docache ) = @_;
1121 return { count => 0 } unless $searchhash and
1122 ref $searchhash->{searches} eq 'HASH';
1124 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
1128 if($self->api_name =~ /metabib/) {
1130 $method =~ s/biblio/metabib/o;
1133 # do some simple sanity checking
1134 if(!$searchhash->{searches} or
1135 ( !grep { /^(?:title|author|subject|series|keyword|identifier\|is[bs]n)/ } keys %{$searchhash->{searches}} ) ) {
1136 return { count => 0 };
1139 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
1140 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
1141 my $end = $offset + $limit - 1;
1143 my $maxlimit = 5000;
1144 $searchhash->{offset} = 0; # possible user value overwritten in hash
1145 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
1147 return { count => 0 } if $offset > $maxlimit;
1150 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
1151 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
1152 my $ckey = $pfx . md5_hex($method . $s);
1154 $logger->info("bib search for: $s");
1156 $searchhash->{limit} -= $offset;
1160 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1164 $method .= ".staff" if($self->api_name =~ /staff/);
1165 $method .= ".atomic";
1167 for (keys %$searchhash) {
1168 delete $$searchhash{$_}
1169 unless defined $$searchhash{$_};
1172 $result = $U->storagereq( $method, %$searchhash );
1176 $docache = 0; # results came FROM cache, so we don't write back
1179 return {count => 0} unless ($result && $$result[0]);
1183 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1186 # If we didn't get this data from the cache, put it into the cache
1187 # then return the correct offset of records
1188 $logger->debug("putting search cache $ckey\n");
1189 put_cache($ckey, $count, \@recs);
1193 # if we have the full set of data, trim out
1194 # the requested chunk based on limit and offset
1196 for ($offset..$end) {
1197 last unless $recs[$_];
1198 push(@t, $recs[$_]);
1203 return { ids => \@recs, count => $count };
1207 __PACKAGE__->register_method(
1208 method => 'staged_search',
1209 api_name => 'open-ils.search.biblio.multiclass.staged',
1211 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1212 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1215 desc => "A search hash with keys: "
1216 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1217 . "See perldoc " . __PACKAGE__ . " for more detail",
1221 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1226 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1227 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1232 __PACKAGE__->register_method(
1233 method => 'staged_search',
1234 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1235 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1237 __PACKAGE__->register_method(
1238 method => 'staged_search',
1239 api_name => 'open-ils.search.metabib.multiclass.staged',
1240 signature => q/@see open-ils.search.biblio.multiclass.staged/
1242 __PACKAGE__->register_method(
1243 method => 'staged_search',
1244 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1245 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1249 my($self, $conn, $search_hash, $docache) = @_;
1251 my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1253 my $method = $IAmMetabib?
1254 'open-ils.storage.metabib.multiclass.staged.search_fts':
1255 'open-ils.storage.biblio.multiclass.staged.search_fts';
1257 $method .= '.staff' if $self->api_name =~ /staff$/;
1258 $method .= '.atomic';
1260 return {count => 0} unless (
1262 $search_hash->{searches} and
1263 scalar( keys %{$search_hash->{searches}} ));
1265 my $search_duration;
1266 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1267 my $user_limit = $search_hash->{limit} || 10;
1268 my $ignore_facet_classes = $search_hash->{ignore_facet_classes};
1269 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1270 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1273 # we're grabbing results on a per-superpage basis, which means the
1274 # limit and offset should coincide with superpage boundaries
1275 $search_hash->{offset} = 0;
1276 $search_hash->{limit} = $superpage_size;
1278 # force a well-known check_limit
1279 $search_hash->{check_limit} = $superpage_size;
1280 # restrict total tested to superpage size * number of superpages
1281 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1283 # Set the configured estimation strategy, defaults to 'inclusion'.
1284 my $estimation_strategy = OpenSRF::Utils::SettingsClient
1287 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1289 $search_hash->{estimation_strategy} = $estimation_strategy;
1291 # pull any existing results from the cache
1292 my $key = search_cache_key($method, $search_hash);
1293 my $facet_key = $key.'_facets';
1294 my $cache_data = $cache->get_cache($key) || {};
1296 # First, we want to make sure that someone else isn't currently trying to perform exactly
1297 # this same search. The point is to allow just one instance of a search to fill the needs
1298 # of all concurrent, identical searches. This will avoid spammy searches killing the
1299 # database without requiring admins to start locking some IP addresses out entirely.
1301 # There's still a tiny race condition where 2 might run, but without sigificantly more code
1302 # and complexity, this is close to the best we can do.
1304 if ($cache_data->{running}) { # someone is already doing the search...
1305 while ( sleep(1) ) { # sleep for a second ... maybe they'll finish
1306 $cache_data = $cache->get_cache($key) || {};
1307 last if (!$cache_data->{running});
1309 } else { # we're the first ... let's give it a try
1310 $cache->put_cache($key, { running => $$ }, $cache_timeout / 3);
1313 # keep retrieving results until we find enough to
1314 # fulfill the user-specified limit and offset
1315 my $all_results = [];
1316 my $page; # current superpage
1317 my $est_hit_count = 0;
1318 my $current_page_summary = {};
1319 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1320 my $is_real_hit_count = 0;
1323 for($page = 0; $page < $max_superpages; $page++) {
1325 my $data = $cache_data->{$page};
1329 $logger->debug("staged search: analyzing superpage $page");
1332 # this window of results is already cached
1333 $logger->debug("staged search: found cached results");
1334 $summary = $data->{summary};
1335 $results = $data->{results};
1338 # retrieve the window of results from the database
1339 $logger->debug("staged search: fetching results from the database");
1340 $search_hash->{skip_check} = $page * $superpage_size;
1342 $results = $U->storagereq($method, %$search_hash);
1343 $search_duration = time - $start;
1344 $summary = shift(@$results) if $results;
1347 $logger->info("search timed out: duration=$search_duration: params=".
1348 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1349 return {count => 0};
1352 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1354 my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1356 $logger->info("search returned 0 results: duration=$search_duration: params=".
1357 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1360 # Create backwards-compatible result structures
1362 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1364 $results = [map {[$_->{id}]} @$results];
1367 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1368 $results = [grep {defined $_->[0]} @$results];
1369 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1372 tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib)
1373 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1375 $current_page_summary = $summary;
1377 # add the new set of results to the set under construction
1378 push(@$all_results, @$results);
1380 my $current_count = scalar(@$all_results);
1382 $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1385 $logger->debug("staged search: located $current_count, with estimated hits=".
1386 $summary->{estimated_hit_count}." : visible=".$summary->{visible}.", checked=".$summary->{checked});
1388 if (defined($summary->{estimated_hit_count})) {
1389 foreach (qw/ checked visible excluded deleted /) {
1390 $global_summary->{$_} += $summary->{$_};
1392 $global_summary->{total} = $summary->{total};
1395 # we've found all the possible hits
1396 last if $current_count == $summary->{visible}
1397 and not defined $summary->{estimated_hit_count};
1399 # we've found enough results to satisfy the requested limit/offset
1400 last if $current_count >= ($user_limit + $user_offset);
1402 # we've scanned all possible hits
1403 if($summary->{checked} < $superpage_size) {
1404 $est_hit_count = scalar(@$all_results);
1405 # we have all possible results in hand, so we know the final hit count
1406 $is_real_hit_count = 1;
1411 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1413 # refine the estimate if we have more than one superpage
1414 if ($page > 0 and not $is_real_hit_count) {
1415 if ($global_summary->{checked} >= $global_summary->{total}) {
1416 $est_hit_count = $global_summary->{visible};
1418 my $updated_hit_count = $U->storagereq(
1419 'open-ils.storage.fts_paging_estimate',
1420 $global_summary->{checked},
1421 $global_summary->{visible},
1422 $global_summary->{excluded},
1423 $global_summary->{deleted},
1424 $global_summary->{total}
1426 $est_hit_count = $updated_hit_count->{$estimation_strategy};
1430 $conn->respond_complete(
1432 count => $est_hit_count,
1433 core_limit => $search_hash->{core_limit},
1434 superpage_size => $search_hash->{check_limit},
1435 superpage_summary => $current_page_summary,
1436 facet_key => $facet_key,
1441 cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1446 sub tag_circulated_records {
1447 my ($auth, $results, $metabib) = @_;
1448 my $e = new_editor(authtoken => $auth);
1449 return $results unless $e->checkauth;
1452 select => { acn => [{ column => 'record', alias => 'tagme' }] },
1453 from => { acp => 'acn' },
1454 where => { id => { in => { from => ['action.usr_visible_circ_copies', $e->requestor->id] } } },
1460 select => { mmsm => [{ column => 'metarecord', alias => 'tagme' }] },
1462 where => { source => { in => $query } },
1467 # Give me the distinct set of bib records that exist in the user's visible circulation history
1468 my $circ_recs = $e->json_query( $query );
1470 # if the record appears in the circ history, push a 1 onto
1471 # the rec array structure to indicate truthiness
1472 for my $rec (@$results) {
1473 push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1479 # creates a unique token to represent the query in the cache
1480 sub search_cache_key {
1482 my $search_hash = shift;
1484 for my $key (sort keys %$search_hash) {
1485 push(@sorted, ($key => $$search_hash{$key}))
1486 unless $key eq 'limit' or
1488 $key eq 'skip_check';
1490 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1491 return $pfx . md5_hex($method . $s);
1494 sub retrieve_cached_facets {
1500 return undef unless ($key and $key =~ /_facets$/);
1502 my $blob = $cache->get_cache($key) || {};
1506 for my $f ( keys %$blob ) {
1507 my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1508 @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1509 for my $s ( @sorted ) {
1510 my ($k) = keys(%$s);
1511 my ($v) = values(%$s);
1512 $$facets{$f}{$k} = $v;
1522 __PACKAGE__->register_method(
1523 method => "retrieve_cached_facets",
1524 api_name => "open-ils.search.facet_cache.retrieve",
1526 desc => 'Returns facet data derived from a specific search based on a key '.
1527 'generated by open-ils.search.biblio.multiclass.staged and friends.',
1530 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1535 desc => 'Two level hash of facet values. Top level key is the facet id defined on the config.metabib_field table. '.
1536 'Second level key is a string facet value. Datum attached to each facet value is the number of distinct records, '.
1537 'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1538 'facet retrieval. These counts are calculated for all superpages that have been checked for visibility.',
1546 # add facets for this search to the facet cache
1547 my($key, $results, $metabib, $ignore) = @_;
1548 my $data = $cache->get_cache($key);
1551 return undef unless (@$results);
1553 # The query we're constructing
1555 # select mfae.field as id,
1557 # count(distinct mmrsm.appropriate-id-field )
1558 # from metabib.facet_entry mfae
1559 # join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1560 # where mmrsm.appropriate-id-field in IDLIST
1563 my $count_field = $metabib ? 'metarecord' : 'source';
1566 mfae => [ { column => 'field', alias => 'id'}, 'value' ],
1568 transform => 'count',
1570 column => $count_field,
1577 mmrsm => { field => 'source', fkey => 'source' },
1578 cmf => { field => 'id', fkey => 'field' }
1582 '+mmrsm' => { $count_field => $results },
1583 '+cmf' => { facet_field => 't' }
1587 $query->{where}->{'+cmf'}->{field_class} = {'not in' => $ignore}
1588 if ref($ignore) and @$ignore > 0;
1590 my $facets = $U->cstorereq("open-ils.cstore.json_query.atomic", $query);
1592 for my $facet (@$facets) {
1593 next unless ($facet->{value});
1594 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1597 $logger->info("facet compilation: cached with key=$key");
1599 $cache->put_cache($key, $data, $cache_timeout);
1602 sub cache_staged_search_page {
1603 # puts this set of results into the cache
1604 my($key, $page, $summary, $results) = @_;
1605 my $data = $cache->get_cache($key);
1608 summary => $summary,
1612 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1613 $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1615 $cache->put_cache($key, $data, $cache_timeout);
1623 my $start = $offset;
1624 my $end = $offset + $limit - 1;
1626 $logger->debug("searching cache for $key : $start..$end\n");
1628 return undef unless $cache;
1629 my $data = $cache->get_cache($key);
1631 return undef unless $data;
1633 my $count = $data->[0];
1636 return undef unless $offset < $count;
1639 for( my $i = $offset; $i <= $end; $i++ ) {
1640 last unless my $d = $$data[$i];
1641 push( @result, $d );
1644 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1651 my( $key, $count, $data ) = @_;
1652 return undef unless $cache;
1653 $logger->debug("search_cache putting ".
1654 scalar(@$data)." items at key $key with timeout $cache_timeout");
1655 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1659 __PACKAGE__->register_method(
1660 method => "biblio_mrid_to_modsbatch_batch",
1661 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1664 sub biblio_mrid_to_modsbatch_batch {
1665 my( $self, $client, $mrids) = @_;
1666 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1668 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1669 for my $id (@$mrids) {
1670 next unless defined $id;
1671 my ($m) = $method->run($id);
1678 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1679 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1681 __PACKAGE__->register_method(
1682 method => "biblio_mrid_to_modsbatch",
1685 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1686 . "As usual, the .staff version of this method will include otherwise hidden records.",
1688 { desc => 'Metarecord ID', type => 'number' },
1689 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1692 desc => 'MVR Object, event on error',
1698 sub biblio_mrid_to_modsbatch {
1699 my( $self, $client, $mrid, $args) = @_;
1701 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1703 my ($mr, $evt) = _grab_metarecord($mrid);
1704 return $evt unless $mr;
1706 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1707 biblio_mrid_make_modsbatch($self, $client, $mr);
1709 return $mvr unless ref($args);
1711 # Here we find the lead record appropriate for the given filters
1712 # and use that for the title and author of the metarecord
1713 my $format = $$args{format};
1714 my $org = $$args{org};
1715 my $depth = $$args{depth};
1717 return $mvr unless $format or $org or $depth;
1719 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1720 $method = "$method.staff" if $self->api_name =~ /staff/o;
1722 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1724 if( my $mods = $U->record_to_mvr($rec) ) {
1726 $mvr->title( $mods->title );
1727 $mvr->author($mods->author);
1728 $logger->debug("mods_slim updating title and ".
1729 "author in mvr with ".$mods->title." : ".$mods->author);
1735 # converts a metarecord to an mvr
1738 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1739 return Fieldmapper::metabib::virtual_record->new($perl);
1742 # checks to see if a metarecord has mods, if so returns true;
1744 __PACKAGE__->register_method(
1745 method => "biblio_mrid_check_mvr",
1746 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1747 notes => "Takes a metarecord ID or a metarecord object and returns true "
1748 . "if the metarecord already has an mvr associated with it."
1751 sub biblio_mrid_check_mvr {
1752 my( $self, $client, $mrid ) = @_;
1756 if(ref($mrid)) { $mr = $mrid; }
1757 else { ($mr, $evt) = _grab_metarecord($mrid); }
1758 return $evt if $evt;
1760 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1762 return _mr_to_mvr($mr) if $mr->mods();
1766 sub _grab_metarecord {
1768 #my $e = OpenILS::Utils::Editor->new;
1769 my $e = new_editor();
1770 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1775 __PACKAGE__->register_method(
1776 method => "biblio_mrid_make_modsbatch",
1777 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1778 notes => "Takes either a metarecord ID or a metarecord object. "
1779 . "Forces the creations of an mvr for the given metarecord. "
1780 . "The created mvr is returned."
1783 sub biblio_mrid_make_modsbatch {
1784 my( $self, $client, $mrid ) = @_;
1786 #my $e = OpenILS::Utils::Editor->new;
1787 my $e = new_editor();
1794 $mr = $e->retrieve_metabib_metarecord($mrid)
1795 or return $e->event;
1798 my $masterid = $mr->master_record;
1799 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1801 my $ids = $U->storagereq(
1802 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1803 return undef unless @$ids;
1805 my $master = $e->retrieve_biblio_record_entry($masterid)
1806 or return $e->event;
1808 # start the mods batch
1809 my $u = OpenILS::Utils::ModsParser->new();
1810 $u->start_mods_batch( $master->marc );
1812 # grab all of the sub-records and shove them into the batch
1813 my @ids = grep { $_ ne $masterid } @$ids;
1814 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1819 my $r = $e->retrieve_biblio_record_entry($i);
1820 push( @$subrecs, $r ) if $r;
1825 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1826 $u->push_mods_batch( $_->marc ) if $_->marc;
1830 # finish up and send to the client
1831 my $mods = $u->finish_mods_batch();
1832 $mods->doc_id($mrid);
1833 $client->respond_complete($mods);
1836 # now update the mods string in the db
1837 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1840 #$e = OpenILS::Utils::Editor->new(xact => 1);
1841 $e = new_editor(xact => 1);
1842 $e->update_metabib_metarecord($mr)
1843 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1850 # converts a mr id into a list of record ids
1852 foreach (qw/open-ils.search.biblio.metarecord_to_records
1853 open-ils.search.biblio.metarecord_to_records.staff/)
1855 __PACKAGE__->register_method(
1856 method => "biblio_mrid_to_record_ids",
1859 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1860 . "As usual, the .staff version of this method will include otherwise hidden records.",
1862 { desc => 'Metarecord ID', type => 'number' },
1863 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1866 desc => 'Results object like {count => $i, ids =>[...]}',
1874 sub biblio_mrid_to_record_ids {
1875 my( $self, $client, $mrid, $args ) = @_;
1877 my $format = $$args{format};
1878 my $org = $$args{org};
1879 my $depth = $$args{depth};
1881 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1882 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1883 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1885 return { count => scalar(@$recs), ids => $recs };
1889 __PACKAGE__->register_method(
1890 method => "biblio_record_to_marc_html",
1891 api_name => "open-ils.search.biblio.record.html"
1894 __PACKAGE__->register_method(
1895 method => "biblio_record_to_marc_html",
1896 api_name => "open-ils.search.authority.to_html"
1899 # Persistent parsers and setting objects
1900 my $parser = XML::LibXML->new();
1901 my $xslt = XML::LibXSLT->new();
1903 my $slim_marc_sheet;
1904 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1906 sub biblio_record_to_marc_html {
1907 my($self, $client, $recordid, $slim, $marcxml) = @_;
1910 my $dir = $settings_client->config_value("dirs", "xsl");
1913 unless($slim_marc_sheet) {
1914 my $xsl = $settings_client->config_value(
1915 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1917 $xsl = $parser->parse_file("$dir/$xsl");
1918 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1921 $sheet = $slim_marc_sheet;
1925 unless($marc_sheet) {
1926 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1927 my $xsl = $settings_client->config_value(
1928 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1929 $xsl = $parser->parse_file("$dir/$xsl");
1930 $marc_sheet = $xslt->parse_stylesheet($xsl);
1932 $sheet = $marc_sheet;
1937 my $e = new_editor();
1938 if($self->api_name =~ /authority/) {
1939 $record = $e->retrieve_authority_record_entry($recordid)
1940 or return $e->event;
1942 $record = $e->retrieve_biblio_record_entry($recordid)
1943 or return $e->event;
1945 $marcxml = $record->marc;
1948 my $xmldoc = $parser->parse_string($marcxml);
1949 my $html = $sheet->transform($xmldoc);
1950 return $html->documentElement->toString();
1953 __PACKAGE__->register_method(
1954 method => "format_biblio_record_entry",
1955 api_name => "open-ils.search.biblio.record.print",
1957 desc => 'Returns a printable version of the specified bib record',
1959 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1962 desc => q/An action_trigger.event object or error event./,
1967 __PACKAGE__->register_method(
1968 method => "format_biblio_record_entry",
1969 api_name => "open-ils.search.biblio.record.email",
1971 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1973 { desc => 'Authentication token', type => 'string'},
1974 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1977 desc => q/Undefined on success, otherwise an error event./,
1983 sub format_biblio_record_entry {
1984 my($self, $conn, $arg1, $arg2) = @_;
1986 my $for_print = ($self->api_name =~ /print/);
1987 my $for_email = ($self->api_name =~ /email/);
1989 my $e; my $auth; my $bib_id; my $context_org;
1993 $context_org = $arg2 || $U->get_org_tree->id;
1994 $e = new_editor(xact => 1);
1995 } elsif ($for_email) {
1998 $e = new_editor(authtoken => $auth, xact => 1);
1999 return $e->die_event unless $e->checkauth;
2000 $context_org = $e->requestor->home_ou;
2004 if (ref $bib_id ne 'ARRAY') {
2005 $bib_ids = [ $bib_id ];
2010 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
2011 $bucket->btype('temp');
2012 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
2014 $bucket->owner($e->requestor)
2018 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
2020 for my $id (@$bib_ids) {
2022 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
2024 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
2025 $bucket_entry->target_biblio_record_entry($bib);
2026 $bucket_entry->bucket($bucket_obj->id);
2027 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
2034 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
2036 } elsif ($for_email) {
2038 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
2045 __PACKAGE__->register_method(
2046 method => "retrieve_all_copy_statuses",
2047 api_name => "open-ils.search.config.copy_status.retrieve.all"
2050 sub retrieve_all_copy_statuses {
2051 my( $self, $client ) = @_;
2052 return new_editor()->retrieve_all_config_copy_status();
2056 __PACKAGE__->register_method(
2057 method => "copy_counts_per_org",
2058 api_name => "open-ils.search.biblio.copy_counts.retrieve"
2061 __PACKAGE__->register_method(
2062 method => "copy_counts_per_org",
2063 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
2066 sub copy_counts_per_org {
2067 my( $self, $client, $record_id ) = @_;
2069 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
2071 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
2072 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
2074 my $counts = $apputils->simple_scalar_request(
2075 "open-ils.storage", $method, $record_id );
2077 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
2082 __PACKAGE__->register_method(
2083 method => "copy_count_summary",
2084 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
2085 notes => "returns an array of these: "
2086 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2087 . "where statusx is a copy status name. The statuses are sorted by ID.",
2091 sub copy_count_summary {
2092 my( $self, $client, $rid, $org, $depth ) = @_;
2095 my $data = $U->storagereq(
2096 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
2099 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2101 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2105 __PACKAGE__->register_method(
2106 method => "copy_location_count_summary",
2107 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
2108 notes => "returns an array of these: "
2109 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
2110 . "where statusx is a copy status name. The statuses are sorted by ID.",
2113 sub copy_location_count_summary {
2114 my( $self, $client, $rid, $org, $depth ) = @_;
2117 my $data = $U->storagereq(
2118 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2121 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2123 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2125 || $a->[4] cmp $b->[4]
2129 __PACKAGE__->register_method(
2130 method => "copy_count_location_summary",
2131 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2132 notes => "returns an array of these: "
2133 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2134 . "where statusx is a copy status name. The statuses are sorted by ID."
2137 sub copy_count_location_summary {
2138 my( $self, $client, $rid, $org, $depth ) = @_;
2141 my $data = $U->storagereq(
2142 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2144 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2146 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2151 foreach (qw/open-ils.search.biblio.marc
2152 open-ils.search.biblio.marc.staff/)
2154 __PACKAGE__->register_method(
2155 method => "marc_search",
2158 desc => 'Fetch biblio IDs based on MARC record criteria. '
2159 . 'As usual, the .staff version of the search includes otherwise hidden records',
2162 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2163 'See perldoc ' . __PACKAGE__ . ' for more detail.',
2166 {desc => 'limit (optional)', type => 'number'},
2167 {desc => 'offset (optional)', type => 'number'}
2170 desc => 'Results object like: { "count": $i, "ids": [...] }',
2177 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
2179 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
2181 searches: complex query object (required)
2182 org_unit: The org ID to focus the search at
2183 depth : The org depth
2184 limit : integer search limit default: 10
2185 offset : integer search offset default: 0
2186 sort : What field to sort the results on? [ author | title | pubdate ]
2187 sort_dir: In what direction do we sort? [ asc | desc ]
2189 Additional keys to refine search criteria:
2192 language : Language (code)
2193 lit_form : Literary form
2194 item_form: Item form
2195 item_type: Item type
2196 format : The MARC format
2198 Please note that the specific strings to be used in the "addtional keys" will be entirely
2199 dependent on your loaded data.
2201 All keys except "searches" are optional.
2202 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
2204 For example, an arg hash might look like:
2226 The arghash is eventually passed to the SRF call:
2227 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2229 Presently, search uses the cache unconditionally.
2233 # FIXME: that example above isn't actually tested.
2234 # TODO: docache option?
2236 my( $self, $conn, $args, $limit, $offset, $timeout ) = @_;
2238 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2239 $method .= ".staff" if $self->api_name =~ /staff/;
2240 $method .= ".atomic";
2242 $limit ||= 10; # FIXME: what about $args->{limit} ?
2243 $offset ||= 0; # FIXME: what about $args->{offset} ?
2245 # allow caller to pass in a call timeout since MARC searches
2246 # can take longer than the default 60-second timeout.
2247 # Default to 2 mins. Arbitrarily cap at 5 mins.
2248 $timeout = 120 if !$timeout or $timeout > 300;
2251 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2252 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2254 my $recs = search_cache($ckey, $offset, $limit);
2258 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2259 my $req = $ses->request($method, %$args);
2260 my $resp = $req->recv($timeout);
2262 if($resp and $recs = $resp->content) {
2263 put_cache($ckey, scalar(@$recs), $recs);
2264 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2273 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2274 my @recs = map { $_->[0] } @$recs;
2276 return { ids => \@recs, count => $count };
2280 foreach my $isbn_method (qw/
2281 open-ils.search.biblio.isbn
2282 open-ils.search.biblio.isbn.staff
2284 __PACKAGE__->register_method(
2285 method => "biblio_search_isbn",
2286 api_name => $isbn_method,
2288 desc => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2290 {desc => 'ISBN', type => 'string'}
2293 desc => 'Results object like: { "count": $i, "ids": [...] }',
2300 sub biblio_search_isbn {
2301 my( $self, $client, $isbn ) = @_;
2302 $logger->debug("Searching ISBN $isbn");
2303 # the previous implementation of this method was essentially unlimited,
2304 # so we will set our limit very high and let multiclass.query provide any
2306 # XXX: if making this unlimited is deemed important, we might consider
2307 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2308 # which is functionally deprecated at this point, or a custom call to
2309 # 'open-ils.storage.biblio.multiclass.search_fts'
2311 my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2312 if ($self->api_name =~ m/.staff$/) {
2313 $isbn_method .= '.staff';
2316 my $method = $self->method_lookup($isbn_method);
2317 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2318 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2319 return { ids => \@recs, count => $search_result->{'count'} };
2322 __PACKAGE__->register_method(
2323 method => "biblio_search_isbn_batch",
2324 api_name => "open-ils.search.biblio.isbn_list",
2327 # XXX: see biblio_search_isbn() for note concerning 'limit'
2328 sub biblio_search_isbn_batch {
2329 my( $self, $client, $isbn_list ) = @_;
2330 $logger->debug("Searching ISBNs @$isbn_list");
2331 my @recs = (); my %rec_set = ();
2332 my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2333 foreach my $isbn ( @$isbn_list ) {
2334 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2335 my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2336 foreach my $rec (@recs_subset) {
2337 if (! $rec_set{ $rec }) {
2338 $rec_set{ $rec } = 1;
2343 return { ids => \@recs, count => scalar(@recs) };
2346 foreach my $issn_method (qw/
2347 open-ils.search.biblio.issn
2348 open-ils.search.biblio.issn.staff
2350 __PACKAGE__->register_method(
2351 method => "biblio_search_issn",
2352 api_name => $issn_method,
2354 desc => 'Retrieve biblio IDs for a given ISSN',
2356 {desc => 'ISBN', type => 'string'}
2359 desc => 'Results object like: { "count": $i, "ids": [...] }',
2366 sub biblio_search_issn {
2367 my( $self, $client, $issn ) = @_;
2368 $logger->debug("Searching ISSN $issn");
2369 # the previous implementation of this method was essentially unlimited,
2370 # so we will set our limit very high and let multiclass.query provide any
2372 # XXX: if making this unlimited is deemed important, we might consider
2373 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2374 # which is functionally deprecated at this point, or a custom call to
2375 # 'open-ils.storage.biblio.multiclass.search_fts'
2377 my $issn_method = 'open-ils.search.biblio.multiclass.query';
2378 if ($self->api_name =~ m/.staff$/) {
2379 $issn_method .= '.staff';
2382 my $method = $self->method_lookup($issn_method);
2383 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2384 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2385 return { ids => \@recs, count => $search_result->{'count'} };
2389 __PACKAGE__->register_method(
2390 method => "fetch_mods_by_copy",
2391 api_name => "open-ils.search.biblio.mods_from_copy",
2394 desc => 'Retrieve MODS record given an attached copy ID',
2396 { desc => 'Copy ID', type => 'number' }
2399 desc => 'MODS record, event on error or uncataloged item'
2404 sub fetch_mods_by_copy {
2405 my( $self, $client, $copyid ) = @_;
2406 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2407 return $evt if $evt;
2408 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2409 return $apputils->record_to_mvr($record);
2413 # -------------------------------------------------------------------------------------
2415 __PACKAGE__->register_method(
2416 method => "cn_browse",
2417 api_name => "open-ils.search.callnumber.browse.target",
2418 notes => "Starts a callnumber browse"
2421 __PACKAGE__->register_method(
2422 method => "cn_browse",
2423 api_name => "open-ils.search.callnumber.browse.page_up",
2424 notes => "Returns the previous page of callnumbers",
2427 __PACKAGE__->register_method(
2428 method => "cn_browse",
2429 api_name => "open-ils.search.callnumber.browse.page_down",
2430 notes => "Returns the next page of callnumbers",
2434 # RETURNS array of arrays like so: label, owning_lib, record, id
2436 my( $self, $client, @params ) = @_;
2439 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2440 if( $self->api_name =~ /target/ );
2441 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2442 if( $self->api_name =~ /page_up/ );
2443 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2444 if( $self->api_name =~ /page_down/ );
2446 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2448 # -------------------------------------------------------------------------------------
2450 __PACKAGE__->register_method(
2451 method => "fetch_cn",
2452 api_name => "open-ils.search.callnumber.retrieve",
2454 notes => "retrieves a callnumber based on ID",
2458 my( $self, $client, $id ) = @_;
2460 my $e = new_editor();
2461 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2462 return $evt if $evt;
2466 __PACKAGE__->register_method(
2467 method => "fetch_fleshed_cn",
2468 api_name => "open-ils.search.callnumber.fleshed.retrieve",
2470 notes => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2473 sub fetch_fleshed_cn {
2474 my( $self, $client, $id ) = @_;
2476 my $e = new_editor();
2477 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2478 return $evt if $evt;
2483 __PACKAGE__->register_method(
2484 method => "fetch_copy_by_cn",
2485 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2487 Returns an array of copy ID's by callnumber ID
2488 @param cnid The callnumber ID
2489 @return An array of copy IDs
2493 sub fetch_copy_by_cn {
2494 my( $self, $conn, $cnid ) = @_;
2495 return $U->cstorereq(
2496 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2497 { call_number => $cnid, deleted => 'f' } );
2500 __PACKAGE__->register_method(
2501 method => 'fetch_cn_by_info',
2502 api_name => 'open-ils.search.call_number.retrieve_by_info',
2504 @param label The callnumber label
2505 @param record The record the cn is attached to
2506 @param org The owning library of the cn
2507 @return The callnumber object
2512 sub fetch_cn_by_info {
2513 my( $self, $conn, $label, $record, $org ) = @_;
2514 return $U->cstorereq(
2515 'open-ils.cstore.direct.asset.call_number.search',
2516 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2521 __PACKAGE__->register_method(
2522 method => 'bib_extras',
2523 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2526 __PACKAGE__->register_method(
2527 method => 'bib_extras',
2528 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2529 ctype => 'item_form'
2531 __PACKAGE__->register_method(
2532 method => 'bib_extras',
2533 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2534 ctype => 'item_type',
2536 __PACKAGE__->register_method(
2537 method => 'bib_extras',
2538 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2539 ctype => 'bib_level'
2541 __PACKAGE__->register_method(
2542 method => 'bib_extras',
2543 api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2549 $logger->warn("deprecation warning: " .$self->api_name);
2551 my $e = new_editor();
2553 my $ctype = $self->{ctype};
2554 my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2557 for my $ccvm (@$ccvms) {
2558 my $obj = "Fieldmapper::config::${ctype}_map"->new;
2559 $obj->value($ccvm->value);
2560 $obj->code($ccvm->code);
2561 $obj->description($ccvm->description) if $obj->can('description');
2570 __PACKAGE__->register_method(
2571 method => 'fetch_slim_record',
2572 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2574 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2576 { desc => 'Array of Record IDs', type => 'array' }
2579 desc => 'Array of biblio records, event on error'
2584 sub fetch_slim_record {
2585 my( $self, $conn, $ids ) = @_;
2587 #my $editor = OpenILS::Utils::Editor->new;
2588 my $editor = new_editor();
2591 return $editor->event unless
2592 my $r = $editor->retrieve_biblio_record_entry($_);
2599 __PACKAGE__->register_method(
2600 method => 'rec_hold_parts',
2601 api_name => 'open-ils.search.biblio.record_hold_parts',
2603 Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2607 sub rec_hold_parts {
2608 my( $self, $conn, $args ) = @_;
2610 my $rec = $$args{record};
2611 my $mrec = $$args{metarecord};
2612 my $pickup_lib = $$args{pickup_lib};
2613 my $e = new_editor();
2616 select => {bmp => ['id', 'label']},
2621 select => {'acpm' => ['part']},
2622 from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2624 '+acp' => {'deleted' => 'f'},
2625 '+bre' => {id => $rec}
2631 order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2634 if(defined $pickup_lib) {
2635 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2636 if($hard_boundary) {
2637 my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2638 $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2642 return $e->json_query($query);
2648 __PACKAGE__->register_method(
2649 method => 'rec_to_mr_rec_descriptors',
2650 api_name => 'open-ils.search.metabib.record_to_descriptors',
2652 specialized method...
2653 Given a biblio record id or a metarecord id,
2654 this returns a list of metabib.record_descriptor
2655 objects that live within the same metarecord
2656 @param args Object of args including:
2660 sub rec_to_mr_rec_descriptors {
2661 my( $self, $conn, $args ) = @_;
2663 my $rec = $$args{record};
2664 my $mrec = $$args{metarecord};
2665 my $item_forms = $$args{item_forms};
2666 my $item_types = $$args{item_types};
2667 my $item_lang = $$args{item_lang};
2668 my $pickup_lib = $$args{pickup_lib};
2670 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2672 my $e = new_editor();
2676 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2677 return $e->event unless @$map;
2678 $mrec = $$map[0]->metarecord;
2681 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2682 return $e->event unless @$recs;
2684 my @recs = map { $_->source } @$recs;
2685 my $search = { record => \@recs };
2686 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2687 $search->{item_type} = $item_types if $item_types and @$item_types;
2688 $search->{item_lang} = $item_lang if $item_lang;
2690 my $desc = $e->search_metabib_record_descriptor($search);
2694 select => { 'bre' => ['id'] },
2699 'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2705 '+bre' => { id => \@recs },
2710 "+ccs" => { holdable => 't' },
2711 "+acpl" => { holdable => 't' }
2715 if ($hard_boundary) { # 0 (or "top") is the same as no setting
2716 my $orgs = $e->json_query(
2717 { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2718 ) or return $e->die_event;
2720 $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2723 my $good_records = $e->json_query($query) or return $e->die_event;
2726 for my $d (@$desc) {
2727 if ( grep { $d->record == $_->{id} } @$good_records ) {
2734 return { metarecord => $mrec, descriptors => $desc };
2738 __PACKAGE__->register_method(
2739 method => 'fetch_age_protect',
2740 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2743 sub fetch_age_protect {
2744 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2748 __PACKAGE__->register_method(
2749 method => 'copies_by_cn_label',
2750 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2753 __PACKAGE__->register_method(
2754 method => 'copies_by_cn_label',
2755 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2758 sub copies_by_cn_label {
2759 my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2760 my $e = new_editor();
2761 my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2762 my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2763 my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2764 return [] unless @$cns;
2766 # show all non-deleted copies in the staff client ...
2767 if ($self->api_name =~ /staff$/o) {
2768 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2771 # ... otherwise, grab the copies ...
2772 my $copies = $e->search_asset_copy(
2773 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2774 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2778 # ... and test for location and status visibility
2779 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];