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 $method = 'open-ils.search.biblio.multiclass.staged';
1012 $method .= '.staff' if $self->api_name =~ /staff$/;
1014 my ($list) = $self->method_lookup($method)->run( $searchhash );
1016 if ($list->{count} > 0 and @{$list->{ids}}) {
1017 $result->{count} = $list->{count};
1019 my $records = $cstore->request(
1020 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
1021 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
1024 for my $rec ( @$records ) {
1026 my $u = OpenILS::Utils::ModsParser->new();
1027 $u->start_mods_batch( $rec->marc );
1028 my $mods = $u->finish_mods_batch();
1030 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
1036 $cstore->disconnect();
1040 # ----------------------------------------------------------------------------
1041 # These are the main OPAC search methods
1042 # ----------------------------------------------------------------------------
1044 __PACKAGE__->register_method(
1045 method => 'the_quest_for_knowledge',
1046 api_name => 'open-ils.search.biblio.multiclass',
1048 desc => "Performs a multi class biblio or metabib search",
1051 desc => "A search hash with keys: "
1052 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
1053 . "See perldoc " . __PACKAGE__ . " for more detail",
1057 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
1062 desc => 'An object of the form: '
1063 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
1068 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
1070 The search-hash argument can have the following elements:
1072 searches: { "$class" : "$value", ...} [REQUIRED]
1073 org_unit: The org id to focus the search at
1074 depth : The org depth
1075 limit : The search limit default: 10
1076 offset : The search offset default: 0
1077 format : The MARC format
1078 sort : What field to sort the results on? [ author | title | pubdate ]
1079 sort_dir: What direction do we sort? [ asc | desc ]
1080 tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
1081 will be tagged with an additional value ("1") as the last value in the record ID array for
1082 each record. Requires the 'authtoken'
1083 authtoken : Authentication token string; When actions are performed that require a user login
1084 (e.g. tagging circulated records), the authentication token is required
1086 The searches element is required, must have a hashref value, and the hashref must contain at least one
1087 of the following classes as a key:
1095 The value paired with a key is the associated search string.
1097 The docache argument enables/disables searching and saving results in cache (default OFF).
1099 The return object, if successful, will look like:
1101 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
1105 __PACKAGE__->register_method(
1106 method => 'the_quest_for_knowledge',
1107 api_name => 'open-ils.search.biblio.multiclass.staff',
1108 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1110 __PACKAGE__->register_method(
1111 method => 'the_quest_for_knowledge',
1112 api_name => 'open-ils.search.metabib.multiclass',
1113 signature => q/@see open-ils.search.biblio.multiclass/
1115 __PACKAGE__->register_method(
1116 method => 'the_quest_for_knowledge',
1117 api_name => 'open-ils.search.metabib.multiclass.staff',
1118 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1121 sub the_quest_for_knowledge {
1122 my( $self, $conn, $searchhash, $docache ) = @_;
1124 return { count => 0 } unless $searchhash and
1125 ref $searchhash->{searches} eq 'HASH';
1127 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
1131 if($self->api_name =~ /metabib/) {
1133 $method =~ s/biblio/metabib/o;
1136 # do some simple sanity checking
1137 if(!$searchhash->{searches} or
1138 ( !grep { /^(?:title|author|subject|series|keyword|identifier\|is[bs]n)/ } keys %{$searchhash->{searches}} ) ) {
1139 return { count => 0 };
1142 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
1143 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
1144 my $end = $offset + $limit - 1;
1146 my $maxlimit = 5000;
1147 $searchhash->{offset} = 0; # possible user value overwritten in hash
1148 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
1150 return { count => 0 } if $offset > $maxlimit;
1153 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
1154 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
1155 my $ckey = $pfx . md5_hex($method . $s);
1157 $logger->info("bib search for: $s");
1159 $searchhash->{limit} -= $offset;
1163 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1167 $method .= ".staff" if($self->api_name =~ /staff/);
1168 $method .= ".atomic";
1170 for (keys %$searchhash) {
1171 delete $$searchhash{$_}
1172 unless defined $$searchhash{$_};
1175 $result = $U->storagereq( $method, %$searchhash );
1179 $docache = 0; # results came FROM cache, so we don't write back
1182 return {count => 0} unless ($result && $$result[0]);
1186 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1189 # If we didn't get this data from the cache, put it into the cache
1190 # then return the correct offset of records
1191 $logger->debug("putting search cache $ckey\n");
1192 put_cache($ckey, $count, \@recs);
1196 # if we have the full set of data, trim out
1197 # the requested chunk based on limit and offset
1199 for ($offset..$end) {
1200 last unless $recs[$_];
1201 push(@t, $recs[$_]);
1206 return { ids => \@recs, count => $count };
1210 __PACKAGE__->register_method(
1211 method => 'staged_search',
1212 api_name => 'open-ils.search.biblio.multiclass.staged',
1214 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1215 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1218 desc => "A search hash with keys: "
1219 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1220 . "See perldoc " . __PACKAGE__ . " for more detail",
1224 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1229 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1230 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1235 __PACKAGE__->register_method(
1236 method => 'staged_search',
1237 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1238 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1240 __PACKAGE__->register_method(
1241 method => 'staged_search',
1242 api_name => 'open-ils.search.metabib.multiclass.staged',
1243 signature => q/@see open-ils.search.biblio.multiclass.staged/
1245 __PACKAGE__->register_method(
1246 method => 'staged_search',
1247 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1248 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1252 my($self, $conn, $search_hash, $docache) = @_;
1254 my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1256 my $method = $IAmMetabib?
1257 'open-ils.storage.metabib.multiclass.staged.search_fts':
1258 'open-ils.storage.biblio.multiclass.staged.search_fts';
1260 $method .= '.staff' if $self->api_name =~ /staff$/;
1261 $method .= '.atomic';
1263 return {count => 0} unless (
1265 $search_hash->{searches} and
1266 scalar( keys %{$search_hash->{searches}} ));
1268 my $search_duration;
1269 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1270 my $user_limit = $search_hash->{limit} || 10;
1271 my $ignore_facet_classes = $search_hash->{ignore_facet_classes};
1272 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1273 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1276 # we're grabbing results on a per-superpage basis, which means the
1277 # limit and offset should coincide with superpage boundaries
1278 $search_hash->{offset} = 0;
1279 $search_hash->{limit} = $superpage_size;
1281 # force a well-known check_limit
1282 $search_hash->{check_limit} = $superpage_size;
1283 # restrict total tested to superpage size * number of superpages
1284 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1286 # Set the configured estimation strategy, defaults to 'inclusion'.
1287 my $estimation_strategy = OpenSRF::Utils::SettingsClient
1290 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1292 $search_hash->{estimation_strategy} = $estimation_strategy;
1294 # pull any existing results from the cache
1295 my $key = search_cache_key($method, $search_hash);
1296 my $facet_key = $key.'_facets';
1297 my $cache_data = $cache->get_cache($key) || {};
1299 # First, we want to make sure that someone else isn't currently trying to perform exactly
1300 # this same search. The point is to allow just one instance of a search to fill the needs
1301 # of all concurrent, identical searches. This will avoid spammy searches killing the
1302 # database without requiring admins to start locking some IP addresses out entirely.
1304 # There's still a tiny race condition where 2 might run, but without sigificantly more code
1305 # and complexity, this is close to the best we can do.
1307 if ($cache_data->{running}) { # someone is already doing the search...
1308 my $stop_looping = time() + $cache_timeout;
1309 while ( sleep(1) and time() < $stop_looping ) { # sleep for a second ... maybe they'll finish
1310 $cache_data = $cache->get_cache($key) || {};
1311 last if (!$cache_data->{running});
1313 } elsif (!$cache_data->{0}) { # we're the first ... let's give it a try
1314 $cache->put_cache($key, { running => $$ }, $cache_timeout / 3);
1317 # keep retrieving results until we find enough to
1318 # fulfill the user-specified limit and offset
1319 my $all_results = [];
1320 my $page; # current superpage
1321 my $est_hit_count = 0;
1322 my $current_page_summary = {};
1323 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1324 my $is_real_hit_count = 0;
1327 for($page = 0; $page < $max_superpages; $page++) {
1329 my $data = $cache_data->{$page};
1333 $logger->debug("staged search: analyzing superpage $page");
1336 # this window of results is already cached
1337 $logger->debug("staged search: found cached results");
1338 $summary = $data->{summary};
1339 $results = $data->{results};
1342 # retrieve the window of results from the database
1343 $logger->debug("staged search: fetching results from the database");
1344 $search_hash->{skip_check} = $page * $superpage_size;
1346 $results = $U->storagereq($method, %$search_hash);
1347 $search_duration = time - $start;
1348 $summary = shift(@$results) if $results;
1351 $logger->info("search timed out: duration=$search_duration: params=".
1352 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1353 return {count => 0};
1356 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1358 my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1360 $logger->info("search returned 0 results: duration=$search_duration: params=".
1361 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1364 # Create backwards-compatible result structures
1366 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1368 $results = [map {[$_->{id}]} @$results];
1371 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1372 $results = [grep {defined $_->[0]} @$results];
1373 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1376 tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib)
1377 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1379 $current_page_summary = $summary;
1381 # add the new set of results to the set under construction
1382 push(@$all_results, @$results);
1384 my $current_count = scalar(@$all_results);
1386 $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1389 $logger->debug("staged search: located $current_count, with estimated hits=".
1390 ($summary->{estimated_hit_count} || "none") .
1391 " : visible=" . ($summary->{visible} || "none") . ", checked=" .
1392 ($summary->{checked} || "none")
1395 if (defined($summary->{estimated_hit_count})) {
1396 foreach (qw/ checked visible excluded deleted /) {
1397 $global_summary->{$_} += $summary->{$_};
1399 $global_summary->{total} = $summary->{total};
1402 # we've found all the possible hits
1403 last if $current_count == $summary->{visible}
1404 and not defined $summary->{estimated_hit_count};
1406 # we've found enough results to satisfy the requested limit/offset
1407 last if $current_count >= ($user_limit + $user_offset);
1409 # we've scanned all possible hits
1410 if($summary->{checked} < $superpage_size) {
1411 $est_hit_count = scalar(@$all_results);
1412 # we have all possible results in hand, so we know the final hit count
1413 $is_real_hit_count = 1;
1418 # Let other backends grab our data now that we're done.
1419 $cache_data = $cache->get_cache($key);
1420 if ($$cache_data{running} and $$cache_data{running} == $$) {
1421 delete $$cache_data{running};
1422 $cache->put_cache($key, $cache_data, $cache_timeout);
1425 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1427 # refine the estimate if we have more than one superpage
1428 if ($page > 0 and not $is_real_hit_count) {
1429 if ($global_summary->{checked} >= $global_summary->{total}) {
1430 $est_hit_count = $global_summary->{visible};
1432 my $updated_hit_count = $U->storagereq(
1433 'open-ils.storage.fts_paging_estimate',
1434 $global_summary->{checked},
1435 $global_summary->{visible},
1436 $global_summary->{excluded},
1437 $global_summary->{deleted},
1438 $global_summary->{total}
1440 $est_hit_count = $updated_hit_count->{$estimation_strategy};
1444 $conn->respond_complete(
1446 count => $est_hit_count,
1447 core_limit => $search_hash->{core_limit},
1448 superpage_size => $search_hash->{check_limit},
1449 superpage_summary => $current_page_summary,
1450 facet_key => $facet_key,
1455 cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1460 sub tag_circulated_records {
1461 my ($auth, $results, $metabib) = @_;
1462 my $e = new_editor(authtoken => $auth);
1463 return $results unless $e->checkauth;
1466 select => { acn => [{ column => 'record', alias => 'tagme' }] },
1467 from => { acp => 'acn' },
1468 where => { id => { in => { from => ['action.usr_visible_circ_copies', $e->requestor->id] } } },
1474 select => { mmsm => [{ column => 'metarecord', alias => 'tagme' }] },
1476 where => { source => { in => $query } },
1481 # Give me the distinct set of bib records that exist in the user's visible circulation history
1482 my $circ_recs = $e->json_query( $query );
1484 # if the record appears in the circ history, push a 1 onto
1485 # the rec array structure to indicate truthiness
1486 for my $rec (@$results) {
1487 push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1493 # creates a unique token to represent the query in the cache
1494 sub search_cache_key {
1496 my $search_hash = shift;
1498 for my $key (sort keys %$search_hash) {
1499 push(@sorted, ($key => $$search_hash{$key}))
1500 unless $key eq 'limit' or
1502 $key eq 'skip_check';
1504 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1505 return $pfx . md5_hex($method . $s);
1508 sub retrieve_cached_facets {
1514 return undef unless ($key and $key =~ /_facets$/);
1516 my $blob = $cache->get_cache($key) || {};
1520 for my $f ( keys %$blob ) {
1521 my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1522 @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1523 for my $s ( @sorted ) {
1524 my ($k) = keys(%$s);
1525 my ($v) = values(%$s);
1526 $$facets{$f}{$k} = $v;
1536 __PACKAGE__->register_method(
1537 method => "retrieve_cached_facets",
1538 api_name => "open-ils.search.facet_cache.retrieve",
1540 desc => 'Returns facet data derived from a specific search based on a key '.
1541 'generated by open-ils.search.biblio.multiclass.staged and friends.',
1544 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1549 desc => 'Two level hash of facet values. Top level key is the facet id defined on the config.metabib_field table. '.
1550 'Second level key is a string facet value. Datum attached to each facet value is the number of distinct records, '.
1551 'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1552 'facet retrieval. These counts are calculated for all superpages that have been checked for visibility.',
1560 # add facets for this search to the facet cache
1561 my($key, $results, $metabib, $ignore) = @_;
1562 my $data = $cache->get_cache($key);
1565 return undef unless (@$results);
1567 # The query we're constructing
1569 # select mfae.field as id,
1571 # count(distinct mmrsm.appropriate-id-field )
1572 # from metabib.facet_entry mfae
1573 # join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1574 # where mmrsm.appropriate-id-field in IDLIST
1577 my $count_field = $metabib ? 'metarecord' : 'source';
1580 mfae => [ { column => 'field', alias => 'id'}, 'value' ],
1582 transform => 'count',
1584 column => $count_field,
1591 mmrsm => { field => 'source', fkey => 'source' },
1592 cmf => { field => 'id', fkey => 'field' }
1596 '+mmrsm' => { $count_field => $results },
1597 '+cmf' => { facet_field => 't' }
1601 $query->{where}->{'+cmf'}->{field_class} = {'not in' => $ignore}
1602 if ref($ignore) and @$ignore > 0;
1604 my $facets = OpenILS::Utils::CStoreEditor->new->json_query($query, {substream => 1});
1606 for my $facet (@$facets) {
1607 next unless ($facet->{value});
1608 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1611 $logger->info("facet compilation: cached with key=$key");
1613 $cache->put_cache($key, $data, $cache_timeout);
1616 sub cache_staged_search_page {
1617 # puts this set of results into the cache
1618 my($key, $page, $summary, $results) = @_;
1619 my $data = $cache->get_cache($key);
1622 summary => $summary,
1626 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1627 ($summary->{estimated_hit_count} || "none") .
1628 ", visible=" . ($summary->{visible} || "none")
1631 $cache->put_cache($key, $data, $cache_timeout);
1639 my $start = $offset;
1640 my $end = $offset + $limit - 1;
1642 $logger->debug("searching cache for $key : $start..$end\n");
1644 return undef unless $cache;
1645 my $data = $cache->get_cache($key);
1647 return undef unless $data;
1649 my $count = $data->[0];
1652 return undef unless $offset < $count;
1655 for( my $i = $offset; $i <= $end; $i++ ) {
1656 last unless my $d = $$data[$i];
1657 push( @result, $d );
1660 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1667 my( $key, $count, $data ) = @_;
1668 return undef unless $cache;
1669 $logger->debug("search_cache putting ".
1670 scalar(@$data)." items at key $key with timeout $cache_timeout");
1671 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1675 __PACKAGE__->register_method(
1676 method => "biblio_mrid_to_modsbatch_batch",
1677 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1680 sub biblio_mrid_to_modsbatch_batch {
1681 my( $self, $client, $mrids) = @_;
1682 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1684 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1685 for my $id (@$mrids) {
1686 next unless defined $id;
1687 my ($m) = $method->run($id);
1694 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1695 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1697 __PACKAGE__->register_method(
1698 method => "biblio_mrid_to_modsbatch",
1701 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1702 . "As usual, the .staff version of this method will include otherwise hidden records.",
1704 { desc => 'Metarecord ID', type => 'number' },
1705 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1708 desc => 'MVR Object, event on error',
1714 sub biblio_mrid_to_modsbatch {
1715 my( $self, $client, $mrid, $args) = @_;
1717 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1719 my ($mr, $evt) = _grab_metarecord($mrid);
1720 return $evt unless $mr;
1722 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1723 biblio_mrid_make_modsbatch($self, $client, $mr);
1725 return $mvr unless ref($args);
1727 # Here we find the lead record appropriate for the given filters
1728 # and use that for the title and author of the metarecord
1729 my $format = $$args{format};
1730 my $org = $$args{org};
1731 my $depth = $$args{depth};
1733 return $mvr unless $format or $org or $depth;
1735 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1736 $method = "$method.staff" if $self->api_name =~ /staff/o;
1738 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1740 if( my $mods = $U->record_to_mvr($rec) ) {
1742 $mvr->title( $mods->title );
1743 $mvr->author($mods->author);
1744 $logger->debug("mods_slim updating title and ".
1745 "author in mvr with ".$mods->title." : ".$mods->author);
1751 # converts a metarecord to an mvr
1754 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1755 return Fieldmapper::metabib::virtual_record->new($perl);
1758 # checks to see if a metarecord has mods, if so returns true;
1760 __PACKAGE__->register_method(
1761 method => "biblio_mrid_check_mvr",
1762 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1763 notes => "Takes a metarecord ID or a metarecord object and returns true "
1764 . "if the metarecord already has an mvr associated with it."
1767 sub biblio_mrid_check_mvr {
1768 my( $self, $client, $mrid ) = @_;
1772 if(ref($mrid)) { $mr = $mrid; }
1773 else { ($mr, $evt) = _grab_metarecord($mrid); }
1774 return $evt if $evt;
1776 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1778 return _mr_to_mvr($mr) if $mr->mods();
1782 sub _grab_metarecord {
1784 my $e = new_editor();
1785 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1790 __PACKAGE__->register_method(
1791 method => "biblio_mrid_make_modsbatch",
1792 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1793 notes => "Takes either a metarecord ID or a metarecord object. "
1794 . "Forces the creations of an mvr for the given metarecord. "
1795 . "The created mvr is returned."
1798 sub biblio_mrid_make_modsbatch {
1799 my( $self, $client, $mrid ) = @_;
1801 my $e = new_editor();
1808 $mr = $e->retrieve_metabib_metarecord($mrid)
1809 or return $e->event;
1812 my $masterid = $mr->master_record;
1813 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1815 my $ids = $U->storagereq(
1816 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1817 return undef unless @$ids;
1819 my $master = $e->retrieve_biblio_record_entry($masterid)
1820 or return $e->event;
1822 # start the mods batch
1823 my $u = OpenILS::Utils::ModsParser->new();
1824 $u->start_mods_batch( $master->marc );
1826 # grab all of the sub-records and shove them into the batch
1827 my @ids = grep { $_ ne $masterid } @$ids;
1828 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1833 my $r = $e->retrieve_biblio_record_entry($i);
1834 push( @$subrecs, $r ) if $r;
1839 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1840 $u->push_mods_batch( $_->marc ) if $_->marc;
1844 # finish up and send to the client
1845 my $mods = $u->finish_mods_batch();
1846 $mods->doc_id($mrid);
1847 $client->respond_complete($mods);
1850 # now update the mods string in the db
1851 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1854 $e = new_editor(xact => 1);
1855 $e->update_metabib_metarecord($mr)
1856 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1863 # converts a mr id into a list of record ids
1865 foreach (qw/open-ils.search.biblio.metarecord_to_records
1866 open-ils.search.biblio.metarecord_to_records.staff/)
1868 __PACKAGE__->register_method(
1869 method => "biblio_mrid_to_record_ids",
1872 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1873 . "As usual, the .staff version of this method will include otherwise hidden records.",
1875 { desc => 'Metarecord ID', type => 'number' },
1876 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1879 desc => 'Results object like {count => $i, ids =>[...]}',
1887 sub biblio_mrid_to_record_ids {
1888 my( $self, $client, $mrid, $args ) = @_;
1890 my $format = $$args{format};
1891 my $org = $$args{org};
1892 my $depth = $$args{depth};
1894 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1895 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1896 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1898 return { count => scalar(@$recs), ids => $recs };
1902 __PACKAGE__->register_method(
1903 method => "biblio_record_to_marc_html",
1904 api_name => "open-ils.search.biblio.record.html"
1907 __PACKAGE__->register_method(
1908 method => "biblio_record_to_marc_html",
1909 api_name => "open-ils.search.authority.to_html"
1912 # Persistent parsers and setting objects
1913 my $parser = XML::LibXML->new();
1914 my $xslt = XML::LibXSLT->new();
1916 my $slim_marc_sheet;
1917 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1919 sub biblio_record_to_marc_html {
1920 my($self, $client, $recordid, $slim, $marcxml) = @_;
1923 my $dir = $settings_client->config_value("dirs", "xsl");
1926 unless($slim_marc_sheet) {
1927 my $xsl = $settings_client->config_value(
1928 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1930 $xsl = $parser->parse_file("$dir/$xsl");
1931 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1934 $sheet = $slim_marc_sheet;
1938 unless($marc_sheet) {
1939 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1940 my $xsl = $settings_client->config_value(
1941 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1942 $xsl = $parser->parse_file("$dir/$xsl");
1943 $marc_sheet = $xslt->parse_stylesheet($xsl);
1945 $sheet = $marc_sheet;
1950 my $e = new_editor();
1951 if($self->api_name =~ /authority/) {
1952 $record = $e->retrieve_authority_record_entry($recordid)
1953 or return $e->event;
1955 $record = $e->retrieve_biblio_record_entry($recordid)
1956 or return $e->event;
1958 $marcxml = $record->marc;
1961 my $xmldoc = $parser->parse_string($marcxml);
1962 my $html = $sheet->transform($xmldoc);
1963 return $html->documentElement->toString();
1966 __PACKAGE__->register_method(
1967 method => "format_biblio_record_entry",
1968 api_name => "open-ils.search.biblio.record.print",
1970 desc => 'Returns a printable version of the specified bib record',
1972 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1975 desc => q/An action_trigger.event object or error event./,
1980 __PACKAGE__->register_method(
1981 method => "format_biblio_record_entry",
1982 api_name => "open-ils.search.biblio.record.email",
1984 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1986 { desc => 'Authentication token', type => 'string'},
1987 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1990 desc => q/Undefined on success, otherwise an error event./,
1996 sub format_biblio_record_entry {
1997 my($self, $conn, $arg1, $arg2) = @_;
1999 my $for_print = ($self->api_name =~ /print/);
2000 my $for_email = ($self->api_name =~ /email/);
2002 my $e; my $auth; my $bib_id; my $context_org;
2006 $context_org = $arg2 || $U->get_org_tree->id;
2007 $e = new_editor(xact => 1);
2008 } elsif ($for_email) {
2011 $e = new_editor(authtoken => $auth, xact => 1);
2012 return $e->die_event unless $e->checkauth;
2013 $context_org = $e->requestor->home_ou;
2017 if (ref $bib_id ne 'ARRAY') {
2018 $bib_ids = [ $bib_id ];
2023 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
2024 $bucket->btype('temp');
2025 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
2027 $bucket->owner($e->requestor)
2031 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
2033 for my $id (@$bib_ids) {
2035 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
2037 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
2038 $bucket_entry->target_biblio_record_entry($bib);
2039 $bucket_entry->bucket($bucket_obj->id);
2040 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
2047 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
2049 } elsif ($for_email) {
2051 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
2058 __PACKAGE__->register_method(
2059 method => "retrieve_all_copy_statuses",
2060 api_name => "open-ils.search.config.copy_status.retrieve.all"
2063 sub retrieve_all_copy_statuses {
2064 my( $self, $client ) = @_;
2065 return new_editor()->retrieve_all_config_copy_status();
2069 __PACKAGE__->register_method(
2070 method => "copy_counts_per_org",
2071 api_name => "open-ils.search.biblio.copy_counts.retrieve"
2074 __PACKAGE__->register_method(
2075 method => "copy_counts_per_org",
2076 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
2079 sub copy_counts_per_org {
2080 my( $self, $client, $record_id ) = @_;
2082 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
2084 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
2085 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
2087 my $counts = $apputils->simple_scalar_request(
2088 "open-ils.storage", $method, $record_id );
2090 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
2095 __PACKAGE__->register_method(
2096 method => "copy_count_summary",
2097 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
2098 notes => "returns an array of these: "
2099 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2100 . "where statusx is a copy status name. The statuses are sorted by ID.",
2104 sub copy_count_summary {
2105 my( $self, $client, $rid, $org, $depth ) = @_;
2108 my $data = $U->storagereq(
2109 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
2112 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2114 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2118 __PACKAGE__->register_method(
2119 method => "copy_location_count_summary",
2120 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
2121 notes => "returns an array of these: "
2122 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
2123 . "where statusx is a copy status name. The statuses are sorted by ID.",
2126 sub copy_location_count_summary {
2127 my( $self, $client, $rid, $org, $depth ) = @_;
2130 my $data = $U->storagereq(
2131 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2134 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2136 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2138 || $a->[4] cmp $b->[4]
2142 __PACKAGE__->register_method(
2143 method => "copy_count_location_summary",
2144 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2145 notes => "returns an array of these: "
2146 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2147 . "where statusx is a copy status name. The statuses are sorted by ID."
2150 sub copy_count_location_summary {
2151 my( $self, $client, $rid, $org, $depth ) = @_;
2154 my $data = $U->storagereq(
2155 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2157 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2159 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2164 foreach (qw/open-ils.search.biblio.marc
2165 open-ils.search.biblio.marc.staff/)
2167 __PACKAGE__->register_method(
2168 method => "marc_search",
2171 desc => 'Fetch biblio IDs based on MARC record criteria. '
2172 . 'As usual, the .staff version of the search includes otherwise hidden records',
2175 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2176 'See perldoc ' . __PACKAGE__ . ' for more detail.',
2179 {desc => 'limit (optional)', type => 'number'},
2180 {desc => 'offset (optional)', type => 'number'}
2183 desc => 'Results object like: { "count": $i, "ids": [...] }',
2190 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
2192 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
2194 searches: complex query object (required)
2195 org_unit: The org ID to focus the search at
2196 depth : The org depth
2197 limit : integer search limit default: 10
2198 offset : integer search offset default: 0
2199 sort : What field to sort the results on? [ author | title | pubdate ]
2200 sort_dir: In what direction do we sort? [ asc | desc ]
2202 Additional keys to refine search criteria:
2205 language : Language (code)
2206 lit_form : Literary form
2207 item_form: Item form
2208 item_type: Item type
2209 format : The MARC format
2211 Please note that the specific strings to be used in the "addtional keys" will be entirely
2212 dependent on your loaded data.
2214 All keys except "searches" are optional.
2215 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
2217 For example, an arg hash might look like:
2239 The arghash is eventually passed to the SRF call:
2240 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2242 Presently, search uses the cache unconditionally.
2246 # FIXME: that example above isn't actually tested.
2247 # TODO: docache option?
2249 my( $self, $conn, $args, $limit, $offset, $timeout ) = @_;
2251 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2252 $method .= ".staff" if $self->api_name =~ /staff/;
2253 $method .= ".atomic";
2255 $limit ||= 10; # FIXME: what about $args->{limit} ?
2256 $offset ||= 0; # FIXME: what about $args->{offset} ?
2258 # allow caller to pass in a call timeout since MARC searches
2259 # can take longer than the default 60-second timeout.
2260 # Default to 2 mins. Arbitrarily cap at 5 mins.
2261 $timeout = 120 if !$timeout or $timeout > 300;
2264 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2265 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2267 my $recs = search_cache($ckey, $offset, $limit);
2271 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2272 my $req = $ses->request($method, %$args);
2273 my $resp = $req->recv($timeout);
2275 if($resp and $recs = $resp->content) {
2276 put_cache($ckey, scalar(@$recs), $recs);
2277 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2286 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2287 my @recs = map { $_->[0] } @$recs;
2289 return { ids => \@recs, count => $count };
2293 foreach my $isbn_method (qw/
2294 open-ils.search.biblio.isbn
2295 open-ils.search.biblio.isbn.staff
2297 __PACKAGE__->register_method(
2298 method => "biblio_search_isbn",
2299 api_name => $isbn_method,
2301 desc => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2303 {desc => 'ISBN', type => 'string'}
2306 desc => 'Results object like: { "count": $i, "ids": [...] }',
2313 sub biblio_search_isbn {
2314 my( $self, $client, $isbn ) = @_;
2315 $logger->debug("Searching ISBN $isbn");
2316 # the previous implementation of this method was essentially unlimited,
2317 # so we will set our limit very high and let multiclass.query provide any
2319 # XXX: if making this unlimited is deemed important, we might consider
2320 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2321 # which is functionally deprecated at this point, or a custom call to
2322 # 'open-ils.storage.biblio.multiclass.search_fts'
2324 my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2325 if ($self->api_name =~ m/.staff$/) {
2326 $isbn_method .= '.staff';
2329 my $method = $self->method_lookup($isbn_method);
2330 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2331 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2332 return { ids => \@recs, count => $search_result->{'count'} };
2335 __PACKAGE__->register_method(
2336 method => "biblio_search_isbn_batch",
2337 api_name => "open-ils.search.biblio.isbn_list",
2340 # XXX: see biblio_search_isbn() for note concerning 'limit'
2341 sub biblio_search_isbn_batch {
2342 my( $self, $client, $isbn_list ) = @_;
2343 $logger->debug("Searching ISBNs @$isbn_list");
2344 my @recs = (); my %rec_set = ();
2345 my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2346 foreach my $isbn ( @$isbn_list ) {
2347 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2348 my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2349 foreach my $rec (@recs_subset) {
2350 if (! $rec_set{ $rec }) {
2351 $rec_set{ $rec } = 1;
2356 return { ids => \@recs, count => scalar(@recs) };
2359 foreach my $issn_method (qw/
2360 open-ils.search.biblio.issn
2361 open-ils.search.biblio.issn.staff
2363 __PACKAGE__->register_method(
2364 method => "biblio_search_issn",
2365 api_name => $issn_method,
2367 desc => 'Retrieve biblio IDs for a given ISSN',
2369 {desc => 'ISBN', type => 'string'}
2372 desc => 'Results object like: { "count": $i, "ids": [...] }',
2379 sub biblio_search_issn {
2380 my( $self, $client, $issn ) = @_;
2381 $logger->debug("Searching ISSN $issn");
2382 # the previous implementation of this method was essentially unlimited,
2383 # so we will set our limit very high and let multiclass.query provide any
2385 # XXX: if making this unlimited is deemed important, we might consider
2386 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2387 # which is functionally deprecated at this point, or a custom call to
2388 # 'open-ils.storage.biblio.multiclass.search_fts'
2390 my $issn_method = 'open-ils.search.biblio.multiclass.query';
2391 if ($self->api_name =~ m/.staff$/) {
2392 $issn_method .= '.staff';
2395 my $method = $self->method_lookup($issn_method);
2396 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2397 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2398 return { ids => \@recs, count => $search_result->{'count'} };
2402 __PACKAGE__->register_method(
2403 method => "fetch_mods_by_copy",
2404 api_name => "open-ils.search.biblio.mods_from_copy",
2407 desc => 'Retrieve MODS record given an attached copy ID',
2409 { desc => 'Copy ID', type => 'number' }
2412 desc => 'MODS record, event on error or uncataloged item'
2417 sub fetch_mods_by_copy {
2418 my( $self, $client, $copyid ) = @_;
2419 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2420 return $evt if $evt;
2421 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2422 return $apputils->record_to_mvr($record);
2426 # -------------------------------------------------------------------------------------
2428 __PACKAGE__->register_method(
2429 method => "cn_browse",
2430 api_name => "open-ils.search.callnumber.browse.target",
2431 notes => "Starts a callnumber browse"
2434 __PACKAGE__->register_method(
2435 method => "cn_browse",
2436 api_name => "open-ils.search.callnumber.browse.page_up",
2437 notes => "Returns the previous page of callnumbers",
2440 __PACKAGE__->register_method(
2441 method => "cn_browse",
2442 api_name => "open-ils.search.callnumber.browse.page_down",
2443 notes => "Returns the next page of callnumbers",
2447 # RETURNS array of arrays like so: label, owning_lib, record, id
2449 my( $self, $client, @params ) = @_;
2452 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2453 if( $self->api_name =~ /target/ );
2454 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2455 if( $self->api_name =~ /page_up/ );
2456 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2457 if( $self->api_name =~ /page_down/ );
2459 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2461 # -------------------------------------------------------------------------------------
2463 __PACKAGE__->register_method(
2464 method => "fetch_cn",
2465 api_name => "open-ils.search.callnumber.retrieve",
2467 notes => "retrieves a callnumber based on ID",
2471 my( $self, $client, $id ) = @_;
2473 my $e = new_editor();
2474 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2475 return $evt if $evt;
2479 __PACKAGE__->register_method(
2480 method => "fetch_fleshed_cn",
2481 api_name => "open-ils.search.callnumber.fleshed.retrieve",
2483 notes => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2486 sub fetch_fleshed_cn {
2487 my( $self, $client, $id ) = @_;
2489 my $e = new_editor();
2490 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2491 return $evt if $evt;
2496 __PACKAGE__->register_method(
2497 method => "fetch_copy_by_cn",
2498 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2500 Returns an array of copy ID's by callnumber ID
2501 @param cnid The callnumber ID
2502 @return An array of copy IDs
2506 sub fetch_copy_by_cn {
2507 my( $self, $conn, $cnid ) = @_;
2508 return $U->cstorereq(
2509 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2510 { call_number => $cnid, deleted => 'f' } );
2513 __PACKAGE__->register_method(
2514 method => 'fetch_cn_by_info',
2515 api_name => 'open-ils.search.call_number.retrieve_by_info',
2517 @param label The callnumber label
2518 @param record The record the cn is attached to
2519 @param org The owning library of the cn
2520 @return The callnumber object
2525 sub fetch_cn_by_info {
2526 my( $self, $conn, $label, $record, $org ) = @_;
2527 return $U->cstorereq(
2528 'open-ils.cstore.direct.asset.call_number.search',
2529 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2534 __PACKAGE__->register_method(
2535 method => 'bib_extras',
2536 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2539 __PACKAGE__->register_method(
2540 method => 'bib_extras',
2541 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2542 ctype => 'item_form'
2544 __PACKAGE__->register_method(
2545 method => 'bib_extras',
2546 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2547 ctype => 'item_type',
2549 __PACKAGE__->register_method(
2550 method => 'bib_extras',
2551 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2552 ctype => 'bib_level'
2554 __PACKAGE__->register_method(
2555 method => 'bib_extras',
2556 api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2562 $logger->warn("deprecation warning: " .$self->api_name);
2564 my $e = new_editor();
2566 my $ctype = $self->{ctype};
2567 my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2570 for my $ccvm (@$ccvms) {
2571 my $obj = "Fieldmapper::config::${ctype}_map"->new;
2572 $obj->value($ccvm->value);
2573 $obj->code($ccvm->code);
2574 $obj->description($ccvm->description) if $obj->can('description');
2583 __PACKAGE__->register_method(
2584 method => 'fetch_slim_record',
2585 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2587 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2589 { desc => 'Array of Record IDs', type => 'array' }
2592 desc => 'Array of biblio records, event on error'
2597 sub fetch_slim_record {
2598 my( $self, $conn, $ids ) = @_;
2600 my $editor = new_editor();
2603 return $editor->event unless
2604 my $r = $editor->retrieve_biblio_record_entry($_);
2611 __PACKAGE__->register_method(
2612 method => 'rec_hold_parts',
2613 api_name => 'open-ils.search.biblio.record_hold_parts',
2615 Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2619 sub rec_hold_parts {
2620 my( $self, $conn, $args ) = @_;
2622 my $rec = $$args{record};
2623 my $mrec = $$args{metarecord};
2624 my $pickup_lib = $$args{pickup_lib};
2625 my $e = new_editor();
2628 select => {bmp => ['id', 'label']},
2633 select => {'acpm' => ['part']},
2634 from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2636 '+acp' => {'deleted' => 'f'},
2637 '+bre' => {id => $rec}
2643 order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2646 if(defined $pickup_lib) {
2647 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2648 if($hard_boundary) {
2649 my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2650 $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2654 return $e->json_query($query);
2660 __PACKAGE__->register_method(
2661 method => 'rec_to_mr_rec_descriptors',
2662 api_name => 'open-ils.search.metabib.record_to_descriptors',
2664 specialized method...
2665 Given a biblio record id or a metarecord id,
2666 this returns a list of metabib.record_descriptor
2667 objects that live within the same metarecord
2668 @param args Object of args including:
2672 sub rec_to_mr_rec_descriptors {
2673 my( $self, $conn, $args ) = @_;
2675 my $rec = $$args{record};
2676 my $mrec = $$args{metarecord};
2677 my $item_forms = $$args{item_forms};
2678 my $item_types = $$args{item_types};
2679 my $item_lang = $$args{item_lang};
2680 my $pickup_lib = $$args{pickup_lib};
2682 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2684 my $e = new_editor();
2688 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2689 return $e->event unless @$map;
2690 $mrec = $$map[0]->metarecord;
2693 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2694 return $e->event unless @$recs;
2696 my @recs = map { $_->source } @$recs;
2697 my $search = { record => \@recs };
2698 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2699 $search->{item_type} = $item_types if $item_types and @$item_types;
2700 $search->{item_lang} = $item_lang if $item_lang;
2702 my $desc = $e->search_metabib_record_descriptor($search);
2706 select => { 'bre' => ['id'] },
2711 'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2717 '+bre' => { id => \@recs },
2722 "+ccs" => { holdable => 't' },
2723 "+acpl" => { holdable => 't' }
2727 if ($hard_boundary) { # 0 (or "top") is the same as no setting
2728 my $orgs = $e->json_query(
2729 { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2730 ) or return $e->die_event;
2732 $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2735 my $good_records = $e->json_query($query) or return $e->die_event;
2738 for my $d (@$desc) {
2739 if ( grep { $d->record == $_->{id} } @$good_records ) {
2746 return { metarecord => $mrec, descriptors => $desc };
2750 __PACKAGE__->register_method(
2751 method => 'fetch_age_protect',
2752 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2755 sub fetch_age_protect {
2756 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2760 __PACKAGE__->register_method(
2761 method => 'copies_by_cn_label',
2762 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2765 __PACKAGE__->register_method(
2766 method => 'copies_by_cn_label',
2767 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2770 sub copies_by_cn_label {
2771 my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2772 my $e = new_editor();
2773 my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2774 my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2775 my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2776 return [] unless @$cns;
2778 # show all non-deleted copies in the staff client ...
2779 if ($self->api_name =~ /staff$/o) {
2780 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2783 # ... otherwise, grab the copies ...
2784 my $copies = $e->search_asset_copy(
2785 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2786 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2790 # ... and test for location and status visibility
2791 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];