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_record_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 sort - sort type (title, author, pubdate)
752 dir - sort direction (asc, desc)
753 available - if set to anything other than "false" or "0", limits to available items
755 * Searching keyword, author, title, subject, and series supports additional search
756 subclasses, specified with a "|". For example, C<title|proper:gone with the wind>.
758 For more, see B<config.metabib_field>.
762 foreach (qw/open-ils.search.biblio.multiclass.query
763 open-ils.search.biblio.multiclass.query.staff
764 open-ils.search.metabib.multiclass.query
765 open-ils.search.metabib.multiclass.query.staff/)
767 __PACKAGE__->register_method(
769 method => 'multiclass_query',
771 desc => 'Perform a search query. The .staff version of the call includes otherwise hidden hits.',
773 {name => 'arghash', desc => 'Arg hash (see open-ils.search.biblio.multiclass)', type => 'object'},
774 {name => 'query', desc => 'Raw human-readable query (see perldoc '. __PACKAGE__ .')', type => 'string'},
775 {name => 'docache', desc => 'Flag for caching (see open-ils.search.biblio.multiclass)', type => 'object'},
778 desc => 'Search results from query, like: { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
779 type => 'object', # TODO: update as miker's new elements are included
785 sub multiclass_query {
786 my($self, $conn, $arghash, $query, $docache) = @_;
788 $logger->debug("initial search query => $query");
789 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)/;
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");
846 } elsif($type eq 'available') {
848 $arghash->{available} = 1 unless $value eq 'false' or $value eq '0';
850 } elsif($type eq 'lang') {
851 # collect languages into an array of languages
852 $arghash->{language} = [] unless $arghash->{language};
853 push(@{$arghash->{language}}, $value);
855 } elsif($type =~ /^sort/o) {
856 # sort and sort_dir modifiers
857 $arghash->{$type} = $value;
860 # append the search term to the term under construction
861 $search->{$type} = {} unless $search->{$type};
862 $search->{$type}->{term} =
863 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $value" : $value;
867 $query .= " $tmp_value";
868 $query =~ s/\s+/ /go;
869 $query =~ s/^\s+//go;
870 $query =~ s/\s+$//go;
872 my $type = $arghash->{default_class} || 'keyword';
873 $type = ($type eq '-') ? 'keyword' : $type;
874 $type = ($type !~ /^(title|author|keyword|subject|series)(?:\|\w+)?$/o) ? 'keyword' : $type;
877 # This is the front part of the string before any special tokens were
878 # parsed OR colon-separated strings that do not denote a class.
879 # Add this data to the default search class
880 $search->{$type} = {} unless $search->{$type};
881 $search->{$type}->{term} =
882 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $query" : $query;
884 my $real_search = $arghash->{searches} = { $type => { term => $orig_query } };
886 # capture the original limit because the search method alters the limit internally
887 my $ol = $arghash->{limit};
889 my $sclient = OpenSRF::Utils::SettingsClient->new;
891 (my $method = $self->api_name) =~ s/\.query//o;
893 $method =~ s/multiclass/multiclass.staged/
894 if $sclient->config_value(apps => 'open-ils.search',
895 app_settings => 'use_staged_search') =~ /true/i;
897 # XXX This stops the session locale from doing the right thing.
898 # XXX Revisit this and have it translate to a lang instead of a locale.
899 #$arghash->{preferred_language} = $U->get_org_locale($arghash->{org_unit})
900 # unless $arghash->{preferred_language};
902 $method = $self->method_lookup($method);
903 my ($data) = $method->run($arghash, $docache);
905 $arghash->{searches} = $search if (!$data->{complex_query});
907 $arghash->{limit} = $ol if $ol;
908 $data->{compiled_search} = $arghash;
909 $data->{query} = $orig_query;
911 $logger->info("compiled search is " . OpenSRF::Utils::JSON->perl2JSON($arghash));
916 __PACKAGE__->register_method(
917 method => 'cat_search_z_style_wrapper',
918 api_name => 'open-ils.search.biblio.zstyle',
920 signature => q/@see open-ils.search.biblio.multiclass/
923 __PACKAGE__->register_method(
924 method => 'cat_search_z_style_wrapper',
925 api_name => 'open-ils.search.biblio.zstyle.staff',
927 signature => q/@see open-ils.search.biblio.multiclass/
930 sub cat_search_z_style_wrapper {
933 my $authtoken = shift;
936 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
938 my $ou = $cstore->request(
939 'open-ils.cstore.direct.actor.org_unit.search',
940 { parent_ou => undef }
943 my $result = { service => 'native-evergreen-catalog', records => [] };
944 my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
946 $$searchhash{searches}{title}{term} = $$args{search}{title} if $$args{search}{title};
947 $$searchhash{searches}{author}{term} = $$args{search}{author} if $$args{search}{author};
948 $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
949 $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
951 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
952 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{isbn} if $$args{search}{isbn};
953 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{issn} if $$args{search}{issn};
954 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
955 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
956 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
958 my $list = the_quest_for_knowledge( $self, $client, $searchhash );
960 if ($list->{count} > 0) {
961 $result->{count} = $list->{count};
963 my $records = $cstore->request(
964 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
965 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
968 for my $rec ( @$records ) {
970 my $u = OpenILS::Utils::ModsParser->new();
971 $u->start_mods_batch( $rec->marc );
972 my $mods = $u->finish_mods_batch();
974 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
980 $cstore->disconnect();
984 # ----------------------------------------------------------------------------
985 # These are the main OPAC search methods
986 # ----------------------------------------------------------------------------
988 __PACKAGE__->register_method(
989 method => 'the_quest_for_knowledge',
990 api_name => 'open-ils.search.biblio.multiclass',
992 desc => "Performs a multi class biblio or metabib search",
995 desc => "A search hash with keys: "
996 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
997 . "See perldoc " . __PACKAGE__ . " for more detail",
1001 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
1006 desc => 'An object of the form: '
1007 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
1012 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
1014 The search-hash argument can have the following elements:
1016 searches: { "$class" : "$value", ...} [REQUIRED]
1017 org_unit: The org id to focus the search at
1018 depth : The org depth
1019 limit : The search limit default: 10
1020 offset : The search offset default: 0
1021 format : The MARC format
1022 sort : What field to sort the results on? [ author | title | pubdate ]
1023 sort_dir: What direction do we sort? [ asc | desc ]
1024 tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
1025 will be tagged with an additional value ("1") as the last value in the record ID array for
1026 each record. Requires the 'authtoken'
1027 authtoken : Authentication token string; When actions are performed that require a user login
1028 (e.g. tagging circulated records), the authentication token is required
1030 The searches element is required, must have a hashref value, and the hashref must contain at least one
1031 of the following classes as a key:
1039 The value paired with a key is the associated search string.
1041 The docache argument enables/disables searching and saving results in cache (default OFF).
1043 The return object, if successful, will look like:
1045 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
1049 __PACKAGE__->register_method(
1050 method => 'the_quest_for_knowledge',
1051 api_name => 'open-ils.search.biblio.multiclass.staff',
1052 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1054 __PACKAGE__->register_method(
1055 method => 'the_quest_for_knowledge',
1056 api_name => 'open-ils.search.metabib.multiclass',
1057 signature => q/@see open-ils.search.biblio.multiclass/
1059 __PACKAGE__->register_method(
1060 method => 'the_quest_for_knowledge',
1061 api_name => 'open-ils.search.metabib.multiclass.staff',
1062 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1065 sub the_quest_for_knowledge {
1066 my( $self, $conn, $searchhash, $docache ) = @_;
1068 return { count => 0 } unless $searchhash and
1069 ref $searchhash->{searches} eq 'HASH';
1071 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
1075 if($self->api_name =~ /metabib/) {
1077 $method =~ s/biblio/metabib/o;
1080 # do some simple sanity checking
1081 if(!$searchhash->{searches} or
1082 ( !grep { /^(?:title|author|subject|series|keyword)/ } keys %{$searchhash->{searches}} ) ) {
1083 return { count => 0 };
1086 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
1087 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
1088 my $end = $offset + $limit - 1;
1090 my $maxlimit = 5000;
1091 $searchhash->{offset} = 0; # possible user value overwritten in hash
1092 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
1094 return { count => 0 } if $offset > $maxlimit;
1097 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
1098 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
1099 my $ckey = $pfx . md5_hex($method . $s);
1101 $logger->info("bib search for: $s");
1103 $searchhash->{limit} -= $offset;
1107 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1111 $method .= ".staff" if($self->api_name =~ /staff/);
1112 $method .= ".atomic";
1114 for (keys %$searchhash) {
1115 delete $$searchhash{$_}
1116 unless defined $$searchhash{$_};
1119 $result = $U->storagereq( $method, %$searchhash );
1123 $docache = 0; # results came FROM cache, so we don't write back
1126 return {count => 0} unless ($result && $$result[0]);
1130 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1133 # If we didn't get this data from the cache, put it into the cache
1134 # then return the correct offset of records
1135 $logger->debug("putting search cache $ckey\n");
1136 put_cache($ckey, $count, \@recs);
1140 # if we have the full set of data, trim out
1141 # the requested chunk based on limit and offset
1143 for ($offset..$end) {
1144 last unless $recs[$_];
1145 push(@t, $recs[$_]);
1150 return { ids => \@recs, count => $count };
1154 __PACKAGE__->register_method(
1155 method => 'staged_search',
1156 api_name => 'open-ils.search.biblio.multiclass.staged',
1158 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1159 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1162 desc => "A search hash with keys: "
1163 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1164 . "See perldoc " . __PACKAGE__ . " for more detail",
1168 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1173 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1174 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1179 __PACKAGE__->register_method(
1180 method => 'staged_search',
1181 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1182 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1184 __PACKAGE__->register_method(
1185 method => 'staged_search',
1186 api_name => 'open-ils.search.metabib.multiclass.staged',
1187 signature => q/@see open-ils.search.biblio.multiclass.staged/
1189 __PACKAGE__->register_method(
1190 method => 'staged_search',
1191 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1192 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1196 my($self, $conn, $search_hash, $docache) = @_;
1198 my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1200 my $method = $IAmMetabib?
1201 'open-ils.storage.metabib.multiclass.staged.search_fts':
1202 'open-ils.storage.biblio.multiclass.staged.search_fts';
1204 $method .= '.staff' if $self->api_name =~ /staff$/;
1205 $method .= '.atomic';
1207 return {count => 0} unless (
1209 $search_hash->{searches} and
1210 scalar( keys %{$search_hash->{searches}} ));
1212 my $search_duration;
1213 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1214 my $user_limit = $search_hash->{limit} || 10;
1215 my $ignore_facet_classes = $search_hash->{ignore_facet_classes};
1216 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1217 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1220 # we're grabbing results on a per-superpage basis, which means the
1221 # limit and offset should coincide with superpage boundaries
1222 $search_hash->{offset} = 0;
1223 $search_hash->{limit} = $superpage_size;
1225 # force a well-known check_limit
1226 $search_hash->{check_limit} = $superpage_size;
1227 # restrict total tested to superpage size * number of superpages
1228 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1230 # Set the configured estimation strategy, defaults to 'inclusion'.
1231 my $estimation_strategy = OpenSRF::Utils::SettingsClient
1234 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1236 $search_hash->{estimation_strategy} = $estimation_strategy;
1238 # pull any existing results from the cache
1239 my $key = search_cache_key($method, $search_hash);
1240 my $facet_key = $key.'_facets';
1241 my $cache_data = $cache->get_cache($key) || {};
1243 # keep retrieving results until we find enough to
1244 # fulfill the user-specified limit and offset
1245 my $all_results = [];
1246 my $page; # current superpage
1247 my $est_hit_count = 0;
1248 my $current_page_summary = {};
1249 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1250 my $is_real_hit_count = 0;
1253 for($page = 0; $page < $max_superpages; $page++) {
1255 my $data = $cache_data->{$page};
1259 $logger->debug("staged search: analyzing superpage $page");
1262 # this window of results is already cached
1263 $logger->debug("staged search: found cached results");
1264 $summary = $data->{summary};
1265 $results = $data->{results};
1268 # retrieve the window of results from the database
1269 $logger->debug("staged search: fetching results from the database");
1270 $search_hash->{skip_check} = $page * $superpage_size;
1272 $results = $U->storagereq($method, %$search_hash);
1273 $search_duration = time - $start;
1274 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1275 $summary = shift(@$results) if $results;
1278 $logger->info("search timed out: duration=$search_duration: params=".
1279 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1280 return {count => 0};
1283 my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1285 $logger->info("search returned 0 results: duration=$search_duration: params=".
1286 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1289 # Create backwards-compatible result structures
1291 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1293 $results = [map {[$_->{id}]} @$results];
1296 tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib)
1297 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1299 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1300 $results = [grep {defined $_->[0]} @$results];
1301 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1304 $current_page_summary = $summary;
1306 # add the new set of results to the set under construction
1307 push(@$all_results, @$results);
1309 my $current_count = scalar(@$all_results);
1311 $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1314 $logger->debug("staged search: located $current_count, with estimated hits=".
1315 $summary->{estimated_hit_count}." : visible=".$summary->{visible}.", checked=".$summary->{checked});
1317 if (defined($summary->{estimated_hit_count})) {
1318 foreach (qw/ checked visible excluded deleted /) {
1319 $global_summary->{$_} += $summary->{$_};
1321 $global_summary->{total} = $summary->{total};
1324 # we've found all the possible hits
1325 last if $current_count == $summary->{visible}
1326 and not defined $summary->{estimated_hit_count};
1328 # we've found enough results to satisfy the requested limit/offset
1329 last if $current_count >= ($user_limit + $user_offset);
1331 # we've scanned all possible hits
1332 if($summary->{checked} < $superpage_size) {
1333 $est_hit_count = scalar(@$all_results);
1334 # we have all possible results in hand, so we know the final hit count
1335 $is_real_hit_count = 1;
1340 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1342 # refine the estimate if we have more than one superpage
1343 if ($page > 0 and not $is_real_hit_count) {
1344 if ($global_summary->{checked} >= $global_summary->{total}) {
1345 $est_hit_count = $global_summary->{visible};
1347 my $updated_hit_count = $U->storagereq(
1348 'open-ils.storage.fts_paging_estimate',
1349 $global_summary->{checked},
1350 $global_summary->{visible},
1351 $global_summary->{excluded},
1352 $global_summary->{deleted},
1353 $global_summary->{total}
1355 $est_hit_count = $updated_hit_count->{$estimation_strategy};
1359 $conn->respond_complete(
1361 count => $est_hit_count,
1362 core_limit => $search_hash->{core_limit},
1363 superpage_size => $search_hash->{check_limit},
1364 superpage_summary => $current_page_summary,
1365 facet_key => $facet_key,
1370 cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1375 sub tag_circulated_records {
1376 my ($auth, $results, $metabib) = @_;
1377 my $e = new_editor(authtoken => $auth);
1378 return $results unless $e->checkauth;
1381 select => { acn => [{ column => 'record', alias => 'tagme' }] },
1382 from => { acp => 'acn' },
1383 where => { id => { in => { from => ['action.usr_visible_circ_copies', $e->requestor->id] } } },
1389 select => { mmsm => [{ column => 'metarecord', alias => 'tagme' }] },
1391 where => { source => { in => $query } },
1396 # Give me the distinct set of bib records that exist in the user's visible circulation history
1397 my $circ_recs = $e->json_query( $query );
1399 # if the record appears in the circ history, push a 1 onto
1400 # the rec array structure to indicate truthiness
1401 for my $rec (@$results) {
1402 push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1408 # creates a unique token to represent the query in the cache
1409 sub search_cache_key {
1411 my $search_hash = shift;
1413 for my $key (sort keys %$search_hash) {
1414 push(@sorted, ($key => $$search_hash{$key}))
1415 unless $key eq 'limit' or
1417 $key eq 'skip_check';
1419 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1420 return $pfx . md5_hex($method . $s);
1423 sub retrieve_cached_facets {
1429 return undef unless ($key and $key =~ /_facets$/);
1431 my $blob = $cache->get_cache($key) || {};
1435 for my $f ( keys %$blob ) {
1436 my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1437 @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1438 for my $s ( @sorted ) {
1439 my ($k) = keys(%$s);
1440 my ($v) = values(%$s);
1441 $$facets{$f}{$k} = $v;
1451 __PACKAGE__->register_method(
1452 method => "retrieve_cached_facets",
1453 api_name => "open-ils.search.facet_cache.retrieve",
1455 desc => 'Returns facet data derived from a specific search based on a key '.
1456 'generated by open-ils.search.biblio.multiclass.staged and friends.',
1459 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1464 desc => 'Two level hash of facet values. Top level key is the facet id defined on the config.metabib_field table. '.
1465 'Second level key is a string facet value. Datum attached to each facet value is the number of distinct records, '.
1466 'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1467 'facet retrieval. These counts are calculated for all superpages that have been checked for visibility.',
1475 # add facets for this search to the facet cache
1476 my($key, $results, $metabib, $ignore) = @_;
1477 my $data = $cache->get_cache($key);
1480 if (!ref($ignore)) {
1481 $ignore = ['identifier']; # ignore the identifier class by default
1484 return undef unless (@$results);
1486 # The query we're constructing
1488 # select mfae.field as id,
1490 # count(distinct mmrsm.appropriate-id-field )
1491 # from metabib.facet_entry mfae
1492 # join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1493 # where mmrsm.appropriate-id-field in IDLIST
1496 my $count_field = $metabib ? 'metarecord' : 'source';
1497 my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1499 mfae => [ { column => 'field', alias => 'id'}, 'value' ],
1501 transform => 'count',
1503 column => $count_field,
1510 mmrsm => { field => 'source', fkey => 'source' },
1511 cmf => { field => 'id', fkey => 'field' }
1515 '+mmrsm' => { $count_field => $results },
1516 '+cmf' => { field_class => { 'not in' => $ignore } }
1521 for my $facet (@$facets) {
1522 next unless ($facet->{value});
1523 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1526 $logger->info("facet compilation: cached with key=$key");
1528 $cache->put_cache($key, $data, $cache_timeout);
1531 sub cache_staged_search_page {
1532 # puts this set of results into the cache
1533 my($key, $page, $summary, $results) = @_;
1534 my $data = $cache->get_cache($key);
1537 summary => $summary,
1541 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1542 $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1544 $cache->put_cache($key, $data, $cache_timeout);
1552 my $start = $offset;
1553 my $end = $offset + $limit - 1;
1555 $logger->debug("searching cache for $key : $start..$end\n");
1557 return undef unless $cache;
1558 my $data = $cache->get_cache($key);
1560 return undef unless $data;
1562 my $count = $data->[0];
1565 return undef unless $offset < $count;
1568 for( my $i = $offset; $i <= $end; $i++ ) {
1569 last unless my $d = $$data[$i];
1570 push( @result, $d );
1573 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1580 my( $key, $count, $data ) = @_;
1581 return undef unless $cache;
1582 $logger->debug("search_cache putting ".
1583 scalar(@$data)." items at key $key with timeout $cache_timeout");
1584 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1588 __PACKAGE__->register_method(
1589 method => "biblio_mrid_to_modsbatch_batch",
1590 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1593 sub biblio_mrid_to_modsbatch_batch {
1594 my( $self, $client, $mrids) = @_;
1595 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1597 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1598 for my $id (@$mrids) {
1599 next unless defined $id;
1600 my ($m) = $method->run($id);
1607 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1608 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1610 __PACKAGE__->register_method(
1611 method => "biblio_mrid_to_modsbatch",
1614 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1615 . "As usual, the .staff version of this method will include otherwise hidden records.",
1617 { desc => 'Metarecord ID', type => 'number' },
1618 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1621 desc => 'MVR Object, event on error',
1627 sub biblio_mrid_to_modsbatch {
1628 my( $self, $client, $mrid, $args) = @_;
1630 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1632 my ($mr, $evt) = _grab_metarecord($mrid);
1633 return $evt unless $mr;
1635 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1636 biblio_mrid_make_modsbatch($self, $client, $mr);
1638 return $mvr unless ref($args);
1640 # Here we find the lead record appropriate for the given filters
1641 # and use that for the title and author of the metarecord
1642 my $format = $$args{format};
1643 my $org = $$args{org};
1644 my $depth = $$args{depth};
1646 return $mvr unless $format or $org or $depth;
1648 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1649 $method = "$method.staff" if $self->api_name =~ /staff/o;
1651 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1653 if( my $mods = $U->record_to_mvr($rec) ) {
1655 $mvr->title( $mods->title );
1656 $mvr->author($mods->author);
1657 $logger->debug("mods_slim updating title and ".
1658 "author in mvr with ".$mods->title." : ".$mods->author);
1664 # converts a metarecord to an mvr
1667 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1668 return Fieldmapper::metabib::virtual_record->new($perl);
1671 # checks to see if a metarecord has mods, if so returns true;
1673 __PACKAGE__->register_method(
1674 method => "biblio_mrid_check_mvr",
1675 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1676 notes => "Takes a metarecord ID or a metarecord object and returns true "
1677 . "if the metarecord already has an mvr associated with it."
1680 sub biblio_mrid_check_mvr {
1681 my( $self, $client, $mrid ) = @_;
1685 if(ref($mrid)) { $mr = $mrid; }
1686 else { ($mr, $evt) = _grab_metarecord($mrid); }
1687 return $evt if $evt;
1689 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1691 return _mr_to_mvr($mr) if $mr->mods();
1695 sub _grab_metarecord {
1697 #my $e = OpenILS::Utils::Editor->new;
1698 my $e = new_editor();
1699 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1704 __PACKAGE__->register_method(
1705 method => "biblio_mrid_make_modsbatch",
1706 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1707 notes => "Takes either a metarecord ID or a metarecord object. "
1708 . "Forces the creations of an mvr for the given metarecord. "
1709 . "The created mvr is returned."
1712 sub biblio_mrid_make_modsbatch {
1713 my( $self, $client, $mrid ) = @_;
1715 #my $e = OpenILS::Utils::Editor->new;
1716 my $e = new_editor();
1723 $mr = $e->retrieve_metabib_metarecord($mrid)
1724 or return $e->event;
1727 my $masterid = $mr->master_record;
1728 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1730 my $ids = $U->storagereq(
1731 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1732 return undef unless @$ids;
1734 my $master = $e->retrieve_biblio_record_entry($masterid)
1735 or return $e->event;
1737 # start the mods batch
1738 my $u = OpenILS::Utils::ModsParser->new();
1739 $u->start_mods_batch( $master->marc );
1741 # grab all of the sub-records and shove them into the batch
1742 my @ids = grep { $_ ne $masterid } @$ids;
1743 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1748 my $r = $e->retrieve_biblio_record_entry($i);
1749 push( @$subrecs, $r ) if $r;
1754 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1755 $u->push_mods_batch( $_->marc ) if $_->marc;
1759 # finish up and send to the client
1760 my $mods = $u->finish_mods_batch();
1761 $mods->doc_id($mrid);
1762 $client->respond_complete($mods);
1765 # now update the mods string in the db
1766 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1769 #$e = OpenILS::Utils::Editor->new(xact => 1);
1770 $e = new_editor(xact => 1);
1771 $e->update_metabib_metarecord($mr)
1772 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1779 # converts a mr id into a list of record ids
1781 foreach (qw/open-ils.search.biblio.metarecord_to_records
1782 open-ils.search.biblio.metarecord_to_records.staff/)
1784 __PACKAGE__->register_method(
1785 method => "biblio_mrid_to_record_ids",
1788 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1789 . "As usual, the .staff version of this method will include otherwise hidden records.",
1791 { desc => 'Metarecord ID', type => 'number' },
1792 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1795 desc => 'Results object like {count => $i, ids =>[...]}',
1803 sub biblio_mrid_to_record_ids {
1804 my( $self, $client, $mrid, $args ) = @_;
1806 my $format = $$args{format};
1807 my $org = $$args{org};
1808 my $depth = $$args{depth};
1810 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1811 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1812 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1814 return { count => scalar(@$recs), ids => $recs };
1818 __PACKAGE__->register_method(
1819 method => "biblio_record_to_marc_html",
1820 api_name => "open-ils.search.biblio.record.html"
1823 __PACKAGE__->register_method(
1824 method => "biblio_record_to_marc_html",
1825 api_name => "open-ils.search.authority.to_html"
1828 # Persistent parsers and setting objects
1829 my $parser = XML::LibXML->new();
1830 my $xslt = XML::LibXSLT->new();
1832 my $slim_marc_sheet;
1833 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1835 sub biblio_record_to_marc_html {
1836 my($self, $client, $recordid, $slim, $marcxml) = @_;
1839 my $dir = $settings_client->config_value("dirs", "xsl");
1842 unless($slim_marc_sheet) {
1843 my $xsl = $settings_client->config_value(
1844 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1846 $xsl = $parser->parse_file("$dir/$xsl");
1847 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1850 $sheet = $slim_marc_sheet;
1854 unless($marc_sheet) {
1855 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1856 my $xsl = $settings_client->config_value(
1857 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1858 $xsl = $parser->parse_file("$dir/$xsl");
1859 $marc_sheet = $xslt->parse_stylesheet($xsl);
1861 $sheet = $marc_sheet;
1866 my $e = new_editor();
1867 if($self->api_name =~ /authority/) {
1868 $record = $e->retrieve_authority_record_entry($recordid)
1869 or return $e->event;
1871 $record = $e->retrieve_biblio_record_entry($recordid)
1872 or return $e->event;
1874 $marcxml = $record->marc;
1877 my $xmldoc = $parser->parse_string($marcxml);
1878 my $html = $sheet->transform($xmldoc);
1879 return $html->documentElement->toString();
1882 __PACKAGE__->register_method(
1883 method => "format_biblio_record_entry",
1884 api_name => "open-ils.search.biblio.record.print",
1886 desc => 'Returns a printable version of the specified bib record',
1888 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1891 desc => q/An action_trigger.event object or error event./,
1896 __PACKAGE__->register_method(
1897 method => "format_biblio_record_entry",
1898 api_name => "open-ils.search.biblio.record.email",
1900 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1902 { desc => 'Authentication token', type => 'string'},
1903 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1906 desc => q/Undefined on success, otherwise an error event./,
1912 sub format_biblio_record_entry {
1913 my($self, $conn, $arg1, $arg2) = @_;
1915 my $for_print = ($self->api_name =~ /print/);
1916 my $for_email = ($self->api_name =~ /email/);
1918 my $e; my $auth; my $bib_id; my $context_org;
1922 $context_org = $arg2 || $U->fetch_org_tree->id;
1923 $e = new_editor(xact => 1);
1924 } elsif ($for_email) {
1927 $e = new_editor(authtoken => $auth, xact => 1);
1928 return $e->die_event unless $e->checkauth;
1929 $context_org = $e->requestor->home_ou;
1933 if (ref $bib_id ne 'ARRAY') {
1934 $bib_ids = [ $bib_id ];
1939 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1940 $bucket->btype('temp');
1941 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1943 $bucket->owner($e->requestor)
1947 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1949 for my $id (@$bib_ids) {
1951 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1953 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1954 $bucket_entry->target_biblio_record_entry($bib);
1955 $bucket_entry->bucket($bucket_obj->id);
1956 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1963 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1965 } elsif ($for_email) {
1967 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1974 __PACKAGE__->register_method(
1975 method => "retrieve_all_copy_statuses",
1976 api_name => "open-ils.search.config.copy_status.retrieve.all"
1979 sub retrieve_all_copy_statuses {
1980 my( $self, $client ) = @_;
1981 return new_editor()->retrieve_all_config_copy_status();
1985 __PACKAGE__->register_method(
1986 method => "copy_counts_per_org",
1987 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1990 __PACKAGE__->register_method(
1991 method => "copy_counts_per_org",
1992 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1995 sub copy_counts_per_org {
1996 my( $self, $client, $record_id ) = @_;
1998 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
2000 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
2001 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
2003 my $counts = $apputils->simple_scalar_request(
2004 "open-ils.storage", $method, $record_id );
2006 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
2011 __PACKAGE__->register_method(
2012 method => "copy_count_summary",
2013 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
2014 notes => "returns an array of these: "
2015 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2016 . "where statusx is a copy status name. The statuses are sorted by ID.",
2020 sub copy_count_summary {
2021 my( $self, $client, $rid, $org, $depth ) = @_;
2024 my $data = $U->storagereq(
2025 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
2028 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2030 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2034 __PACKAGE__->register_method(
2035 method => "copy_location_count_summary",
2036 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
2037 notes => "returns an array of these: "
2038 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
2039 . "where statusx is a copy status name. The statuses are sorted by ID.",
2042 sub copy_location_count_summary {
2043 my( $self, $client, $rid, $org, $depth ) = @_;
2046 my $data = $U->storagereq(
2047 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2050 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2052 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2054 || $a->[4] cmp $b->[4]
2058 __PACKAGE__->register_method(
2059 method => "copy_count_location_summary",
2060 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2061 notes => "returns an array of these: "
2062 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2063 . "where statusx is a copy status name. The statuses are sorted by ID."
2066 sub copy_count_location_summary {
2067 my( $self, $client, $rid, $org, $depth ) = @_;
2070 my $data = $U->storagereq(
2071 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2073 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2075 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2080 foreach (qw/open-ils.search.biblio.marc
2081 open-ils.search.biblio.marc.staff/)
2083 __PACKAGE__->register_method(
2084 method => "marc_search",
2087 desc => 'Fetch biblio IDs based on MARC record criteria. '
2088 . 'As usual, the .staff version of the search includes otherwise hidden records',
2091 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2092 'See perldoc ' . __PACKAGE__ . ' for more detail.',
2095 {desc => 'limit (optional)', type => 'number'},
2096 {desc => 'offset (optional)', type => 'number'}
2099 desc => 'Results object like: { "count": $i, "ids": [...] }',
2106 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
2108 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
2110 searches: complex query object (required)
2111 org_unit: The org ID to focus the search at
2112 depth : The org depth
2113 limit : integer search limit default: 10
2114 offset : integer search offset default: 0
2115 sort : What field to sort the results on? [ author | title | pubdate ]
2116 sort_dir: In what direction do we sort? [ asc | desc ]
2118 Additional keys to refine search criteria:
2121 language : Language (code)
2122 lit_form : Literary form
2123 item_form: Item form
2124 item_type: Item type
2125 format : The MARC format
2127 Please note that the specific strings to be used in the "addtional keys" will be entirely
2128 dependent on your loaded data.
2130 All keys except "searches" are optional.
2131 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
2133 For example, an arg hash might look like:
2155 The arghash is eventually passed to the SRF call:
2156 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2158 Presently, search uses the cache unconditionally.
2162 # FIXME: that example above isn't actually tested.
2163 # TODO: docache option?
2165 my( $self, $conn, $args, $limit, $offset ) = @_;
2167 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2168 $method .= ".staff" if $self->api_name =~ /staff/;
2169 $method .= ".atomic";
2171 $limit ||= 10; # FIXME: what about $args->{limit} ?
2172 $offset ||= 0; # FIXME: what about $args->{offset} ?
2175 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2176 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2178 my $recs = search_cache($ckey, $offset, $limit);
2181 $recs = $U->storagereq($method, %$args) || [];
2183 put_cache($ckey, scalar(@$recs), $recs);
2184 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2191 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2192 my @recs = map { $_->[0] } @$recs;
2194 return { ids => \@recs, count => $count };
2198 foreach my $isbn_method (qw/
2199 open-ils.search.biblio.isbn
2200 open-ils.search.biblio.isbn.staff
2202 __PACKAGE__->register_method(
2203 method => "biblio_search_isbn",
2204 api_name => $isbn_method,
2206 desc => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2208 {desc => 'ISBN', type => 'string'}
2211 desc => 'Results object like: { "count": $i, "ids": [...] }',
2218 sub biblio_search_isbn {
2219 my( $self, $client, $isbn ) = @_;
2220 $logger->debug("Searching ISBN $isbn");
2221 # the previous implementation of this method was essentially unlimited,
2222 # so we will set our limit very high and let multiclass.query provide any
2224 # XXX: if making this unlimited is deemed important, we might consider
2225 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2226 # which is functionally deprecated at this point, or a custom call to
2227 # 'open-ils.storage.biblio.multiclass.search_fts'
2229 my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2230 if ($self->api_name =~ m/.staff$/) {
2231 $isbn_method .= '.staff';
2234 my $method = $self->method_lookup($isbn_method);
2235 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2236 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2237 return { ids => \@recs, count => $search_result->{'count'} };
2240 __PACKAGE__->register_method(
2241 method => "biblio_search_isbn_batch",
2242 api_name => "open-ils.search.biblio.isbn_list",
2245 # XXX: see biblio_search_isbn() for note concerning 'limit'
2246 sub biblio_search_isbn_batch {
2247 my( $self, $client, $isbn_list ) = @_;
2248 $logger->debug("Searching ISBNs @$isbn_list");
2249 my @recs = (); my %rec_set = ();
2250 my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2251 foreach my $isbn ( @$isbn_list ) {
2252 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2253 my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2254 foreach my $rec (@recs_subset) {
2255 if (! $rec_set{ $rec }) {
2256 $rec_set{ $rec } = 1;
2261 return { ids => \@recs, count => scalar(@recs) };
2264 foreach my $issn_method (qw/
2265 open-ils.search.biblio.issn
2266 open-ils.search.biblio.issn.staff
2268 __PACKAGE__->register_method(
2269 method => "biblio_search_issn",
2270 api_name => $issn_method,
2272 desc => 'Retrieve biblio IDs for a given ISSN',
2274 {desc => 'ISBN', type => 'string'}
2277 desc => 'Results object like: { "count": $i, "ids": [...] }',
2284 sub biblio_search_issn {
2285 my( $self, $client, $issn ) = @_;
2286 $logger->debug("Searching ISSN $issn");
2287 # the previous implementation of this method was essentially unlimited,
2288 # so we will set our limit very high and let multiclass.query provide any
2290 # XXX: if making this unlimited is deemed important, we might consider
2291 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2292 # which is functionally deprecated at this point, or a custom call to
2293 # 'open-ils.storage.biblio.multiclass.search_fts'
2295 my $issn_method = 'open-ils.search.biblio.multiclass.query';
2296 if ($self->api_name =~ m/.staff$/) {
2297 $issn_method .= '.staff';
2300 my $method = $self->method_lookup($issn_method);
2301 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2302 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2303 return { ids => \@recs, count => $search_result->{'count'} };
2307 __PACKAGE__->register_method(
2308 method => "fetch_mods_by_copy",
2309 api_name => "open-ils.search.biblio.mods_from_copy",
2312 desc => 'Retrieve MODS record given an attached copy ID',
2314 { desc => 'Copy ID', type => 'number' }
2317 desc => 'MODS record, event on error or uncataloged item'
2322 sub fetch_mods_by_copy {
2323 my( $self, $client, $copyid ) = @_;
2324 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2325 return $evt if $evt;
2326 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2327 return $apputils->record_to_mvr($record);
2331 # -------------------------------------------------------------------------------------
2333 __PACKAGE__->register_method(
2334 method => "cn_browse",
2335 api_name => "open-ils.search.callnumber.browse.target",
2336 notes => "Starts a callnumber browse"
2339 __PACKAGE__->register_method(
2340 method => "cn_browse",
2341 api_name => "open-ils.search.callnumber.browse.page_up",
2342 notes => "Returns the previous page of callnumbers",
2345 __PACKAGE__->register_method(
2346 method => "cn_browse",
2347 api_name => "open-ils.search.callnumber.browse.page_down",
2348 notes => "Returns the next page of callnumbers",
2352 # RETURNS array of arrays like so: label, owning_lib, record, id
2354 my( $self, $client, @params ) = @_;
2357 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2358 if( $self->api_name =~ /target/ );
2359 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2360 if( $self->api_name =~ /page_up/ );
2361 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2362 if( $self->api_name =~ /page_down/ );
2364 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2366 # -------------------------------------------------------------------------------------
2368 __PACKAGE__->register_method(
2369 method => "fetch_cn",
2370 api_name => "open-ils.search.callnumber.retrieve",
2372 notes => "retrieves a callnumber based on ID",
2376 my( $self, $client, $id ) = @_;
2377 my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
2378 return $evt if $evt;
2382 __PACKAGE__->register_method(
2383 method => "fetch_fleshed_cn",
2384 api_name => "open-ils.search.callnumber.fleshed.retrieve",
2386 notes => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2389 sub fetch_fleshed_cn {
2390 my( $self, $client, $id ) = @_;
2391 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1 );
2392 return $evt if $evt;
2397 __PACKAGE__->register_method(
2398 method => "fetch_copy_by_cn",
2399 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2401 Returns an array of copy ID's by callnumber ID
2402 @param cnid The callnumber ID
2403 @return An array of copy IDs
2407 sub fetch_copy_by_cn {
2408 my( $self, $conn, $cnid ) = @_;
2409 return $U->cstorereq(
2410 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2411 { call_number => $cnid, deleted => 'f' } );
2414 __PACKAGE__->register_method(
2415 method => 'fetch_cn_by_info',
2416 api_name => 'open-ils.search.call_number.retrieve_by_info',
2418 @param label The callnumber label
2419 @param record The record the cn is attached to
2420 @param org The owning library of the cn
2421 @return The callnumber object
2426 sub fetch_cn_by_info {
2427 my( $self, $conn, $label, $record, $org ) = @_;
2428 return $U->cstorereq(
2429 'open-ils.cstore.direct.asset.call_number.search',
2430 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2435 __PACKAGE__->register_method(
2436 method => 'bib_extras',
2437 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2440 __PACKAGE__->register_method(
2441 method => 'bib_extras',
2442 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2443 ctype => 'item_form'
2445 __PACKAGE__->register_method(
2446 method => 'bib_extras',
2447 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2448 ctype => 'item_type',
2450 __PACKAGE__->register_method(
2451 method => 'bib_extras',
2452 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2453 ctype => 'bib_level'
2455 __PACKAGE__->register_method(
2456 method => 'bib_extras',
2457 api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2463 $logger->warn("deprecation warning: " .$self->api_name);
2465 my $e = new_editor();
2467 my $ctype = $self->{ctype};
2468 my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2471 for my $ccvm (@$ccvms) {
2472 my $obj = "Fieldmapper::config::${ctype}_map"->new;
2473 $obj->value($ccvm->value);
2474 $obj->code($ccvm->code);
2475 $obj->description($ccvm->description) if $obj->can('description');
2484 __PACKAGE__->register_method(
2485 method => 'fetch_slim_record',
2486 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2488 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2490 { desc => 'Array of Record IDs', type => 'array' }
2493 desc => 'Array of biblio records, event on error'
2498 sub fetch_slim_record {
2499 my( $self, $conn, $ids ) = @_;
2501 #my $editor = OpenILS::Utils::Editor->new;
2502 my $editor = new_editor();
2505 return $editor->event unless
2506 my $r = $editor->retrieve_biblio_record_entry($_);
2513 __PACKAGE__->register_method(
2514 method => 'rec_hold_parts',
2515 api_name => 'open-ils.search.biblio.record_hold_parts',
2517 Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2521 sub rec_hold_parts {
2522 my( $self, $conn, $args ) = @_;
2524 my $rec = $$args{record};
2525 my $mrec = $$args{metarecord};
2526 my $pickup_lib = $$args{pickup_lib};
2527 my $e = new_editor();
2530 select => {bmp => ['id', 'label']},
2535 select => {'acpm' => ['part']},
2536 from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2538 '+acp' => {'deleted' => 'f'},
2539 '+bre' => {id => $rec}
2545 order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2548 if(defined $pickup_lib) {
2549 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2550 if($hard_boundary) {
2551 my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2552 $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2556 return $e->json_query($query);
2562 __PACKAGE__->register_method(
2563 method => 'rec_to_mr_rec_descriptors',
2564 api_name => 'open-ils.search.metabib.record_to_descriptors',
2566 specialized method...
2567 Given a biblio record id or a metarecord id,
2568 this returns a list of metabib.record_descriptor
2569 objects that live within the same metarecord
2570 @param args Object of args including:
2574 sub rec_to_mr_rec_descriptors {
2575 my( $self, $conn, $args ) = @_;
2577 my $rec = $$args{record};
2578 my $mrec = $$args{metarecord};
2579 my $item_forms = $$args{item_forms};
2580 my $item_types = $$args{item_types};
2581 my $item_lang = $$args{item_lang};
2582 my $pickup_lib = $$args{pickup_lib};
2584 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2586 my $e = new_editor();
2590 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2591 return $e->event unless @$map;
2592 $mrec = $$map[0]->metarecord;
2595 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2596 return $e->event unless @$recs;
2598 my @recs = map { $_->source } @$recs;
2599 my $search = { record => \@recs };
2600 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2601 $search->{item_type} = $item_types if $item_types and @$item_types;
2602 $search->{item_lang} = $item_lang if $item_lang;
2604 my $desc = $e->search_metabib_record_descriptor($search);
2606 if ($hard_boundary) { # 0 (or "top") is the same as no setting
2607 my $orgs = $e->json_query(
2608 { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2611 my $good_records = $e->json_query(
2613 select => { 'bre' => ['id'] },
2614 from => { 'bre' => { 'acn' => { 'join' => { 'acp' } } } },
2616 '+bre' => { id => \@recs },
2618 circ_lib => [ map { $_->{id} } @$orgs ],
2626 for my $d (@$desc) {
2627 if ( grep { $d->record == $_->{id} } @$good_records ) {
2635 return { metarecord => $mrec, descriptors => $desc };
2639 __PACKAGE__->register_method(
2640 method => 'fetch_age_protect',
2641 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2644 sub fetch_age_protect {
2645 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2649 __PACKAGE__->register_method(
2650 method => 'copies_by_cn_label',
2651 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2654 __PACKAGE__->register_method(
2655 method => 'copies_by_cn_label',
2656 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2659 sub copies_by_cn_label {
2660 my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2661 my $e = new_editor();
2662 my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2663 my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2664 my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2665 return [] unless @$cns;
2667 # show all non-deleted copies in the staff client ...
2668 if ($self->api_name =~ /staff$/o) {
2669 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2672 # ... otherwise, grab the copies ...
2673 my $copies = $e->search_asset_copy(
2674 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2675 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2679 # ... and test for location and status visibility
2680 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];