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 ];
280 __PACKAGE__->register_method(
281 method => "biblio_search_tcn",
282 api_name => "open-ils.search.biblio.tcn",
285 desc => "Retrieve related record ID(s) given a TCN",
287 { desc => 'TCN', type => 'string' },
288 { desc => 'Flag indicating to include deleted records', type => 'string' }
291 desc => 'Results object like: { "count": $i, "ids": [...] }',
298 sub biblio_search_tcn {
300 my( $self, $client, $tcn, $include_deleted ) = @_;
302 $tcn =~ s/^\s+|\s+$//og;
304 my $e = new_editor();
305 my $search = {tcn_value => $tcn};
306 $search->{deleted} = 'f' unless $include_deleted;
307 my $recs = $e->search_biblio_record_entry( $search, {idlist =>1} );
309 return { count => scalar(@$recs), ids => $recs };
313 # --------------------------------------------------------------------------------
315 __PACKAGE__->register_method(
316 method => "biblio_barcode_to_copy",
317 api_name => "open-ils.search.asset.copy.find_by_barcode",
319 sub biblio_barcode_to_copy {
320 my( $self, $client, $barcode ) = @_;
321 my( $copy, $evt ) = $U->fetch_copy_by_barcode($barcode);
326 __PACKAGE__->register_method(
327 method => "biblio_id_to_copy",
328 api_name => "open-ils.search.asset.copy.batch.retrieve",
330 sub biblio_id_to_copy {
331 my( $self, $client, $ids ) = @_;
332 $logger->info("Fetching copies @$ids");
333 return $U->cstorereq(
334 "open-ils.cstore.direct.asset.copy.search.atomic", { id => $ids } );
338 __PACKAGE__->register_method(
339 method => "biblio_id_to_uris",
340 api_name=> "open-ils.search.asset.uri.retrieve_by_bib",
344 @param BibID Which bib record contains the URIs
345 @param OrgID Where to look for URIs
346 @param OrgDepth Range adjustment for OrgID
347 @return A stream or list of 'auri' objects
351 sub biblio_id_to_uris {
352 my( $self, $client, $bib, $org, $depth ) = @_;
353 die "Org ID required" unless defined($org);
354 die "Bib ID required" unless defined($bib);
357 push @params, $depth if (defined $depth);
359 my $ids = $U->cstorereq( "open-ils.cstore.json_query.atomic",
360 { select => { auri => [ 'id' ] },
364 field => 'call_number',
370 filter => { active => 't' }
381 select => { aou => [ { column => 'id', transform => 'actor.org_unit_descendants', params => \@params, result_field => 'id' } ] },
383 where => { id => $org },
393 my $uris = $U->cstorereq(
394 "open-ils.cstore.direct.asset.uri.search.atomic",
395 { id => [ map { (values %$_) } @$ids ] }
398 $client->respond($_) for (@$uris);
404 __PACKAGE__->register_method(
405 method => "copy_retrieve",
406 api_name => "open-ils.search.asset.copy.retrieve",
409 desc => 'Retrieve a copy object based on the Copy ID',
411 { desc => 'Copy ID', type => 'number'}
414 desc => 'Copy object, event on error'
420 my( $self, $client, $cid ) = @_;
421 my( $copy, $evt ) = $U->fetch_copy($cid);
422 return $evt || $copy;
425 __PACKAGE__->register_method(
426 method => "volume_retrieve",
427 api_name => "open-ils.search.asset.call_number.retrieve"
429 sub volume_retrieve {
430 my( $self, $client, $vid ) = @_;
431 my $e = new_editor();
432 my $vol = $e->retrieve_asset_call_number($vid) or return $e->event;
436 __PACKAGE__->register_method(
437 method => "fleshed_copy_retrieve_batch",
438 api_name => "open-ils.search.asset.copy.fleshed.batch.retrieve",
442 sub fleshed_copy_retrieve_batch {
443 my( $self, $client, $ids ) = @_;
444 $logger->info("Fetching fleshed copies @$ids");
445 return $U->cstorereq(
446 "open-ils.cstore.direct.asset.copy.search.atomic",
449 flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries parts / ] }
454 __PACKAGE__->register_method(
455 method => "fleshed_copy_retrieve",
456 api_name => "open-ils.search.asset.copy.fleshed.retrieve",
459 sub fleshed_copy_retrieve {
460 my( $self, $client, $id ) = @_;
461 my( $c, $e) = $U->fetch_fleshed_copy($id);
466 __PACKAGE__->register_method(
467 method => 'fleshed_by_barcode',
468 api_name => "open-ils.search.asset.copy.fleshed2.find_by_barcode",
471 sub fleshed_by_barcode {
472 my( $self, $conn, $barcode ) = @_;
473 my $e = new_editor();
474 my $copyid = $e->search_asset_copy(
475 {barcode => $barcode, deleted => 'f'}, {idlist=>1})->[0]
477 return fleshed_copy_retrieve2( $self, $conn, $copyid);
481 __PACKAGE__->register_method(
482 method => "fleshed_copy_retrieve2",
483 api_name => "open-ils.search.asset.copy.fleshed2.retrieve",
487 sub fleshed_copy_retrieve2 {
488 my( $self, $client, $id ) = @_;
489 my $e = new_editor();
490 my $copy = $e->retrieve_asset_copy(
497 qw/ location status stat_cat_entry_copy_maps notes age_protect parts peer_record_maps /
499 ascecm => [qw/ stat_cat stat_cat_entry /],
503 ) or return $e->event;
505 # For backwards compatibility
506 #$copy->stat_cat_entries($copy->stat_cat_entry_copy_maps);
508 if( $copy->status->id == OILS_COPY_STATUS_CHECKED_OUT ) {
510 $e->search_action_circulation(
512 { target_copy => $copy->id },
514 order_by => { circ => 'xact_start desc' },
526 __PACKAGE__->register_method(
527 method => 'flesh_copy_custom',
528 api_name => 'open-ils.search.asset.copy.fleshed.custom',
532 sub flesh_copy_custom {
533 my( $self, $conn, $copyid, $fields ) = @_;
534 my $e = new_editor();
535 my $copy = $e->retrieve_asset_copy(
545 ) or return $e->event;
550 __PACKAGE__->register_method(
551 method => "biblio_barcode_to_title",
552 api_name => "open-ils.search.biblio.find_by_barcode",
555 sub biblio_barcode_to_title {
556 my( $self, $client, $barcode ) = @_;
558 my $title = $apputils->simple_scalar_request(
560 "open-ils.storage.biblio.record_entry.retrieve_by_barcode", $barcode );
562 return { ids => [ $title->id ], count => 1 } if $title;
563 return { count => 0 };
566 __PACKAGE__->register_method(
567 method => 'title_id_by_item_barcode',
568 api_name => 'open-ils.search.bib_id.by_barcode',
571 desc => 'Retrieve bib record id associated with the copy identified by the given barcode',
573 { desc => 'Item barcode', type => 'string' }
576 desc => 'Bib record id.'
581 __PACKAGE__->register_method(
582 method => 'title_id_by_item_barcode',
583 api_name => 'open-ils.search.multi_home.bib_ids.by_barcode',
586 desc => 'Retrieve bib record ids associated with the copy identified by the given barcode. This includes peer bibs for Multi-Home items.',
588 { desc => 'Item barcode', type => 'string' }
591 desc => 'Array of bib record ids. First element is the native bib for the item.'
597 sub title_id_by_item_barcode {
598 my( $self, $conn, $barcode ) = @_;
599 my $e = new_editor();
600 my $copies = $e->search_asset_copy(
602 { deleted => 'f', barcode => $barcode },
606 acp => [ 'call_number' ],
613 return $e->event unless @$copies;
615 if( $self->api_name =~ /multi_home/ ) {
616 my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
618 { target_copy => $$copies[0]->id }
621 my @temp = map { $_->peer_record } @{ $multi_home_list };
622 unshift @temp, $$copies[0]->call_number->record->id;
625 return $$copies[0]->call_number->record->id;
629 __PACKAGE__->register_method(
630 method => 'find_peer_bibs',
631 api_name => 'open-ils.search.peer_bibs.test',
634 desc => 'Tests to see if the specified record is a peer record.',
636 { desc => 'Biblio record entry Id', type => 'number' }
639 desc => 'True if specified id can be found in biblio.peer_bib_copy_map.peer_record.',
645 __PACKAGE__->register_method(
646 method => 'find_peer_bibs',
647 api_name => 'open-ils.search.peer_bibs',
650 desc => 'Return acps and mvrs for multi-home items linked to specified peer record.',
652 { desc => 'Biblio record entry Id', type => 'number' }
655 desc => '{ records => Array of mvrs, items => array of acps }',
662 my( $self, $client, $doc_id ) = @_;
663 my $e = new_editor();
665 my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
667 { peer_record => $doc_id },
671 bpbcm => [ 'target_copy', 'peer_type' ],
672 acp => [ 'call_number', 'location', 'status', 'peer_record_maps' ]
678 if ($self->api_name =~ /test/) {
679 return scalar( @{$multi_home_list} ) > 0 ? 1 : 0;
682 if (scalar(@{$multi_home_list})==0) {
686 # create a unique hash of the primary record MVRs for foreign copies
687 # XXX PLEASE let's change to unAPI2 (supports foreign copies) in the TT opac?!?
689 ($_->target_copy->call_number->record, _records_to_mods( $_->target_copy->call_number->record )->[0])
692 # set the foreign_copy_maps field to an empty array
693 map { $rec_hash{$_}->foreign_copy_maps([]) } keys( %rec_hash );
695 # push the maps onto the correct MVRs
696 for (@$multi_home_list) {
698 @{$rec_hash{ $_->target_copy->call_number->record }->foreign_copy_maps()},
703 return [sort {$a->title cmp $b->title} values(%rec_hash)];
706 __PACKAGE__->register_method(
707 method => "biblio_copy_to_mods",
708 api_name => "open-ils.search.biblio.copy.mods.retrieve",
711 # takes a copy object and returns it fleshed mods object
712 sub biblio_copy_to_mods {
713 my( $self, $client, $copy ) = @_;
715 my $volume = $U->cstorereq(
716 "open-ils.cstore.direct.asset.call_number.retrieve",
717 $copy->call_number() );
719 my $mods = _records_to_mods($volume->record());
720 $mods = shift @$mods;
721 $volume->copies([$copy]);
722 push @{$mods->call_numbers()}, $volume;
730 OpenILS::Application::Search::Biblio
736 =head3 open-ils.search.biblio.multiclass.query (arghash, query, docache)
738 For arghash and docache, see B<open-ils.search.biblio.multiclass>.
740 The query argument is a string, but built like a hash with key: value pairs.
741 Recognized search keys include:
743 keyword (kw) - search keyword(s) *
744 author (au) - search author(s) *
745 name (au) - same as author *
746 title (ti) - search title *
747 subject (su) - search subject *
748 series (se) - search series *
749 lang - limit by language (specifiy multiple langs with lang:l1 lang:l2 ...)
750 site - search at specified org unit, corresponds to actor.org_unit.shortname
751 pref_ou - extend search to specified org unit, corresponds to actor.org_unit.shortname
752 sort - sort type (title, author, pubdate)
753 dir - sort direction (asc, desc)
754 available - if set to anything other than "false" or "0", limits to available items
756 * Searching keyword, author, title, subject, and series supports additional search
757 subclasses, specified with a "|". For example, C<title|proper:gone with the wind>.
759 For more, see B<config.metabib_field>.
763 foreach (qw/open-ils.search.biblio.multiclass.query
764 open-ils.search.biblio.multiclass.query.staff
765 open-ils.search.metabib.multiclass.query
766 open-ils.search.metabib.multiclass.query.staff/)
768 __PACKAGE__->register_method(
770 method => 'multiclass_query',
772 desc => 'Perform a search query. The .staff version of the call includes otherwise hidden hits.',
774 {name => 'arghash', desc => 'Arg hash (see open-ils.search.biblio.multiclass)', type => 'object'},
775 {name => 'query', desc => 'Raw human-readable query (see perldoc '. __PACKAGE__ .')', type => 'string'},
776 {name => 'docache', desc => 'Flag for caching (see open-ils.search.biblio.multiclass)', type => 'object'},
779 desc => 'Search results from query, like: { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
780 type => 'object', # TODO: update as miker's new elements are included
786 sub multiclass_query {
787 my($self, $conn, $arghash, $query, $docache) = @_;
789 $logger->debug("initial search query => $query");
790 my $orig_query = $query;
793 $query =~ s/^\s+//go;
795 # convert convenience classes (e.g. kw for keyword) to the full class name
796 # ensure that the convenience class isn't part of a word (e.g. 'playhouse')
797 $query =~ s/(^|\s)kw(:|\|)/$1keyword$2/go;
798 $query =~ s/(^|\s)ti(:|\|)/$1title$2/go;
799 $query =~ s/(^|\s)au(:|\|)/$1author$2/go;
800 $query =~ s/(^|\s)su(:|\|)/$1subject$2/go;
801 $query =~ s/(^|\s)se(:|\|)/$1series$2/go;
802 $query =~ s/(^|\s)name(:|\|)/$1author$2/og;
804 $logger->debug("cleansed query string => $query");
807 my $simple_class_re = qr/((?:\w+(?:\|\w+)?):[^:]+?)$/;
808 my $class_list_re = qr/(?:keyword|title|author|subject|series)/;
809 my $modifier_list_re = qr/(?:site|dir|sort|lang|available|preflib)/;
812 while ($query =~ s/$simple_class_re//so) {
815 my $where = index($qpart,':');
816 my $type = substr($qpart, 0, $where++);
817 my $value = substr($qpart, $where);
819 if ($type !~ /^(?:$class_list_re|$modifier_list_re)/o) {
820 $tmp_value = "$qpart $tmp_value";
824 if ($type =~ /$class_list_re/o ) {
825 $value .= $tmp_value;
829 next unless $type and $value;
831 $value =~ s/^\s*//og;
832 $value =~ s/\s*$//og;
833 $type = 'sort_dir' if $type eq 'dir';
835 if($type eq 'site') {
836 # 'site' is the org shortname. when using this, we also want
837 # to search at the requested org's depth
838 my $e = new_editor();
839 if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
840 $arghash->{org_unit} = $org->id if $org;
841 $arghash->{depth} = $e->retrieve_actor_org_unit_type($org->ou_type)->depth;
843 $logger->warn("'site:' query used on invalid org shortname: $value ... ignoring");
845 } elsif($type eq 'pref_ou') {
846 # 'pref_ou' is the preferred org shortname.
847 my $e = new_editor();
848 if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
849 $arghash->{pref_ou} = $org->id if $org;
851 $logger->warn("'pref_ou:' query used on invalid org shortname: $value ... ignoring");
854 } elsif($type eq 'available') {
856 $arghash->{available} = 1 unless $value eq 'false' or $value eq '0';
858 } elsif($type eq 'lang') {
859 # collect languages into an array of languages
860 $arghash->{language} = [] unless $arghash->{language};
861 push(@{$arghash->{language}}, $value);
863 } elsif($type =~ /^sort/o) {
864 # sort and sort_dir modifiers
865 $arghash->{$type} = $value;
868 # append the search term to the term under construction
869 $search->{$type} = {} unless $search->{$type};
870 $search->{$type}->{term} =
871 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $value" : $value;
875 $query .= " $tmp_value";
876 $query =~ s/\s+/ /go;
877 $query =~ s/^\s+//go;
878 $query =~ s/\s+$//go;
880 my $type = $arghash->{default_class} || 'keyword';
881 $type = ($type eq '-') ? 'keyword' : $type;
882 $type = ($type !~ /^(title|author|keyword|subject|series)(?:\|\w+)?$/o) ? 'keyword' : $type;
885 # This is the front part of the string before any special tokens were
886 # parsed OR colon-separated strings that do not denote a class.
887 # Add this data to the default search class
888 $search->{$type} = {} unless $search->{$type};
889 $search->{$type}->{term} =
890 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $query" : $query;
892 my $real_search = $arghash->{searches} = { $type => { term => $orig_query } };
894 # capture the original limit because the search method alters the limit internally
895 my $ol = $arghash->{limit};
897 my $sclient = OpenSRF::Utils::SettingsClient->new;
899 (my $method = $self->api_name) =~ s/\.query//o;
901 $method =~ s/multiclass/multiclass.staged/
902 if $sclient->config_value(apps => 'open-ils.search',
903 app_settings => 'use_staged_search') =~ /true/i;
905 # XXX This stops the session locale from doing the right thing.
906 # XXX Revisit this and have it translate to a lang instead of a locale.
907 #$arghash->{preferred_language} = $U->get_org_locale($arghash->{org_unit})
908 # unless $arghash->{preferred_language};
910 $method = $self->method_lookup($method);
911 my ($data) = $method->run($arghash, $docache);
913 $arghash->{searches} = $search if (!$data->{complex_query});
915 $arghash->{limit} = $ol if $ol;
916 $data->{compiled_search} = $arghash;
917 $data->{query} = $orig_query;
919 $logger->info("compiled search is " . OpenSRF::Utils::JSON->perl2JSON($arghash));
924 __PACKAGE__->register_method(
925 method => 'cat_search_z_style_wrapper',
926 api_name => 'open-ils.search.biblio.zstyle',
928 signature => q/@see open-ils.search.biblio.multiclass/
931 __PACKAGE__->register_method(
932 method => 'cat_search_z_style_wrapper',
933 api_name => 'open-ils.search.biblio.zstyle.staff',
935 signature => q/@see open-ils.search.biblio.multiclass/
938 sub cat_search_z_style_wrapper {
941 my $authtoken = shift;
944 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
946 my $ou = $cstore->request(
947 'open-ils.cstore.direct.actor.org_unit.search',
948 { parent_ou => undef }
951 my $result = { service => 'native-evergreen-catalog', records => [] };
952 my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
954 $$searchhash{searches}{title}{term} = $$args{search}{title} if $$args{search}{title};
955 $$searchhash{searches}{author}{term} = $$args{search}{author} if $$args{search}{author};
956 $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
957 $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
958 $$searchhash{searches}{'identifier|isbn'}{term} = $$args{search}{isbn} if $$args{search}{isbn};
959 $$searchhash{searches}{'identifier|issn'}{term} = $$args{search}{issn} if $$args{search}{issn};
961 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
962 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
963 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
964 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
966 my $list = the_quest_for_knowledge( $self, $client, $searchhash );
968 if ($list->{count} > 0 and @{$list->{ids}}) {
969 $result->{count} = $list->{count};
971 my $records = $cstore->request(
972 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
973 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
976 for my $rec ( @$records ) {
978 my $u = OpenILS::Utils::ModsParser->new();
979 $u->start_mods_batch( $rec->marc );
980 my $mods = $u->finish_mods_batch();
982 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
988 $cstore->disconnect();
992 # ----------------------------------------------------------------------------
993 # These are the main OPAC search methods
994 # ----------------------------------------------------------------------------
996 __PACKAGE__->register_method(
997 method => 'the_quest_for_knowledge',
998 api_name => 'open-ils.search.biblio.multiclass',
1000 desc => "Performs a multi class biblio or metabib search",
1003 desc => "A search hash with keys: "
1004 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
1005 . "See perldoc " . __PACKAGE__ . " for more detail",
1009 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
1014 desc => 'An object of the form: '
1015 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
1020 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
1022 The search-hash argument can have the following elements:
1024 searches: { "$class" : "$value", ...} [REQUIRED]
1025 org_unit: The org id to focus the search at
1026 depth : The org depth
1027 limit : The search limit default: 10
1028 offset : The search offset default: 0
1029 format : The MARC format
1030 sort : What field to sort the results on? [ author | title | pubdate ]
1031 sort_dir: What direction do we sort? [ asc | desc ]
1032 tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
1033 will be tagged with an additional value ("1") as the last value in the record ID array for
1034 each record. Requires the 'authtoken'
1035 authtoken : Authentication token string; When actions are performed that require a user login
1036 (e.g. tagging circulated records), the authentication token is required
1038 The searches element is required, must have a hashref value, and the hashref must contain at least one
1039 of the following classes as a key:
1047 The value paired with a key is the associated search string.
1049 The docache argument enables/disables searching and saving results in cache (default OFF).
1051 The return object, if successful, will look like:
1053 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
1057 __PACKAGE__->register_method(
1058 method => 'the_quest_for_knowledge',
1059 api_name => 'open-ils.search.biblio.multiclass.staff',
1060 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1062 __PACKAGE__->register_method(
1063 method => 'the_quest_for_knowledge',
1064 api_name => 'open-ils.search.metabib.multiclass',
1065 signature => q/@see open-ils.search.biblio.multiclass/
1067 __PACKAGE__->register_method(
1068 method => 'the_quest_for_knowledge',
1069 api_name => 'open-ils.search.metabib.multiclass.staff',
1070 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1073 sub the_quest_for_knowledge {
1074 my( $self, $conn, $searchhash, $docache ) = @_;
1076 return { count => 0 } unless $searchhash and
1077 ref $searchhash->{searches} eq 'HASH';
1079 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
1083 if($self->api_name =~ /metabib/) {
1085 $method =~ s/biblio/metabib/o;
1088 # do some simple sanity checking
1089 if(!$searchhash->{searches} or
1090 ( !grep { /^(?:title|author|subject|series|keyword|identifier\|is[bs]n)/ } keys %{$searchhash->{searches}} ) ) {
1091 return { count => 0 };
1094 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
1095 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
1096 my $end = $offset + $limit - 1;
1098 my $maxlimit = 5000;
1099 $searchhash->{offset} = 0; # possible user value overwritten in hash
1100 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
1102 return { count => 0 } if $offset > $maxlimit;
1105 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
1106 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
1107 my $ckey = $pfx . md5_hex($method . $s);
1109 $logger->info("bib search for: $s");
1111 $searchhash->{limit} -= $offset;
1115 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1119 $method .= ".staff" if($self->api_name =~ /staff/);
1120 $method .= ".atomic";
1122 for (keys %$searchhash) {
1123 delete $$searchhash{$_}
1124 unless defined $$searchhash{$_};
1127 $result = $U->storagereq( $method, %$searchhash );
1131 $docache = 0; # results came FROM cache, so we don't write back
1134 return {count => 0} unless ($result && $$result[0]);
1138 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1141 # If we didn't get this data from the cache, put it into the cache
1142 # then return the correct offset of records
1143 $logger->debug("putting search cache $ckey\n");
1144 put_cache($ckey, $count, \@recs);
1148 # if we have the full set of data, trim out
1149 # the requested chunk based on limit and offset
1151 for ($offset..$end) {
1152 last unless $recs[$_];
1153 push(@t, $recs[$_]);
1158 return { ids => \@recs, count => $count };
1162 __PACKAGE__->register_method(
1163 method => 'staged_search',
1164 api_name => 'open-ils.search.biblio.multiclass.staged',
1166 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1167 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1170 desc => "A search hash with keys: "
1171 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1172 . "See perldoc " . __PACKAGE__ . " for more detail",
1176 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1181 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1182 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1187 __PACKAGE__->register_method(
1188 method => 'staged_search',
1189 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1190 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1192 __PACKAGE__->register_method(
1193 method => 'staged_search',
1194 api_name => 'open-ils.search.metabib.multiclass.staged',
1195 signature => q/@see open-ils.search.biblio.multiclass.staged/
1197 __PACKAGE__->register_method(
1198 method => 'staged_search',
1199 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1200 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1204 my($self, $conn, $search_hash, $docache) = @_;
1206 my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1208 my $method = $IAmMetabib?
1209 'open-ils.storage.metabib.multiclass.staged.search_fts':
1210 'open-ils.storage.biblio.multiclass.staged.search_fts';
1212 $method .= '.staff' if $self->api_name =~ /staff$/;
1213 $method .= '.atomic';
1215 return {count => 0} unless (
1217 $search_hash->{searches} and
1218 scalar( keys %{$search_hash->{searches}} ));
1220 my $search_duration;
1221 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1222 my $user_limit = $search_hash->{limit} || 10;
1223 my $ignore_facet_classes = $search_hash->{ignore_facet_classes};
1224 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1225 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1228 # we're grabbing results on a per-superpage basis, which means the
1229 # limit and offset should coincide with superpage boundaries
1230 $search_hash->{offset} = 0;
1231 $search_hash->{limit} = $superpage_size;
1233 # force a well-known check_limit
1234 $search_hash->{check_limit} = $superpage_size;
1235 # restrict total tested to superpage size * number of superpages
1236 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1238 # Set the configured estimation strategy, defaults to 'inclusion'.
1239 my $estimation_strategy = OpenSRF::Utils::SettingsClient
1242 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1244 $search_hash->{estimation_strategy} = $estimation_strategy;
1246 # pull any existing results from the cache
1247 my $key = search_cache_key($method, $search_hash);
1248 my $facet_key = $key.'_facets';
1249 my $cache_data = $cache->get_cache($key) || {};
1251 # keep retrieving results until we find enough to
1252 # fulfill the user-specified limit and offset
1253 my $all_results = [];
1254 my $page; # current superpage
1255 my $est_hit_count = 0;
1256 my $current_page_summary = {};
1257 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1258 my $is_real_hit_count = 0;
1261 for($page = 0; $page < $max_superpages; $page++) {
1263 my $data = $cache_data->{$page};
1267 $logger->debug("staged search: analyzing superpage $page");
1270 # this window of results is already cached
1271 $logger->debug("staged search: found cached results");
1272 $summary = $data->{summary};
1273 $results = $data->{results};
1276 # retrieve the window of results from the database
1277 $logger->debug("staged search: fetching results from the database");
1278 $search_hash->{skip_check} = $page * $superpage_size;
1280 $results = $U->storagereq($method, %$search_hash);
1281 $search_duration = time - $start;
1282 $summary = shift(@$results) if $results;
1285 $logger->info("search timed out: duration=$search_duration: params=".
1286 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1287 return {count => 0};
1290 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1292 my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1294 $logger->info("search returned 0 results: duration=$search_duration: params=".
1295 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1298 # Create backwards-compatible result structures
1300 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1302 $results = [map {[$_->{id}]} @$results];
1305 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1306 $results = [grep {defined $_->[0]} @$results];
1307 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1310 tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib)
1311 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1313 $current_page_summary = $summary;
1315 # add the new set of results to the set under construction
1316 push(@$all_results, @$results);
1318 my $current_count = scalar(@$all_results);
1320 $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1323 $logger->debug("staged search: located $current_count, with estimated hits=".
1324 $summary->{estimated_hit_count}." : visible=".$summary->{visible}.", checked=".$summary->{checked});
1326 if (defined($summary->{estimated_hit_count})) {
1327 foreach (qw/ checked visible excluded deleted /) {
1328 $global_summary->{$_} += $summary->{$_};
1330 $global_summary->{total} = $summary->{total};
1333 # we've found all the possible hits
1334 last if $current_count == $summary->{visible}
1335 and not defined $summary->{estimated_hit_count};
1337 # we've found enough results to satisfy the requested limit/offset
1338 last if $current_count >= ($user_limit + $user_offset);
1340 # we've scanned all possible hits
1341 if($summary->{checked} < $superpage_size) {
1342 $est_hit_count = scalar(@$all_results);
1343 # we have all possible results in hand, so we know the final hit count
1344 $is_real_hit_count = 1;
1349 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1351 # refine the estimate if we have more than one superpage
1352 if ($page > 0 and not $is_real_hit_count) {
1353 if ($global_summary->{checked} >= $global_summary->{total}) {
1354 $est_hit_count = $global_summary->{visible};
1356 my $updated_hit_count = $U->storagereq(
1357 'open-ils.storage.fts_paging_estimate',
1358 $global_summary->{checked},
1359 $global_summary->{visible},
1360 $global_summary->{excluded},
1361 $global_summary->{deleted},
1362 $global_summary->{total}
1364 $est_hit_count = $updated_hit_count->{$estimation_strategy};
1368 $conn->respond_complete(
1370 count => $est_hit_count,
1371 core_limit => $search_hash->{core_limit},
1372 superpage_size => $search_hash->{check_limit},
1373 superpage_summary => $current_page_summary,
1374 facet_key => $facet_key,
1379 cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1384 sub tag_circulated_records {
1385 my ($auth, $results, $metabib) = @_;
1386 my $e = new_editor(authtoken => $auth);
1387 return $results unless $e->checkauth;
1390 select => { acn => [{ column => 'record', alias => 'tagme' }] },
1391 from => { acp => 'acn' },
1392 where => { id => { in => { from => ['action.usr_visible_circ_copies', $e->requestor->id] } } },
1398 select => { mmsm => [{ column => 'metarecord', alias => 'tagme' }] },
1400 where => { source => { in => $query } },
1405 # Give me the distinct set of bib records that exist in the user's visible circulation history
1406 my $circ_recs = $e->json_query( $query );
1408 # if the record appears in the circ history, push a 1 onto
1409 # the rec array structure to indicate truthiness
1410 for my $rec (@$results) {
1411 push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1417 # creates a unique token to represent the query in the cache
1418 sub search_cache_key {
1420 my $search_hash = shift;
1422 for my $key (sort keys %$search_hash) {
1423 push(@sorted, ($key => $$search_hash{$key}))
1424 unless $key eq 'limit' or
1426 $key eq 'skip_check';
1428 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1429 return $pfx . md5_hex($method . $s);
1432 sub retrieve_cached_facets {
1438 return undef unless ($key and $key =~ /_facets$/);
1440 my $blob = $cache->get_cache($key) || {};
1444 for my $f ( keys %$blob ) {
1445 my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1446 @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1447 for my $s ( @sorted ) {
1448 my ($k) = keys(%$s);
1449 my ($v) = values(%$s);
1450 $$facets{$f}{$k} = $v;
1460 __PACKAGE__->register_method(
1461 method => "retrieve_cached_facets",
1462 api_name => "open-ils.search.facet_cache.retrieve",
1464 desc => 'Returns facet data derived from a specific search based on a key '.
1465 'generated by open-ils.search.biblio.multiclass.staged and friends.',
1468 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1473 desc => 'Two level hash of facet values. Top level key is the facet id defined on the config.metabib_field table. '.
1474 'Second level key is a string facet value. Datum attached to each facet value is the number of distinct records, '.
1475 'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1476 'facet retrieval. These counts are calculated for all superpages that have been checked for visibility.',
1484 # add facets for this search to the facet cache
1485 my($key, $results, $metabib, $ignore) = @_;
1486 my $data = $cache->get_cache($key);
1489 return undef unless (@$results);
1491 # The query we're constructing
1493 # select mfae.field as id,
1495 # count(distinct mmrsm.appropriate-id-field )
1496 # from metabib.facet_entry mfae
1497 # join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1498 # where mmrsm.appropriate-id-field in IDLIST
1501 my $count_field = $metabib ? 'metarecord' : 'source';
1504 mfae => [ { column => 'field', alias => 'id'}, 'value' ],
1506 transform => 'count',
1508 column => $count_field,
1515 mmrsm => { field => 'source', fkey => 'source' },
1516 cmf => { field => 'id', fkey => 'field' }
1520 '+mmrsm' => { $count_field => $results },
1521 '+cmf' => { facet_field => 't' }
1525 $query->{where}->{'+cmf'}->{field_class} = {'not in' => $ignore}
1526 if ref($ignore) and @$ignore > 0;
1528 my $facets = $U->cstorereq("open-ils.cstore.json_query.atomic", $query);
1530 for my $facet (@$facets) {
1531 next unless ($facet->{value});
1532 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1535 $logger->info("facet compilation: cached with key=$key");
1537 $cache->put_cache($key, $data, $cache_timeout);
1540 sub cache_staged_search_page {
1541 # puts this set of results into the cache
1542 my($key, $page, $summary, $results) = @_;
1543 my $data = $cache->get_cache($key);
1546 summary => $summary,
1550 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1551 $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1553 $cache->put_cache($key, $data, $cache_timeout);
1561 my $start = $offset;
1562 my $end = $offset + $limit - 1;
1564 $logger->debug("searching cache for $key : $start..$end\n");
1566 return undef unless $cache;
1567 my $data = $cache->get_cache($key);
1569 return undef unless $data;
1571 my $count = $data->[0];
1574 return undef unless $offset < $count;
1577 for( my $i = $offset; $i <= $end; $i++ ) {
1578 last unless my $d = $$data[$i];
1579 push( @result, $d );
1582 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1589 my( $key, $count, $data ) = @_;
1590 return undef unless $cache;
1591 $logger->debug("search_cache putting ".
1592 scalar(@$data)." items at key $key with timeout $cache_timeout");
1593 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1597 __PACKAGE__->register_method(
1598 method => "biblio_mrid_to_modsbatch_batch",
1599 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1602 sub biblio_mrid_to_modsbatch_batch {
1603 my( $self, $client, $mrids) = @_;
1604 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1606 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1607 for my $id (@$mrids) {
1608 next unless defined $id;
1609 my ($m) = $method->run($id);
1616 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1617 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1619 __PACKAGE__->register_method(
1620 method => "biblio_mrid_to_modsbatch",
1623 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1624 . "As usual, the .staff version of this method will include otherwise hidden records.",
1626 { desc => 'Metarecord ID', type => 'number' },
1627 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1630 desc => 'MVR Object, event on error',
1636 sub biblio_mrid_to_modsbatch {
1637 my( $self, $client, $mrid, $args) = @_;
1639 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1641 my ($mr, $evt) = _grab_metarecord($mrid);
1642 return $evt unless $mr;
1644 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1645 biblio_mrid_make_modsbatch($self, $client, $mr);
1647 return $mvr unless ref($args);
1649 # Here we find the lead record appropriate for the given filters
1650 # and use that for the title and author of the metarecord
1651 my $format = $$args{format};
1652 my $org = $$args{org};
1653 my $depth = $$args{depth};
1655 return $mvr unless $format or $org or $depth;
1657 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1658 $method = "$method.staff" if $self->api_name =~ /staff/o;
1660 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1662 if( my $mods = $U->record_to_mvr($rec) ) {
1664 $mvr->title( $mods->title );
1665 $mvr->author($mods->author);
1666 $logger->debug("mods_slim updating title and ".
1667 "author in mvr with ".$mods->title." : ".$mods->author);
1673 # converts a metarecord to an mvr
1676 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1677 return Fieldmapper::metabib::virtual_record->new($perl);
1680 # checks to see if a metarecord has mods, if so returns true;
1682 __PACKAGE__->register_method(
1683 method => "biblio_mrid_check_mvr",
1684 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1685 notes => "Takes a metarecord ID or a metarecord object and returns true "
1686 . "if the metarecord already has an mvr associated with it."
1689 sub biblio_mrid_check_mvr {
1690 my( $self, $client, $mrid ) = @_;
1694 if(ref($mrid)) { $mr = $mrid; }
1695 else { ($mr, $evt) = _grab_metarecord($mrid); }
1696 return $evt if $evt;
1698 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1700 return _mr_to_mvr($mr) if $mr->mods();
1704 sub _grab_metarecord {
1706 #my $e = OpenILS::Utils::Editor->new;
1707 my $e = new_editor();
1708 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1713 __PACKAGE__->register_method(
1714 method => "biblio_mrid_make_modsbatch",
1715 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1716 notes => "Takes either a metarecord ID or a metarecord object. "
1717 . "Forces the creations of an mvr for the given metarecord. "
1718 . "The created mvr is returned."
1721 sub biblio_mrid_make_modsbatch {
1722 my( $self, $client, $mrid ) = @_;
1724 #my $e = OpenILS::Utils::Editor->new;
1725 my $e = new_editor();
1732 $mr = $e->retrieve_metabib_metarecord($mrid)
1733 or return $e->event;
1736 my $masterid = $mr->master_record;
1737 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1739 my $ids = $U->storagereq(
1740 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1741 return undef unless @$ids;
1743 my $master = $e->retrieve_biblio_record_entry($masterid)
1744 or return $e->event;
1746 # start the mods batch
1747 my $u = OpenILS::Utils::ModsParser->new();
1748 $u->start_mods_batch( $master->marc );
1750 # grab all of the sub-records and shove them into the batch
1751 my @ids = grep { $_ ne $masterid } @$ids;
1752 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1757 my $r = $e->retrieve_biblio_record_entry($i);
1758 push( @$subrecs, $r ) if $r;
1763 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1764 $u->push_mods_batch( $_->marc ) if $_->marc;
1768 # finish up and send to the client
1769 my $mods = $u->finish_mods_batch();
1770 $mods->doc_id($mrid);
1771 $client->respond_complete($mods);
1774 # now update the mods string in the db
1775 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1778 #$e = OpenILS::Utils::Editor->new(xact => 1);
1779 $e = new_editor(xact => 1);
1780 $e->update_metabib_metarecord($mr)
1781 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1788 # converts a mr id into a list of record ids
1790 foreach (qw/open-ils.search.biblio.metarecord_to_records
1791 open-ils.search.biblio.metarecord_to_records.staff/)
1793 __PACKAGE__->register_method(
1794 method => "biblio_mrid_to_record_ids",
1797 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1798 . "As usual, the .staff version of this method will include otherwise hidden records.",
1800 { desc => 'Metarecord ID', type => 'number' },
1801 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1804 desc => 'Results object like {count => $i, ids =>[...]}',
1812 sub biblio_mrid_to_record_ids {
1813 my( $self, $client, $mrid, $args ) = @_;
1815 my $format = $$args{format};
1816 my $org = $$args{org};
1817 my $depth = $$args{depth};
1819 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1820 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1821 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1823 return { count => scalar(@$recs), ids => $recs };
1827 __PACKAGE__->register_method(
1828 method => "biblio_record_to_marc_html",
1829 api_name => "open-ils.search.biblio.record.html"
1832 __PACKAGE__->register_method(
1833 method => "biblio_record_to_marc_html",
1834 api_name => "open-ils.search.authority.to_html"
1837 # Persistent parsers and setting objects
1838 my $parser = XML::LibXML->new();
1839 my $xslt = XML::LibXSLT->new();
1841 my $slim_marc_sheet;
1842 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1844 sub biblio_record_to_marc_html {
1845 my($self, $client, $recordid, $slim, $marcxml) = @_;
1848 my $dir = $settings_client->config_value("dirs", "xsl");
1851 unless($slim_marc_sheet) {
1852 my $xsl = $settings_client->config_value(
1853 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1855 $xsl = $parser->parse_file("$dir/$xsl");
1856 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1859 $sheet = $slim_marc_sheet;
1863 unless($marc_sheet) {
1864 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1865 my $xsl = $settings_client->config_value(
1866 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1867 $xsl = $parser->parse_file("$dir/$xsl");
1868 $marc_sheet = $xslt->parse_stylesheet($xsl);
1870 $sheet = $marc_sheet;
1875 my $e = new_editor();
1876 if($self->api_name =~ /authority/) {
1877 $record = $e->retrieve_authority_record_entry($recordid)
1878 or return $e->event;
1880 $record = $e->retrieve_biblio_record_entry($recordid)
1881 or return $e->event;
1883 $marcxml = $record->marc;
1886 my $xmldoc = $parser->parse_string($marcxml);
1887 my $html = $sheet->transform($xmldoc);
1888 return $html->documentElement->toString();
1891 __PACKAGE__->register_method(
1892 method => "format_biblio_record_entry",
1893 api_name => "open-ils.search.biblio.record.print",
1895 desc => 'Returns a printable version of the specified bib record',
1897 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1900 desc => q/An action_trigger.event object or error event./,
1905 __PACKAGE__->register_method(
1906 method => "format_biblio_record_entry",
1907 api_name => "open-ils.search.biblio.record.email",
1909 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1911 { desc => 'Authentication token', type => 'string'},
1912 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1915 desc => q/Undefined on success, otherwise an error event./,
1921 sub format_biblio_record_entry {
1922 my($self, $conn, $arg1, $arg2) = @_;
1924 my $for_print = ($self->api_name =~ /print/);
1925 my $for_email = ($self->api_name =~ /email/);
1927 my $e; my $auth; my $bib_id; my $context_org;
1931 $context_org = $arg2 || $U->fetch_org_tree->id;
1932 $e = new_editor(xact => 1);
1933 } elsif ($for_email) {
1936 $e = new_editor(authtoken => $auth, xact => 1);
1937 return $e->die_event unless $e->checkauth;
1938 $context_org = $e->requestor->home_ou;
1942 if (ref $bib_id ne 'ARRAY') {
1943 $bib_ids = [ $bib_id ];
1948 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1949 $bucket->btype('temp');
1950 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1952 $bucket->owner($e->requestor)
1956 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1958 for my $id (@$bib_ids) {
1960 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1962 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1963 $bucket_entry->target_biblio_record_entry($bib);
1964 $bucket_entry->bucket($bucket_obj->id);
1965 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1972 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1974 } elsif ($for_email) {
1976 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1983 __PACKAGE__->register_method(
1984 method => "retrieve_all_copy_statuses",
1985 api_name => "open-ils.search.config.copy_status.retrieve.all"
1988 sub retrieve_all_copy_statuses {
1989 my( $self, $client ) = @_;
1990 return new_editor()->retrieve_all_config_copy_status();
1994 __PACKAGE__->register_method(
1995 method => "copy_counts_per_org",
1996 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1999 __PACKAGE__->register_method(
2000 method => "copy_counts_per_org",
2001 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
2004 sub copy_counts_per_org {
2005 my( $self, $client, $record_id ) = @_;
2007 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
2009 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
2010 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
2012 my $counts = $apputils->simple_scalar_request(
2013 "open-ils.storage", $method, $record_id );
2015 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
2020 __PACKAGE__->register_method(
2021 method => "copy_count_summary",
2022 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
2023 notes => "returns an array of these: "
2024 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2025 . "where statusx is a copy status name. The statuses are sorted by ID.",
2029 sub copy_count_summary {
2030 my( $self, $client, $rid, $org, $depth ) = @_;
2033 my $data = $U->storagereq(
2034 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
2037 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2039 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2043 __PACKAGE__->register_method(
2044 method => "copy_location_count_summary",
2045 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
2046 notes => "returns an array of these: "
2047 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
2048 . "where statusx is a copy status name. The statuses are sorted by ID.",
2051 sub copy_location_count_summary {
2052 my( $self, $client, $rid, $org, $depth ) = @_;
2055 my $data = $U->storagereq(
2056 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2059 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2061 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2063 || $a->[4] cmp $b->[4]
2067 __PACKAGE__->register_method(
2068 method => "copy_count_location_summary",
2069 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2070 notes => "returns an array of these: "
2071 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2072 . "where statusx is a copy status name. The statuses are sorted by ID."
2075 sub copy_count_location_summary {
2076 my( $self, $client, $rid, $org, $depth ) = @_;
2079 my $data = $U->storagereq(
2080 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2082 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2084 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2089 foreach (qw/open-ils.search.biblio.marc
2090 open-ils.search.biblio.marc.staff/)
2092 __PACKAGE__->register_method(
2093 method => "marc_search",
2096 desc => 'Fetch biblio IDs based on MARC record criteria. '
2097 . 'As usual, the .staff version of the search includes otherwise hidden records',
2100 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2101 'See perldoc ' . __PACKAGE__ . ' for more detail.',
2104 {desc => 'limit (optional)', type => 'number'},
2105 {desc => 'offset (optional)', type => 'number'}
2108 desc => 'Results object like: { "count": $i, "ids": [...] }',
2115 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
2117 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
2119 searches: complex query object (required)
2120 org_unit: The org ID to focus the search at
2121 depth : The org depth
2122 limit : integer search limit default: 10
2123 offset : integer search offset default: 0
2124 sort : What field to sort the results on? [ author | title | pubdate ]
2125 sort_dir: In what direction do we sort? [ asc | desc ]
2127 Additional keys to refine search criteria:
2130 language : Language (code)
2131 lit_form : Literary form
2132 item_form: Item form
2133 item_type: Item type
2134 format : The MARC format
2136 Please note that the specific strings to be used in the "addtional keys" will be entirely
2137 dependent on your loaded data.
2139 All keys except "searches" are optional.
2140 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
2142 For example, an arg hash might look like:
2164 The arghash is eventually passed to the SRF call:
2165 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2167 Presently, search uses the cache unconditionally.
2171 # FIXME: that example above isn't actually tested.
2172 # TODO: docache option?
2174 my( $self, $conn, $args, $limit, $offset, $timeout ) = @_;
2176 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2177 $method .= ".staff" if $self->api_name =~ /staff/;
2178 $method .= ".atomic";
2180 $limit ||= 10; # FIXME: what about $args->{limit} ?
2181 $offset ||= 0; # FIXME: what about $args->{offset} ?
2183 # allow caller to pass in a call timeout since MARC searches
2184 # can take longer than the default 60-second timeout.
2185 # Default to 2 mins. Arbitrarily cap at 5 mins.
2186 $timeout = 120 if !$timeout or $timeout > 300;
2189 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2190 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2192 my $recs = search_cache($ckey, $offset, $limit);
2196 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2197 my $req = $ses->request($method, %$args);
2198 my $resp = $req->recv($timeout);
2200 if($resp and $recs = $resp->content) {
2201 put_cache($ckey, scalar(@$recs), $recs);
2202 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2211 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2212 my @recs = map { $_->[0] } @$recs;
2214 return { ids => \@recs, count => $count };
2218 foreach my $isbn_method (qw/
2219 open-ils.search.biblio.isbn
2220 open-ils.search.biblio.isbn.staff
2222 __PACKAGE__->register_method(
2223 method => "biblio_search_isbn",
2224 api_name => $isbn_method,
2226 desc => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2228 {desc => 'ISBN', type => 'string'}
2231 desc => 'Results object like: { "count": $i, "ids": [...] }',
2238 sub biblio_search_isbn {
2239 my( $self, $client, $isbn ) = @_;
2240 $logger->debug("Searching ISBN $isbn");
2241 # the previous implementation of this method was essentially unlimited,
2242 # so we will set our limit very high and let multiclass.query provide any
2244 # XXX: if making this unlimited is deemed important, we might consider
2245 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2246 # which is functionally deprecated at this point, or a custom call to
2247 # 'open-ils.storage.biblio.multiclass.search_fts'
2249 my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2250 if ($self->api_name =~ m/.staff$/) {
2251 $isbn_method .= '.staff';
2254 my $method = $self->method_lookup($isbn_method);
2255 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2256 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2257 return { ids => \@recs, count => $search_result->{'count'} };
2260 __PACKAGE__->register_method(
2261 method => "biblio_search_isbn_batch",
2262 api_name => "open-ils.search.biblio.isbn_list",
2265 # XXX: see biblio_search_isbn() for note concerning 'limit'
2266 sub biblio_search_isbn_batch {
2267 my( $self, $client, $isbn_list ) = @_;
2268 $logger->debug("Searching ISBNs @$isbn_list");
2269 my @recs = (); my %rec_set = ();
2270 my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2271 foreach my $isbn ( @$isbn_list ) {
2272 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2273 my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2274 foreach my $rec (@recs_subset) {
2275 if (! $rec_set{ $rec }) {
2276 $rec_set{ $rec } = 1;
2281 return { ids => \@recs, count => scalar(@recs) };
2284 foreach my $issn_method (qw/
2285 open-ils.search.biblio.issn
2286 open-ils.search.biblio.issn.staff
2288 __PACKAGE__->register_method(
2289 method => "biblio_search_issn",
2290 api_name => $issn_method,
2292 desc => 'Retrieve biblio IDs for a given ISSN',
2294 {desc => 'ISBN', type => 'string'}
2297 desc => 'Results object like: { "count": $i, "ids": [...] }',
2304 sub biblio_search_issn {
2305 my( $self, $client, $issn ) = @_;
2306 $logger->debug("Searching ISSN $issn");
2307 # the previous implementation of this method was essentially unlimited,
2308 # so we will set our limit very high and let multiclass.query provide any
2310 # XXX: if making this unlimited is deemed important, we might consider
2311 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2312 # which is functionally deprecated at this point, or a custom call to
2313 # 'open-ils.storage.biblio.multiclass.search_fts'
2315 my $issn_method = 'open-ils.search.biblio.multiclass.query';
2316 if ($self->api_name =~ m/.staff$/) {
2317 $issn_method .= '.staff';
2320 my $method = $self->method_lookup($issn_method);
2321 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2322 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2323 return { ids => \@recs, count => $search_result->{'count'} };
2327 __PACKAGE__->register_method(
2328 method => "fetch_mods_by_copy",
2329 api_name => "open-ils.search.biblio.mods_from_copy",
2332 desc => 'Retrieve MODS record given an attached copy ID',
2334 { desc => 'Copy ID', type => 'number' }
2337 desc => 'MODS record, event on error or uncataloged item'
2342 sub fetch_mods_by_copy {
2343 my( $self, $client, $copyid ) = @_;
2344 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2345 return $evt if $evt;
2346 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2347 return $apputils->record_to_mvr($record);
2351 # -------------------------------------------------------------------------------------
2353 __PACKAGE__->register_method(
2354 method => "cn_browse",
2355 api_name => "open-ils.search.callnumber.browse.target",
2356 notes => "Starts a callnumber browse"
2359 __PACKAGE__->register_method(
2360 method => "cn_browse",
2361 api_name => "open-ils.search.callnumber.browse.page_up",
2362 notes => "Returns the previous page of callnumbers",
2365 __PACKAGE__->register_method(
2366 method => "cn_browse",
2367 api_name => "open-ils.search.callnumber.browse.page_down",
2368 notes => "Returns the next page of callnumbers",
2372 # RETURNS array of arrays like so: label, owning_lib, record, id
2374 my( $self, $client, @params ) = @_;
2377 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2378 if( $self->api_name =~ /target/ );
2379 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2380 if( $self->api_name =~ /page_up/ );
2381 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2382 if( $self->api_name =~ /page_down/ );
2384 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2386 # -------------------------------------------------------------------------------------
2388 __PACKAGE__->register_method(
2389 method => "fetch_cn",
2390 api_name => "open-ils.search.callnumber.retrieve",
2392 notes => "retrieves a callnumber based on ID",
2396 my( $self, $client, $id ) = @_;
2398 my $e = new_editor();
2399 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2400 return $evt if $evt;
2404 __PACKAGE__->register_method(
2405 method => "fetch_fleshed_cn",
2406 api_name => "open-ils.search.callnumber.fleshed.retrieve",
2408 notes => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2411 sub fetch_fleshed_cn {
2412 my( $self, $client, $id ) = @_;
2414 my $e = new_editor();
2415 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2416 return $evt if $evt;
2421 __PACKAGE__->register_method(
2422 method => "fetch_copy_by_cn",
2423 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2425 Returns an array of copy ID's by callnumber ID
2426 @param cnid The callnumber ID
2427 @return An array of copy IDs
2431 sub fetch_copy_by_cn {
2432 my( $self, $conn, $cnid ) = @_;
2433 return $U->cstorereq(
2434 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2435 { call_number => $cnid, deleted => 'f' } );
2438 __PACKAGE__->register_method(
2439 method => 'fetch_cn_by_info',
2440 api_name => 'open-ils.search.call_number.retrieve_by_info',
2442 @param label The callnumber label
2443 @param record The record the cn is attached to
2444 @param org The owning library of the cn
2445 @return The callnumber object
2450 sub fetch_cn_by_info {
2451 my( $self, $conn, $label, $record, $org ) = @_;
2452 return $U->cstorereq(
2453 'open-ils.cstore.direct.asset.call_number.search',
2454 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2459 __PACKAGE__->register_method(
2460 method => 'bib_extras',
2461 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2464 __PACKAGE__->register_method(
2465 method => 'bib_extras',
2466 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2467 ctype => 'item_form'
2469 __PACKAGE__->register_method(
2470 method => 'bib_extras',
2471 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2472 ctype => 'item_type',
2474 __PACKAGE__->register_method(
2475 method => 'bib_extras',
2476 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2477 ctype => 'bib_level'
2479 __PACKAGE__->register_method(
2480 method => 'bib_extras',
2481 api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2487 $logger->warn("deprecation warning: " .$self->api_name);
2489 my $e = new_editor();
2491 my $ctype = $self->{ctype};
2492 my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2495 for my $ccvm (@$ccvms) {
2496 my $obj = "Fieldmapper::config::${ctype}_map"->new;
2497 $obj->value($ccvm->value);
2498 $obj->code($ccvm->code);
2499 $obj->description($ccvm->description) if $obj->can('description');
2508 __PACKAGE__->register_method(
2509 method => 'fetch_slim_record',
2510 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2512 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2514 { desc => 'Array of Record IDs', type => 'array' }
2517 desc => 'Array of biblio records, event on error'
2522 sub fetch_slim_record {
2523 my( $self, $conn, $ids ) = @_;
2525 #my $editor = OpenILS::Utils::Editor->new;
2526 my $editor = new_editor();
2529 return $editor->event unless
2530 my $r = $editor->retrieve_biblio_record_entry($_);
2537 __PACKAGE__->register_method(
2538 method => 'rec_hold_parts',
2539 api_name => 'open-ils.search.biblio.record_hold_parts',
2541 Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2545 sub rec_hold_parts {
2546 my( $self, $conn, $args ) = @_;
2548 my $rec = $$args{record};
2549 my $mrec = $$args{metarecord};
2550 my $pickup_lib = $$args{pickup_lib};
2551 my $e = new_editor();
2554 select => {bmp => ['id', 'label']},
2559 select => {'acpm' => ['part']},
2560 from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2562 '+acp' => {'deleted' => 'f'},
2563 '+bre' => {id => $rec}
2569 order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2572 if(defined $pickup_lib) {
2573 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2574 if($hard_boundary) {
2575 my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2576 $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2580 return $e->json_query($query);
2586 __PACKAGE__->register_method(
2587 method => 'rec_to_mr_rec_descriptors',
2588 api_name => 'open-ils.search.metabib.record_to_descriptors',
2590 specialized method...
2591 Given a biblio record id or a metarecord id,
2592 this returns a list of metabib.record_descriptor
2593 objects that live within the same metarecord
2594 @param args Object of args including:
2598 sub rec_to_mr_rec_descriptors {
2599 my( $self, $conn, $args ) = @_;
2601 my $rec = $$args{record};
2602 my $mrec = $$args{metarecord};
2603 my $item_forms = $$args{item_forms};
2604 my $item_types = $$args{item_types};
2605 my $item_lang = $$args{item_lang};
2606 my $pickup_lib = $$args{pickup_lib};
2608 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2610 my $e = new_editor();
2614 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2615 return $e->event unless @$map;
2616 $mrec = $$map[0]->metarecord;
2619 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2620 return $e->event unless @$recs;
2622 my @recs = map { $_->source } @$recs;
2623 my $search = { record => \@recs };
2624 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2625 $search->{item_type} = $item_types if $item_types and @$item_types;
2626 $search->{item_lang} = $item_lang if $item_lang;
2628 my $desc = $e->search_metabib_record_descriptor($search);
2632 select => { 'bre' => ['id'] },
2637 'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2643 '+bre' => { id => \@recs },
2648 "+ccs" => { holdable => 't' },
2649 "+acpl" => { holdable => 't' }
2653 if ($hard_boundary) { # 0 (or "top") is the same as no setting
2654 my $orgs = $e->json_query(
2655 { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2656 ) or return $e->die_event;
2658 $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2661 my $good_records = $e->json_query($query) or return $e->die_event;
2664 for my $d (@$desc) {
2665 if ( grep { $d->record == $_->{id} } @$good_records ) {
2672 return { metarecord => $mrec, descriptors => $desc };
2676 __PACKAGE__->register_method(
2677 method => 'fetch_age_protect',
2678 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2681 sub fetch_age_protect {
2682 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2686 __PACKAGE__->register_method(
2687 method => 'copies_by_cn_label',
2688 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2691 __PACKAGE__->register_method(
2692 method => 'copies_by_cn_label',
2693 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2696 sub copies_by_cn_label {
2697 my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2698 my $e = new_editor();
2699 my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2700 my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2701 my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2702 return [] unless @$cns;
2704 # show all non-deleted copies in the staff client ...
2705 if ($self->api_name =~ /staff$/o) {
2706 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2709 # ... otherwise, grab the copies ...
2710 my $copies = $e->search_asset_copy(
2711 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2712 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2716 # ... and test for location and status visibility
2717 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];