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};
1005 $$searchhash{searches}{'identifier|upc'}{term} = $$args{search}{upc} if $$args{search}{upc};
1007 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
1008 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
1009 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
1010 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
1012 my $method = 'open-ils.search.biblio.multiclass.staged';
1013 $method .= '.staff' if $self->api_name =~ /staff$/;
1015 my ($list) = $self->method_lookup($method)->run( $searchhash );
1017 if ($list->{count} > 0 and @{$list->{ids}}) {
1018 $result->{count} = $list->{count};
1020 my $records = $cstore->request(
1021 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
1022 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
1025 for my $rec ( @$records ) {
1027 my $u = OpenILS::Utils::ModsParser->new();
1028 $u->start_mods_batch( $rec->marc );
1029 my $mods = $u->finish_mods_batch();
1031 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
1037 $cstore->disconnect();
1041 # ----------------------------------------------------------------------------
1042 # These are the main OPAC search methods
1043 # ----------------------------------------------------------------------------
1045 __PACKAGE__->register_method(
1046 method => 'the_quest_for_knowledge',
1047 api_name => 'open-ils.search.biblio.multiclass',
1049 desc => "Performs a multi class biblio or metabib search",
1052 desc => "A search hash with keys: "
1053 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
1054 . "See perldoc " . __PACKAGE__ . " for more detail",
1058 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
1063 desc => 'An object of the form: '
1064 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
1069 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
1071 The search-hash argument can have the following elements:
1073 searches: { "$class" : "$value", ...} [REQUIRED]
1074 org_unit: The org id to focus the search at
1075 depth : The org depth
1076 limit : The search limit default: 10
1077 offset : The search offset default: 0
1078 format : The MARC format
1079 sort : What field to sort the results on? [ author | title | pubdate ]
1080 sort_dir: What direction do we sort? [ asc | desc ]
1081 tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
1082 will be tagged with an additional value ("1") as the last value in the record ID array for
1083 each record. Requires the 'authtoken'
1084 authtoken : Authentication token string; When actions are performed that require a user login
1085 (e.g. tagging circulated records), the authentication token is required
1087 The searches element is required, must have a hashref value, and the hashref must contain at least one
1088 of the following classes as a key:
1096 The value paired with a key is the associated search string.
1098 The docache argument enables/disables searching and saving results in cache (default OFF).
1100 The return object, if successful, will look like:
1102 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
1106 __PACKAGE__->register_method(
1107 method => 'the_quest_for_knowledge',
1108 api_name => 'open-ils.search.biblio.multiclass.staff',
1109 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1111 __PACKAGE__->register_method(
1112 method => 'the_quest_for_knowledge',
1113 api_name => 'open-ils.search.metabib.multiclass',
1114 signature => q/@see open-ils.search.biblio.multiclass/
1116 __PACKAGE__->register_method(
1117 method => 'the_quest_for_knowledge',
1118 api_name => 'open-ils.search.metabib.multiclass.staff',
1119 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1122 sub the_quest_for_knowledge {
1123 my( $self, $conn, $searchhash, $docache ) = @_;
1125 return { count => 0 } unless $searchhash and
1126 ref $searchhash->{searches} eq 'HASH';
1128 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
1132 if($self->api_name =~ /metabib/) {
1134 $method =~ s/biblio/metabib/o;
1137 # do some simple sanity checking
1138 if(!$searchhash->{searches} or
1139 ( !grep { /^(?:title|author|subject|series|keyword|identifier\|is[bs]n)/ } keys %{$searchhash->{searches}} ) ) {
1140 return { count => 0 };
1143 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
1144 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
1145 my $end = $offset + $limit - 1;
1147 my $maxlimit = 5000;
1148 $searchhash->{offset} = 0; # possible user value overwritten in hash
1149 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
1151 return { count => 0 } if $offset > $maxlimit;
1154 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
1155 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
1156 my $ckey = $pfx . md5_hex($method . $s);
1158 $logger->info("bib search for: $s");
1160 $searchhash->{limit} -= $offset;
1164 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1168 $method .= ".staff" if($self->api_name =~ /staff/);
1169 $method .= ".atomic";
1171 for (keys %$searchhash) {
1172 delete $$searchhash{$_}
1173 unless defined $$searchhash{$_};
1176 $result = $U->storagereq( $method, %$searchhash );
1180 $docache = 0; # results came FROM cache, so we don't write back
1183 return {count => 0} unless ($result && $$result[0]);
1187 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1190 # If we didn't get this data from the cache, put it into the cache
1191 # then return the correct offset of records
1192 $logger->debug("putting search cache $ckey\n");
1193 put_cache($ckey, $count, \@recs);
1197 # if we have the full set of data, trim out
1198 # the requested chunk based on limit and offset
1200 for ($offset..$end) {
1201 last unless $recs[$_];
1202 push(@t, $recs[$_]);
1207 return { ids => \@recs, count => $count };
1211 __PACKAGE__->register_method(
1212 method => 'staged_search',
1213 api_name => 'open-ils.search.biblio.multiclass.staged',
1215 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1216 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1219 desc => "A search hash with keys: "
1220 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1221 . "See perldoc " . __PACKAGE__ . " for more detail",
1225 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1230 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1231 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1236 __PACKAGE__->register_method(
1237 method => 'staged_search',
1238 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1239 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1241 __PACKAGE__->register_method(
1242 method => 'staged_search',
1243 api_name => 'open-ils.search.metabib.multiclass.staged',
1244 signature => q/@see open-ils.search.biblio.multiclass.staged/
1246 __PACKAGE__->register_method(
1247 method => 'staged_search',
1248 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1249 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1253 my($self, $conn, $search_hash, $docache) = @_;
1255 my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1257 my $method = $IAmMetabib?
1258 'open-ils.storage.metabib.multiclass.staged.search_fts':
1259 'open-ils.storage.biblio.multiclass.staged.search_fts';
1261 $method .= '.staff' if $self->api_name =~ /staff$/;
1262 $method .= '.atomic';
1264 return {count => 0} unless (
1266 $search_hash->{searches} and
1267 scalar( keys %{$search_hash->{searches}} ));
1269 my $search_duration;
1270 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1271 my $user_limit = $search_hash->{limit} || 10;
1272 my $ignore_facet_classes = $search_hash->{ignore_facet_classes};
1273 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1274 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1277 # we're grabbing results on a per-superpage basis, which means the
1278 # limit and offset should coincide with superpage boundaries
1279 $search_hash->{offset} = 0;
1280 $search_hash->{limit} = $superpage_size;
1282 # force a well-known check_limit
1283 $search_hash->{check_limit} = $superpage_size;
1284 # restrict total tested to superpage size * number of superpages
1285 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1287 # Set the configured estimation strategy, defaults to 'inclusion'.
1288 my $estimation_strategy = OpenSRF::Utils::SettingsClient
1291 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1293 $search_hash->{estimation_strategy} = $estimation_strategy;
1295 # pull any existing results from the cache
1296 my $key = search_cache_key($method, $search_hash);
1297 my $facet_key = $key.'_facets';
1298 my $cache_data = $cache->get_cache($key) || {};
1300 # First, we want to make sure that someone else isn't currently trying to perform exactly
1301 # this same search. The point is to allow just one instance of a search to fill the needs
1302 # of all concurrent, identical searches. This will avoid spammy searches killing the
1303 # database without requiring admins to start locking some IP addresses out entirely.
1305 # There's still a tiny race condition where 2 might run, but without sigificantly more code
1306 # and complexity, this is close to the best we can do.
1308 if ($cache_data->{running}) { # someone is already doing the search...
1309 my $stop_looping = time() + $cache_timeout;
1310 while ( sleep(1) and time() < $stop_looping ) { # sleep for a second ... maybe they'll finish
1311 $cache_data = $cache->get_cache($key) || {};
1312 last if (!$cache_data->{running});
1314 } elsif (!$cache_data->{0}) { # we're the first ... let's give it a try
1315 $cache->put_cache($key, { running => $$ }, $cache_timeout / 3);
1318 # keep retrieving results until we find enough to
1319 # fulfill the user-specified limit and offset
1320 my $all_results = [];
1321 my $page; # current superpage
1322 my $est_hit_count = 0;
1323 my $current_page_summary = {};
1324 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1325 my $is_real_hit_count = 0;
1328 for($page = 0; $page < $max_superpages; $page++) {
1330 my $data = $cache_data->{$page};
1334 $logger->debug("staged search: analyzing superpage $page");
1337 # this window of results is already cached
1338 $logger->debug("staged search: found cached results");
1339 $summary = $data->{summary};
1340 $results = $data->{results};
1343 # retrieve the window of results from the database
1344 $logger->debug("staged search: fetching results from the database");
1345 $search_hash->{skip_check} = $page * $superpage_size;
1347 $results = $U->storagereq($method, %$search_hash);
1348 $search_duration = time - $start;
1349 $summary = shift(@$results) if $results;
1352 $logger->info("search timed out: duration=$search_duration: params=".
1353 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1354 return {count => 0};
1357 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1359 my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1361 $logger->info("search returned 0 results: duration=$search_duration: params=".
1362 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1365 # Create backwards-compatible result structures
1367 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}, $_->{rel}, $_->{record}]} @$results];
1369 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}]} @$results];
1372 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1373 $results = [grep {defined $_->[0]} @$results];
1374 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1377 tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib)
1378 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1380 $current_page_summary = $summary;
1382 # add the new set of results to the set under construction
1383 push(@$all_results, @$results);
1385 my $current_count = scalar(@$all_results);
1387 $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1390 $logger->debug("staged search: located $current_count, with estimated hits=".
1391 ($summary->{estimated_hit_count} || "none") .
1392 " : visible=" . ($summary->{visible} || "none") . ", checked=" .
1393 ($summary->{checked} || "none")
1396 if (defined($summary->{estimated_hit_count})) {
1397 foreach (qw/ checked visible excluded deleted /) {
1398 $global_summary->{$_} += $summary->{$_};
1400 $global_summary->{total} = $summary->{total};
1403 # we've found all the possible hits
1404 last if $current_count == $summary->{visible}
1405 and not defined $summary->{estimated_hit_count};
1407 # we've found enough results to satisfy the requested limit/offset
1408 last if $current_count >= ($user_limit + $user_offset);
1410 # we've scanned all possible hits
1411 if($summary->{checked} < $superpage_size) {
1412 $est_hit_count = scalar(@$all_results);
1413 # we have all possible results in hand, so we know the final hit count
1414 $is_real_hit_count = 1;
1419 # Let other backends grab our data now that we're done.
1420 $cache_data = $cache->get_cache($key);
1421 if ($$cache_data{running} and $$cache_data{running} == $$) {
1422 delete $$cache_data{running};
1423 $cache->put_cache($key, $cache_data, $cache_timeout);
1426 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1428 # refine the estimate if we have more than one superpage
1429 if ($page > 0 and not $is_real_hit_count) {
1430 if ($global_summary->{checked} >= $global_summary->{total}) {
1431 $est_hit_count = $global_summary->{visible};
1433 my $updated_hit_count = $U->storagereq(
1434 'open-ils.storage.fts_paging_estimate',
1435 $global_summary->{checked},
1436 $global_summary->{visible},
1437 $global_summary->{excluded},
1438 $global_summary->{deleted},
1439 $global_summary->{total}
1441 $est_hit_count = $updated_hit_count->{$estimation_strategy};
1445 $conn->respond_complete(
1447 count => $est_hit_count,
1448 core_limit => $search_hash->{core_limit},
1449 superpage_size => $search_hash->{check_limit},
1450 superpage_summary => $current_page_summary,
1451 facet_key => $facet_key,
1456 cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1461 sub tag_circulated_records {
1462 my ($auth, $results, $metabib) = @_;
1463 my $e = new_editor(authtoken => $auth);
1464 return $results unless $e->checkauth;
1467 select => { acn => [{ column => 'record', alias => 'tagme' }] },
1468 from => { auch => { acp => { join => 'acn' }} },
1469 where => { usr => $e->requestor->id },
1475 select => { mmrsm => [{ column => 'metarecord', alias => 'tagme' }] },
1477 where => { source => { in => $query } },
1482 # Give me the distinct set of bib records that exist in the user's visible circulation history
1483 my $circ_recs = $e->json_query( $query );
1485 # if the record appears in the circ history, push a 1 onto
1486 # the rec array structure to indicate truthiness
1487 for my $rec (@$results) {
1488 push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1494 # creates a unique token to represent the query in the cache
1495 sub search_cache_key {
1497 my $search_hash = shift;
1499 for my $key (sort keys %$search_hash) {
1500 push(@sorted, ($key => $$search_hash{$key}))
1501 unless $key eq 'limit' or
1503 $key eq 'skip_check';
1505 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1506 return $pfx . md5_hex($method . $s);
1509 sub retrieve_cached_facets {
1515 return undef unless ($key and $key =~ /_facets$/);
1517 my $blob = $cache->get_cache($key) || {};
1521 for my $f ( keys %$blob ) {
1522 my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1523 @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1524 for my $s ( @sorted ) {
1525 my ($k) = keys(%$s);
1526 my ($v) = values(%$s);
1527 $$facets{$f}{$k} = $v;
1537 __PACKAGE__->register_method(
1538 method => "retrieve_cached_facets",
1539 api_name => "open-ils.search.facet_cache.retrieve",
1541 desc => 'Returns facet data derived from a specific search based on a key '.
1542 'generated by open-ils.search.biblio.multiclass.staged and friends.',
1545 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1550 desc => 'Two level hash of facet values. Top level key is the facet id defined on the config.metabib_field table. '.
1551 'Second level key is a string facet value. Datum attached to each facet value is the number of distinct records, '.
1552 'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1553 'facet retrieval. These counts are calculated for all superpages that have been checked for visibility.',
1561 # add facets for this search to the facet cache
1562 my($key, $results, $metabib, $ignore) = @_;
1563 my $data = $cache->get_cache($key);
1566 return undef unless (@$results);
1568 my $facets_function = $metabib ? 'search.facets_for_metarecord_set'
1569 : 'search.facets_for_record_set';
1570 my $results_str = '{' . join(',', @$results) . '}';
1571 my $ignore_str = ref($ignore) ? '{' . join(',', @$ignore) . '}'
1574 from => [ $facets_function, $ignore_str, $results_str ]
1577 my $facets = OpenILS::Utils::CStoreEditor->new->json_query($query, {substream => 1});
1579 for my $facet (@$facets) {
1580 next unless ($facet->{value});
1581 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1584 $logger->info("facet compilation: cached with key=$key");
1586 $cache->put_cache($key, $data, $cache_timeout);
1589 sub cache_staged_search_page {
1590 # puts this set of results into the cache
1591 my($key, $page, $summary, $results) = @_;
1592 my $data = $cache->get_cache($key);
1595 summary => $summary,
1599 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1600 ($summary->{estimated_hit_count} || "none") .
1601 ", visible=" . ($summary->{visible} || "none")
1604 $cache->put_cache($key, $data, $cache_timeout);
1612 my $start = $offset;
1613 my $end = $offset + $limit - 1;
1615 $logger->debug("searching cache for $key : $start..$end\n");
1617 return undef unless $cache;
1618 my $data = $cache->get_cache($key);
1620 return undef unless $data;
1622 my $count = $data->[0];
1625 return undef unless $offset < $count;
1628 for( my $i = $offset; $i <= $end; $i++ ) {
1629 last unless my $d = $$data[$i];
1630 push( @result, $d );
1633 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1640 my( $key, $count, $data ) = @_;
1641 return undef unless $cache;
1642 $logger->debug("search_cache putting ".
1643 scalar(@$data)." items at key $key with timeout $cache_timeout");
1644 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1648 __PACKAGE__->register_method(
1649 method => "biblio_mrid_to_modsbatch_batch",
1650 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1653 sub biblio_mrid_to_modsbatch_batch {
1654 my( $self, $client, $mrids) = @_;
1655 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1657 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1658 for my $id (@$mrids) {
1659 next unless defined $id;
1660 my ($m) = $method->run($id);
1667 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1668 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1670 __PACKAGE__->register_method(
1671 method => "biblio_mrid_to_modsbatch",
1674 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1675 . "As usual, the .staff version of this method will include otherwise hidden records.",
1677 { desc => 'Metarecord ID', type => 'number' },
1678 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1681 desc => 'MVR Object, event on error',
1687 sub biblio_mrid_to_modsbatch {
1688 my( $self, $client, $mrid, $args) = @_;
1690 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1692 my ($mr, $evt) = _grab_metarecord($mrid);
1693 return $evt unless $mr;
1695 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1696 biblio_mrid_make_modsbatch($self, $client, $mr);
1698 return $mvr unless ref($args);
1700 # Here we find the lead record appropriate for the given filters
1701 # and use that for the title and author of the metarecord
1702 my $format = $$args{format};
1703 my $org = $$args{org};
1704 my $depth = $$args{depth};
1706 return $mvr unless $format or $org or $depth;
1708 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1709 $method = "$method.staff" if $self->api_name =~ /staff/o;
1711 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1713 if( my $mods = $U->record_to_mvr($rec) ) {
1715 $mvr->title( $mods->title );
1716 $mvr->author($mods->author);
1717 $logger->debug("mods_slim updating title and ".
1718 "author in mvr with ".$mods->title." : ".$mods->author);
1724 # converts a metarecord to an mvr
1727 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1728 return Fieldmapper::metabib::virtual_record->new($perl);
1731 # checks to see if a metarecord has mods, if so returns true;
1733 __PACKAGE__->register_method(
1734 method => "biblio_mrid_check_mvr",
1735 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1736 notes => "Takes a metarecord ID or a metarecord object and returns true "
1737 . "if the metarecord already has an mvr associated with it."
1740 sub biblio_mrid_check_mvr {
1741 my( $self, $client, $mrid ) = @_;
1745 if(ref($mrid)) { $mr = $mrid; }
1746 else { ($mr, $evt) = _grab_metarecord($mrid); }
1747 return $evt if $evt;
1749 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1751 return _mr_to_mvr($mr) if $mr->mods();
1755 sub _grab_metarecord {
1757 my $e = new_editor();
1758 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1763 __PACKAGE__->register_method(
1764 method => "biblio_mrid_make_modsbatch",
1765 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1766 notes => "Takes either a metarecord ID or a metarecord object. "
1767 . "Forces the creations of an mvr for the given metarecord. "
1768 . "The created mvr is returned."
1771 sub biblio_mrid_make_modsbatch {
1772 my( $self, $client, $mrid ) = @_;
1774 my $e = new_editor();
1781 $mr = $e->retrieve_metabib_metarecord($mrid)
1782 or return $e->event;
1785 my $masterid = $mr->master_record;
1786 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1788 my $ids = $U->storagereq(
1789 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1790 return undef unless @$ids;
1792 my $master = $e->retrieve_biblio_record_entry($masterid)
1793 or return $e->event;
1795 # start the mods batch
1796 my $u = OpenILS::Utils::ModsParser->new();
1797 $u->start_mods_batch( $master->marc );
1799 # grab all of the sub-records and shove them into the batch
1800 my @ids = grep { $_ ne $masterid } @$ids;
1801 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1806 my $r = $e->retrieve_biblio_record_entry($i);
1807 push( @$subrecs, $r ) if $r;
1812 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1813 $u->push_mods_batch( $_->marc ) if $_->marc;
1817 # finish up and send to the client
1818 my $mods = $u->finish_mods_batch();
1819 $mods->doc_id($mrid);
1820 $client->respond_complete($mods);
1823 # now update the mods string in the db
1824 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1827 $e = new_editor(xact => 1);
1828 $e->update_metabib_metarecord($mr)
1829 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1836 # converts a mr id into a list of record ids
1838 foreach (qw/open-ils.search.biblio.metarecord_to_records
1839 open-ils.search.biblio.metarecord_to_records.staff/)
1841 __PACKAGE__->register_method(
1842 method => "biblio_mrid_to_record_ids",
1845 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1846 . "As usual, the .staff version of this method will include otherwise hidden records.",
1848 { desc => 'Metarecord ID', type => 'number' },
1849 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1852 desc => 'Results object like {count => $i, ids =>[...]}',
1860 sub biblio_mrid_to_record_ids {
1861 my( $self, $client, $mrid, $args ) = @_;
1863 my $format = $$args{format};
1864 my $org = $$args{org};
1865 my $depth = $$args{depth};
1867 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1868 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1869 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1871 return { count => scalar(@$recs), ids => $recs };
1875 __PACKAGE__->register_method(
1876 method => "biblio_record_to_marc_html",
1877 api_name => "open-ils.search.biblio.record.html"
1880 __PACKAGE__->register_method(
1881 method => "biblio_record_to_marc_html",
1882 api_name => "open-ils.search.authority.to_html"
1885 # Persistent parsers and setting objects
1886 my $parser = XML::LibXML->new();
1887 my $xslt = XML::LibXSLT->new();
1889 my $slim_marc_sheet;
1890 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1892 sub biblio_record_to_marc_html {
1893 my($self, $client, $recordid, $slim, $marcxml) = @_;
1896 my $dir = $settings_client->config_value("dirs", "xsl");
1899 unless($slim_marc_sheet) {
1900 my $xsl = $settings_client->config_value(
1901 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1903 $xsl = $parser->parse_file("$dir/$xsl");
1904 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1907 $sheet = $slim_marc_sheet;
1911 unless($marc_sheet) {
1912 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1913 my $xsl = $settings_client->config_value(
1914 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1915 $xsl = $parser->parse_file("$dir/$xsl");
1916 $marc_sheet = $xslt->parse_stylesheet($xsl);
1918 $sheet = $marc_sheet;
1923 my $e = new_editor();
1924 if($self->api_name =~ /authority/) {
1925 $record = $e->retrieve_authority_record_entry($recordid)
1926 or return $e->event;
1928 $record = $e->retrieve_biblio_record_entry($recordid)
1929 or return $e->event;
1931 $marcxml = $record->marc;
1934 my $xmldoc = $parser->parse_string($marcxml);
1935 my $html = $sheet->transform($xmldoc);
1936 return $html->documentElement->toString();
1939 __PACKAGE__->register_method(
1940 method => "format_biblio_record_entry",
1941 api_name => "open-ils.search.biblio.record.print",
1943 desc => 'Returns a printable version of the specified bib record',
1945 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1948 desc => q/An action_trigger.event object or error event./,
1953 __PACKAGE__->register_method(
1954 method => "format_biblio_record_entry",
1955 api_name => "open-ils.search.biblio.record.email",
1957 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1959 { desc => 'Authentication token', type => 'string'},
1960 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1963 desc => q/Undefined on success, otherwise an error event./,
1969 sub format_biblio_record_entry {
1970 my($self, $conn, $arg1, $arg2) = @_;
1972 my $for_print = ($self->api_name =~ /print/);
1973 my $for_email = ($self->api_name =~ /email/);
1975 my $e; my $auth; my $bib_id; my $context_org;
1979 $context_org = $arg2 || $U->get_org_tree->id;
1980 $e = new_editor(xact => 1);
1981 } elsif ($for_email) {
1984 $e = new_editor(authtoken => $auth, xact => 1);
1985 return $e->die_event unless $e->checkauth;
1986 $context_org = $e->requestor->home_ou;
1990 if (ref $bib_id ne 'ARRAY') {
1991 $bib_ids = [ $bib_id ];
1996 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1997 $bucket->btype('temp');
1998 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
2000 $bucket->owner($e->requestor)
2004 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
2006 for my $id (@$bib_ids) {
2008 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
2010 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
2011 $bucket_entry->target_biblio_record_entry($bib);
2012 $bucket_entry->bucket($bucket_obj->id);
2013 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
2020 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
2022 } elsif ($for_email) {
2024 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
2031 __PACKAGE__->register_method(
2032 method => "retrieve_all_copy_statuses",
2033 api_name => "open-ils.search.config.copy_status.retrieve.all"
2036 sub retrieve_all_copy_statuses {
2037 my( $self, $client ) = @_;
2038 return new_editor()->retrieve_all_config_copy_status();
2042 __PACKAGE__->register_method(
2043 method => "copy_counts_per_org",
2044 api_name => "open-ils.search.biblio.copy_counts.retrieve"
2047 __PACKAGE__->register_method(
2048 method => "copy_counts_per_org",
2049 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
2052 sub copy_counts_per_org {
2053 my( $self, $client, $record_id ) = @_;
2055 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
2057 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
2058 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
2060 my $counts = $apputils->simple_scalar_request(
2061 "open-ils.storage", $method, $record_id );
2063 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
2068 __PACKAGE__->register_method(
2069 method => "copy_count_summary",
2070 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
2071 notes => "returns an array of these: "
2072 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2073 . "where statusx is a copy status name. The statuses are sorted by ID.",
2077 sub copy_count_summary {
2078 my( $self, $client, $rid, $org, $depth ) = @_;
2081 my $data = $U->storagereq(
2082 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
2085 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2087 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2091 __PACKAGE__->register_method(
2092 method => "copy_location_count_summary",
2093 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
2094 notes => "returns an array of these: "
2095 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
2096 . "where statusx is a copy status name. The statuses are sorted by ID.",
2099 sub copy_location_count_summary {
2100 my( $self, $client, $rid, $org, $depth ) = @_;
2103 my $data = $U->storagereq(
2104 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2107 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2109 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2111 || $a->[4] cmp $b->[4]
2115 __PACKAGE__->register_method(
2116 method => "copy_count_location_summary",
2117 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2118 notes => "returns an array of these: "
2119 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2120 . "where statusx is a copy status name. The statuses are sorted by ID."
2123 sub copy_count_location_summary {
2124 my( $self, $client, $rid, $org, $depth ) = @_;
2127 my $data = $U->storagereq(
2128 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2130 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2132 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2137 foreach (qw/open-ils.search.biblio.marc
2138 open-ils.search.biblio.marc.staff/)
2140 __PACKAGE__->register_method(
2141 method => "marc_search",
2144 desc => 'Fetch biblio IDs based on MARC record criteria. '
2145 . 'As usual, the .staff version of the search includes otherwise hidden records',
2148 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2149 'See perldoc ' . __PACKAGE__ . ' for more detail.',
2152 {desc => 'timeout (optional)', type => 'number'}
2155 desc => 'Results object like: { "count": $i, "ids": [...] }',
2162 =head3 open-ils.search.biblio.marc (arghash, timeout)
2164 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
2166 searches: complex query object (required)
2167 org_unit: The org ID to focus the search at
2168 depth : The org depth
2169 limit : integer search limit default: 10
2170 offset : integer search offset default: 0
2171 sort : What field to sort the results on? [ author | title | pubdate ]
2172 sort_dir: In what direction do we sort? [ asc | desc ]
2174 Additional keys to refine search criteria:
2177 language : Language (code)
2178 lit_form : Literary form
2179 item_form: Item form
2180 item_type: Item type
2181 format : The MARC format
2183 Please note that the specific strings to be used in the "addtional keys" will be entirely
2184 dependent on your loaded data.
2186 All keys except "searches" are optional.
2187 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
2189 For example, an arg hash might look like:
2211 The arghash is eventually passed to the SRF call:
2212 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2214 Presently, search uses the cache unconditionally.
2218 # FIXME: that example above isn't actually tested.
2219 # FIXME: sort and limit added. item_type not tested yet.
2220 # TODO: docache option?
2222 my( $self, $conn, $args, $timeout ) = @_;
2224 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2225 $method .= ".staff" if $self->api_name =~ /staff/;
2226 $method .= ".atomic";
2228 my $limit = $args->{limit} || 10;
2229 my $offset = $args->{offset} || 0;
2231 # allow caller to pass in a call timeout since MARC searches
2232 # can take longer than the default 60-second timeout.
2233 # Default to 2 mins. Arbitrarily cap at 5 mins.
2234 $timeout = 120 if !$timeout or $timeout > 300;
2237 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2238 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2240 my $recs = search_cache($ckey, $offset, $limit);
2244 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2245 my $req = $ses->request($method, %$args);
2246 my $resp = $req->recv($timeout);
2248 if($resp and $recs = $resp->content) {
2249 put_cache($ckey, scalar(@$recs), $recs);
2258 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2259 my @recs = map { $_->[0] } @$recs;
2261 return { ids => \@recs, count => $count };
2265 foreach my $isbn_method (qw/
2266 open-ils.search.biblio.isbn
2267 open-ils.search.biblio.isbn.staff
2269 __PACKAGE__->register_method(
2270 method => "biblio_search_isbn",
2271 api_name => $isbn_method,
2273 desc => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2275 {desc => 'ISBN', type => 'string'}
2278 desc => 'Results object like: { "count": $i, "ids": [...] }',
2285 sub biblio_search_isbn {
2286 my( $self, $client, $isbn ) = @_;
2287 $logger->debug("Searching ISBN $isbn");
2288 # the previous implementation of this method was essentially unlimited,
2289 # so we will set our limit very high and let multiclass.query provide any
2291 # XXX: if making this unlimited is deemed important, we might consider
2292 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2293 # which is functionally deprecated at this point, or a custom call to
2294 # 'open-ils.storage.biblio.multiclass.search_fts'
2296 my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2297 if ($self->api_name =~ m/.staff$/) {
2298 $isbn_method .= '.staff';
2301 my $method = $self->method_lookup($isbn_method);
2302 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2303 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2304 return { ids => \@recs, count => $search_result->{'count'} };
2307 __PACKAGE__->register_method(
2308 method => "biblio_search_isbn_batch",
2309 api_name => "open-ils.search.biblio.isbn_list",
2312 # XXX: see biblio_search_isbn() for note concerning 'limit'
2313 sub biblio_search_isbn_batch {
2314 my( $self, $client, $isbn_list ) = @_;
2315 $logger->debug("Searching ISBNs @$isbn_list");
2316 my @recs = (); my %rec_set = ();
2317 my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2318 foreach my $isbn ( @$isbn_list ) {
2319 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2320 my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2321 foreach my $rec (@recs_subset) {
2322 if (! $rec_set{ $rec }) {
2323 $rec_set{ $rec } = 1;
2328 return { ids => \@recs, count => scalar(@recs) };
2331 foreach my $issn_method (qw/
2332 open-ils.search.biblio.issn
2333 open-ils.search.biblio.issn.staff
2335 __PACKAGE__->register_method(
2336 method => "biblio_search_issn",
2337 api_name => $issn_method,
2339 desc => 'Retrieve biblio IDs for a given ISSN',
2341 {desc => 'ISBN', type => 'string'}
2344 desc => 'Results object like: { "count": $i, "ids": [...] }',
2351 sub biblio_search_issn {
2352 my( $self, $client, $issn ) = @_;
2353 $logger->debug("Searching ISSN $issn");
2354 # the previous implementation of this method was essentially unlimited,
2355 # so we will set our limit very high and let multiclass.query provide any
2357 # XXX: if making this unlimited is deemed important, we might consider
2358 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2359 # which is functionally deprecated at this point, or a custom call to
2360 # 'open-ils.storage.biblio.multiclass.search_fts'
2362 my $issn_method = 'open-ils.search.biblio.multiclass.query';
2363 if ($self->api_name =~ m/.staff$/) {
2364 $issn_method .= '.staff';
2367 my $method = $self->method_lookup($issn_method);
2368 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2369 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2370 return { ids => \@recs, count => $search_result->{'count'} };
2374 __PACKAGE__->register_method(
2375 method => "fetch_mods_by_copy",
2376 api_name => "open-ils.search.biblio.mods_from_copy",
2379 desc => 'Retrieve MODS record given an attached copy ID',
2381 { desc => 'Copy ID', type => 'number' }
2384 desc => 'MODS record, event on error or uncataloged item'
2389 sub fetch_mods_by_copy {
2390 my( $self, $client, $copyid ) = @_;
2391 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2392 return $evt if $evt;
2393 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2394 return $apputils->record_to_mvr($record);
2398 # -------------------------------------------------------------------------------------
2400 __PACKAGE__->register_method(
2401 method => "cn_browse",
2402 api_name => "open-ils.search.callnumber.browse.target",
2403 notes => "Starts a callnumber browse"
2406 __PACKAGE__->register_method(
2407 method => "cn_browse",
2408 api_name => "open-ils.search.callnumber.browse.page_up",
2409 notes => "Returns the previous page of callnumbers",
2412 __PACKAGE__->register_method(
2413 method => "cn_browse",
2414 api_name => "open-ils.search.callnumber.browse.page_down",
2415 notes => "Returns the next page of callnumbers",
2419 # RETURNS array of arrays like so: label, owning_lib, record, id
2421 my( $self, $client, @params ) = @_;
2424 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2425 if( $self->api_name =~ /target/ );
2426 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2427 if( $self->api_name =~ /page_up/ );
2428 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2429 if( $self->api_name =~ /page_down/ );
2431 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2433 # -------------------------------------------------------------------------------------
2435 __PACKAGE__->register_method(
2436 method => "fetch_cn",
2437 api_name => "open-ils.search.callnumber.retrieve",
2439 notes => "retrieves a callnumber based on ID",
2443 my( $self, $client, $id ) = @_;
2445 my $e = new_editor();
2446 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2447 return $evt if $evt;
2451 __PACKAGE__->register_method(
2452 method => "fetch_fleshed_cn",
2453 api_name => "open-ils.search.callnumber.fleshed.retrieve",
2455 notes => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2458 sub fetch_fleshed_cn {
2459 my( $self, $client, $id ) = @_;
2461 my $e = new_editor();
2462 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2463 return $evt if $evt;
2468 __PACKAGE__->register_method(
2469 method => "fetch_copy_by_cn",
2470 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2472 Returns an array of copy ID's by callnumber ID
2473 @param cnid The callnumber ID
2474 @return An array of copy IDs
2478 sub fetch_copy_by_cn {
2479 my( $self, $conn, $cnid ) = @_;
2480 return $U->cstorereq(
2481 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2482 { call_number => $cnid, deleted => 'f' } );
2485 __PACKAGE__->register_method(
2486 method => 'fetch_cn_by_info',
2487 api_name => 'open-ils.search.call_number.retrieve_by_info',
2489 @param label The callnumber label
2490 @param record The record the cn is attached to
2491 @param org The owning library of the cn
2492 @return The callnumber object
2497 sub fetch_cn_by_info {
2498 my( $self, $conn, $label, $record, $org ) = @_;
2499 return $U->cstorereq(
2500 'open-ils.cstore.direct.asset.call_number.search',
2501 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2506 __PACKAGE__->register_method(
2507 method => 'bib_extras',
2508 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2511 __PACKAGE__->register_method(
2512 method => 'bib_extras',
2513 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2514 ctype => 'item_form'
2516 __PACKAGE__->register_method(
2517 method => 'bib_extras',
2518 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2519 ctype => 'item_type',
2521 __PACKAGE__->register_method(
2522 method => 'bib_extras',
2523 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2524 ctype => 'bib_level'
2526 __PACKAGE__->register_method(
2527 method => 'bib_extras',
2528 api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2534 $logger->warn("deprecation warning: " .$self->api_name);
2536 my $e = new_editor();
2538 my $ctype = $self->{ctype};
2539 my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2542 for my $ccvm (@$ccvms) {
2543 my $obj = "Fieldmapper::config::${ctype}_map"->new;
2544 $obj->value($ccvm->value);
2545 $obj->code($ccvm->code);
2546 $obj->description($ccvm->description) if $obj->can('description');
2555 __PACKAGE__->register_method(
2556 method => 'fetch_slim_record',
2557 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2559 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2561 { desc => 'Array of Record IDs', type => 'array' }
2564 desc => 'Array of biblio records, event on error'
2569 sub fetch_slim_record {
2570 my( $self, $conn, $ids ) = @_;
2572 my $editor = new_editor();
2575 return $editor->event unless
2576 my $r = $editor->retrieve_biblio_record_entry($_);
2583 __PACKAGE__->register_method(
2584 method => 'rec_hold_parts',
2585 api_name => 'open-ils.search.biblio.record_hold_parts',
2587 Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2591 sub rec_hold_parts {
2592 my( $self, $conn, $args ) = @_;
2594 my $rec = $$args{record};
2595 my $mrec = $$args{metarecord};
2596 my $pickup_lib = $$args{pickup_lib};
2597 my $e = new_editor();
2600 select => {bmp => ['id', 'label']},
2605 select => {'acpm' => ['part']},
2606 from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2608 '+acp' => {'deleted' => 'f'},
2609 '+bre' => {id => $rec}
2616 order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2619 if(defined $pickup_lib) {
2620 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2621 if($hard_boundary) {
2622 my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2623 $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2627 return $e->json_query($query);
2633 __PACKAGE__->register_method(
2634 method => 'rec_to_mr_rec_descriptors',
2635 api_name => 'open-ils.search.metabib.record_to_descriptors',
2637 specialized method...
2638 Given a biblio record id or a metarecord id,
2639 this returns a list of metabib.record_descriptor
2640 objects that live within the same metarecord
2641 @param args Object of args including:
2645 sub rec_to_mr_rec_descriptors {
2646 my( $self, $conn, $args ) = @_;
2648 my $rec = $$args{record};
2649 my $mrec = $$args{metarecord};
2650 my $item_forms = $$args{item_forms};
2651 my $item_types = $$args{item_types};
2652 my $item_lang = $$args{item_lang};
2653 my $pickup_lib = $$args{pickup_lib};
2655 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2657 my $e = new_editor();
2661 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2662 return $e->event unless @$map;
2663 $mrec = $$map[0]->metarecord;
2666 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2667 return $e->event unless @$recs;
2669 my @recs = map { $_->source } @$recs;
2670 my $search = { record => \@recs };
2671 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2672 $search->{item_type} = $item_types if $item_types and @$item_types;
2673 $search->{item_lang} = $item_lang if $item_lang;
2675 my $desc = $e->search_metabib_record_descriptor($search);
2679 select => { 'bre' => ['id'] },
2684 'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2690 '+bre' => { id => \@recs },
2695 "+ccs" => { holdable => 't' },
2696 "+acpl" => { holdable => 't', deleted => 'f' }
2700 if ($hard_boundary) { # 0 (or "top") is the same as no setting
2701 my $orgs = $e->json_query(
2702 { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2703 ) or return $e->die_event;
2705 $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2708 my $good_records = $e->json_query($query) or return $e->die_event;
2711 for my $d (@$desc) {
2712 if ( grep { $d->record == $_->{id} } @$good_records ) {
2719 return { metarecord => $mrec, descriptors => $desc };
2723 __PACKAGE__->register_method(
2724 method => 'fetch_age_protect',
2725 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2728 sub fetch_age_protect {
2729 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2733 __PACKAGE__->register_method(
2734 method => 'copies_by_cn_label',
2735 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2738 __PACKAGE__->register_method(
2739 method => 'copies_by_cn_label',
2740 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2743 sub copies_by_cn_label {
2744 my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2745 my $e = new_editor();
2746 my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2747 my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2748 my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2749 return [] unless @$cns;
2751 # show all non-deleted copies in the staff client ...
2752 if ($self->api_name =~ /staff$/o) {
2753 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2756 # ... otherwise, grab the copies ...
2757 my $copies = $e->search_asset_copy(
2758 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2759 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2763 # ... and test for location and status visibility
2764 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];