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 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;
792 $query =~ s/^\s+//go;
794 # convert convenience classes (e.g. kw for keyword) to the full class name
795 # ensure that the convenience class isn't part of a word (e.g. 'playhouse')
796 $query =~ s/(^|\s)kw(:|\|)/$1keyword$2/go;
797 $query =~ s/(^|\s)ti(:|\|)/$1title$2/go;
798 $query =~ s/(^|\s)au(:|\|)/$1author$2/go;
799 $query =~ s/(^|\s)su(:|\|)/$1subject$2/go;
800 $query =~ s/(^|\s)se(:|\|)/$1series$2/go;
801 $query =~ s/(^|\s)name(:|\|)/$1author$2/og;
803 $logger->debug("cleansed query string => $query");
806 my $simple_class_re = qr/((?:\w+(?:\|\w+)?):[^:]+?)$/;
807 my $class_list_re = qr/(?:keyword|title|author|subject|series)/;
808 my $modifier_list_re = qr/(?:site|dir|sort|lang|available)/;
811 while ($query =~ s/$simple_class_re//so) {
814 my $where = index($qpart,':');
815 my $type = substr($qpart, 0, $where++);
816 my $value = substr($qpart, $where);
818 if ($type !~ /^(?:$class_list_re|$modifier_list_re)/o) {
819 $tmp_value = "$qpart $tmp_value";
823 if ($type =~ /$class_list_re/o ) {
824 $value .= $tmp_value;
828 next unless $type and $value;
830 $value =~ s/^\s*//og;
831 $value =~ s/\s*$//og;
832 $type = 'sort_dir' if $type eq 'dir';
834 if($type eq 'site') {
835 # 'site' is the org shortname. when using this, we also want
836 # to search at the requested org's depth
837 my $e = new_editor();
838 if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
839 $arghash->{org_unit} = $org->id if $org;
840 $arghash->{depth} = $e->retrieve_actor_org_unit_type($org->ou_type)->depth;
842 $logger->warn("'site:' query used on invalid org shortname: $value ... ignoring");
845 } elsif($type eq 'available') {
847 $arghash->{available} = 1 unless $value eq 'false' or $value eq '0';
849 } elsif($type eq 'lang') {
850 # collect languages into an array of languages
851 $arghash->{language} = [] unless $arghash->{language};
852 push(@{$arghash->{language}}, $value);
854 } elsif($type =~ /^sort/o) {
855 # sort and sort_dir modifiers
856 $arghash->{$type} = $value;
859 # append the search term to the term under construction
860 $search->{$type} = {} unless $search->{$type};
861 $search->{$type}->{term} =
862 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $value" : $value;
866 $query .= " $tmp_value";
867 $query =~ s/\s+/ /go;
868 $query =~ s/^\s+//go;
869 $query =~ s/\s+$//go;
871 my $type = $arghash->{default_class} || 'keyword';
872 $type = ($type eq '-') ? 'keyword' : $type;
873 $type = ($type !~ /^(title|author|keyword|subject|series)(?:\|\w+)?$/o) ? 'keyword' : $type;
876 # This is the front part of the string before any special tokens were
877 # parsed OR colon-separated strings that do not denote a class.
878 # Add this data to the default search class
879 $search->{$type} = {} unless $search->{$type};
880 $search->{$type}->{term} =
881 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $query" : $query;
883 my $real_search = $arghash->{searches} = { $type => { term => $orig_query } };
885 # capture the original limit because the search method alters the limit internally
886 my $ol = $arghash->{limit};
888 my $sclient = OpenSRF::Utils::SettingsClient->new;
890 (my $method = $self->api_name) =~ s/\.query//o;
892 $method =~ s/multiclass/multiclass.staged/
893 if $sclient->config_value(apps => 'open-ils.search',
894 app_settings => 'use_staged_search') =~ /true/i;
896 # XXX This stops the session locale from doing the right thing.
897 # XXX Revisit this and have it translate to a lang instead of a locale.
898 #$arghash->{preferred_language} = $U->get_org_locale($arghash->{org_unit})
899 # unless $arghash->{preferred_language};
901 $method = $self->method_lookup($method);
902 my ($data) = $method->run($arghash, $docache);
904 $arghash->{searches} = $search if (!$data->{complex_query});
906 $arghash->{limit} = $ol if $ol;
907 $data->{compiled_search} = $arghash;
908 $data->{query} = $orig_query;
910 $logger->info("compiled search is " . OpenSRF::Utils::JSON->perl2JSON($arghash));
915 __PACKAGE__->register_method(
916 method => 'cat_search_z_style_wrapper',
917 api_name => 'open-ils.search.biblio.zstyle',
919 signature => q/@see open-ils.search.biblio.multiclass/
922 __PACKAGE__->register_method(
923 method => 'cat_search_z_style_wrapper',
924 api_name => 'open-ils.search.biblio.zstyle.staff',
926 signature => q/@see open-ils.search.biblio.multiclass/
929 sub cat_search_z_style_wrapper {
932 my $authtoken = shift;
935 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
937 my $ou = $cstore->request(
938 'open-ils.cstore.direct.actor.org_unit.search',
939 { parent_ou => undef }
942 my $result = { service => 'native-evergreen-catalog', records => [] };
943 my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
945 $$searchhash{searches}{title}{term} = $$args{search}{title} if $$args{search}{title};
946 $$searchhash{searches}{author}{term} = $$args{search}{author} if $$args{search}{author};
947 $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
948 $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
949 $$searchhash{searches}{'identifier|isbn'}{term} = $$args{search}{isbn} if $$args{search}{isbn};
950 $$searchhash{searches}{'identifier|issn'}{term} = $$args{search}{issn} if $$args{search}{issn};
952 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
953 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
954 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
955 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
957 my $list = the_quest_for_knowledge( $self, $client, $searchhash );
959 if ($list->{count} > 0 and @{$list->{ids}}) {
960 $result->{count} = $list->{count};
962 my $records = $cstore->request(
963 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
964 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
967 for my $rec ( @$records ) {
969 my $u = OpenILS::Utils::ModsParser->new();
970 $u->start_mods_batch( $rec->marc );
971 my $mods = $u->finish_mods_batch();
973 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
979 $cstore->disconnect();
983 # ----------------------------------------------------------------------------
984 # These are the main OPAC search methods
985 # ----------------------------------------------------------------------------
987 __PACKAGE__->register_method(
988 method => 'the_quest_for_knowledge',
989 api_name => 'open-ils.search.biblio.multiclass',
991 desc => "Performs a multi class biblio or metabib search",
994 desc => "A search hash with keys: "
995 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
996 . "See perldoc " . __PACKAGE__ . " for more detail",
1000 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
1005 desc => 'An object of the form: '
1006 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
1011 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
1013 The search-hash argument can have the following elements:
1015 searches: { "$class" : "$value", ...} [REQUIRED]
1016 org_unit: The org id to focus the search at
1017 depth : The org depth
1018 limit : The search limit default: 10
1019 offset : The search offset default: 0
1020 format : The MARC format
1021 sort : What field to sort the results on? [ author | title | pubdate ]
1022 sort_dir: What direction do we sort? [ asc | desc ]
1023 tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
1024 will be tagged with an additional value ("1") as the last value in the record ID array for
1025 each record. Requires the 'authtoken'
1026 authtoken : Authentication token string; When actions are performed that require a user login
1027 (e.g. tagging circulated records), the authentication token is required
1029 The searches element is required, must have a hashref value, and the hashref must contain at least one
1030 of the following classes as a key:
1038 The value paired with a key is the associated search string.
1040 The docache argument enables/disables searching and saving results in cache (default OFF).
1042 The return object, if successful, will look like:
1044 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
1048 __PACKAGE__->register_method(
1049 method => 'the_quest_for_knowledge',
1050 api_name => 'open-ils.search.biblio.multiclass.staff',
1051 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1053 __PACKAGE__->register_method(
1054 method => 'the_quest_for_knowledge',
1055 api_name => 'open-ils.search.metabib.multiclass',
1056 signature => q/@see open-ils.search.biblio.multiclass/
1058 __PACKAGE__->register_method(
1059 method => 'the_quest_for_knowledge',
1060 api_name => 'open-ils.search.metabib.multiclass.staff',
1061 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1064 sub the_quest_for_knowledge {
1065 my( $self, $conn, $searchhash, $docache ) = @_;
1067 return { count => 0 } unless $searchhash and
1068 ref $searchhash->{searches} eq 'HASH';
1070 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
1074 if($self->api_name =~ /metabib/) {
1076 $method =~ s/biblio/metabib/o;
1079 # do some simple sanity checking
1080 if(!$searchhash->{searches} or
1081 ( !grep { /^(?:title|author|subject|series|keyword|identifier\|is[bs]n)/ } keys %{$searchhash->{searches}} ) ) {
1082 return { count => 0 };
1085 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
1086 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
1087 my $end = $offset + $limit - 1;
1089 my $maxlimit = 5000;
1090 $searchhash->{offset} = 0; # possible user value overwritten in hash
1091 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
1093 return { count => 0 } if $offset > $maxlimit;
1096 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
1097 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
1098 my $ckey = $pfx . md5_hex($method . $s);
1100 $logger->info("bib search for: $s");
1102 $searchhash->{limit} -= $offset;
1106 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1110 $method .= ".staff" if($self->api_name =~ /staff/);
1111 $method .= ".atomic";
1113 for (keys %$searchhash) {
1114 delete $$searchhash{$_}
1115 unless defined $$searchhash{$_};
1118 $result = $U->storagereq( $method, %$searchhash );
1122 $docache = 0; # results came FROM cache, so we don't write back
1125 return {count => 0} unless ($result && $$result[0]);
1129 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1132 # If we didn't get this data from the cache, put it into the cache
1133 # then return the correct offset of records
1134 $logger->debug("putting search cache $ckey\n");
1135 put_cache($ckey, $count, \@recs);
1139 # if we have the full set of data, trim out
1140 # the requested chunk based on limit and offset
1142 for ($offset..$end) {
1143 last unless $recs[$_];
1144 push(@t, $recs[$_]);
1149 return { ids => \@recs, count => $count };
1153 __PACKAGE__->register_method(
1154 method => 'staged_search',
1155 api_name => 'open-ils.search.biblio.multiclass.staged',
1157 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1158 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1161 desc => "A search hash with keys: "
1162 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1163 . "See perldoc " . __PACKAGE__ . " for more detail",
1167 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1172 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1173 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1178 __PACKAGE__->register_method(
1179 method => 'staged_search',
1180 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1181 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1183 __PACKAGE__->register_method(
1184 method => 'staged_search',
1185 api_name => 'open-ils.search.metabib.multiclass.staged',
1186 signature => q/@see open-ils.search.biblio.multiclass.staged/
1188 __PACKAGE__->register_method(
1189 method => 'staged_search',
1190 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1191 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1195 my($self, $conn, $search_hash, $docache) = @_;
1197 my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1199 my $method = $IAmMetabib?
1200 'open-ils.storage.metabib.multiclass.staged.search_fts':
1201 'open-ils.storage.biblio.multiclass.staged.search_fts';
1203 $method .= '.staff' if $self->api_name =~ /staff$/;
1204 $method .= '.atomic';
1206 return {count => 0} unless (
1208 $search_hash->{searches} and
1209 scalar( keys %{$search_hash->{searches}} ));
1211 my $search_duration;
1212 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1213 my $user_limit = $search_hash->{limit} || 10;
1214 my $ignore_facet_classes = $search_hash->{ignore_facet_classes};
1215 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1216 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1219 # we're grabbing results on a per-superpage basis, which means the
1220 # limit and offset should coincide with superpage boundaries
1221 $search_hash->{offset} = 0;
1222 $search_hash->{limit} = $superpage_size;
1224 # force a well-known check_limit
1225 $search_hash->{check_limit} = $superpage_size;
1226 # restrict total tested to superpage size * number of superpages
1227 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1229 # Set the configured estimation strategy, defaults to 'inclusion'.
1230 my $estimation_strategy = OpenSRF::Utils::SettingsClient
1233 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1235 $search_hash->{estimation_strategy} = $estimation_strategy;
1237 # pull any existing results from the cache
1238 my $key = search_cache_key($method, $search_hash);
1239 my $facet_key = $key.'_facets';
1240 my $cache_data = $cache->get_cache($key) || {};
1242 # keep retrieving results until we find enough to
1243 # fulfill the user-specified limit and offset
1244 my $all_results = [];
1245 my $page; # current superpage
1246 my $est_hit_count = 0;
1247 my $current_page_summary = {};
1248 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1249 my $is_real_hit_count = 0;
1252 for($page = 0; $page < $max_superpages; $page++) {
1254 my $data = $cache_data->{$page};
1258 $logger->debug("staged search: analyzing superpage $page");
1261 # this window of results is already cached
1262 $logger->debug("staged search: found cached results");
1263 $summary = $data->{summary};
1264 $results = $data->{results};
1267 # retrieve the window of results from the database
1268 $logger->debug("staged search: fetching results from the database");
1269 $search_hash->{skip_check} = $page * $superpage_size;
1271 $results = $U->storagereq($method, %$search_hash);
1272 $search_duration = time - $start;
1273 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1274 $summary = shift(@$results) if $results;
1277 $logger->info("search timed out: duration=$search_duration: params=".
1278 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1279 return {count => 0};
1282 my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1284 $logger->info("search returned 0 results: duration=$search_duration: params=".
1285 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1288 # Create backwards-compatible result structures
1290 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1292 $results = [map {[$_->{id}]} @$results];
1295 tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib)
1296 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1298 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1299 $results = [grep {defined $_->[0]} @$results];
1300 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1303 $current_page_summary = $summary;
1305 # add the new set of results to the set under construction
1306 push(@$all_results, @$results);
1308 my $current_count = scalar(@$all_results);
1310 $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1313 $logger->debug("staged search: located $current_count, with estimated hits=".
1314 $summary->{estimated_hit_count}." : visible=".$summary->{visible}.", checked=".$summary->{checked});
1316 if (defined($summary->{estimated_hit_count})) {
1317 foreach (qw/ checked visible excluded deleted /) {
1318 $global_summary->{$_} += $summary->{$_};
1320 $global_summary->{total} = $summary->{total};
1323 # we've found all the possible hits
1324 last if $current_count == $summary->{visible}
1325 and not defined $summary->{estimated_hit_count};
1327 # we've found enough results to satisfy the requested limit/offset
1328 last if $current_count >= ($user_limit + $user_offset);
1330 # we've scanned all possible hits
1331 if($summary->{checked} < $superpage_size) {
1332 $est_hit_count = scalar(@$all_results);
1333 # we have all possible results in hand, so we know the final hit count
1334 $is_real_hit_count = 1;
1339 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1341 # refine the estimate if we have more than one superpage
1342 if ($page > 0 and not $is_real_hit_count) {
1343 if ($global_summary->{checked} >= $global_summary->{total}) {
1344 $est_hit_count = $global_summary->{visible};
1346 my $updated_hit_count = $U->storagereq(
1347 'open-ils.storage.fts_paging_estimate',
1348 $global_summary->{checked},
1349 $global_summary->{visible},
1350 $global_summary->{excluded},
1351 $global_summary->{deleted},
1352 $global_summary->{total}
1354 $est_hit_count = $updated_hit_count->{$estimation_strategy};
1358 $conn->respond_complete(
1360 count => $est_hit_count,
1361 core_limit => $search_hash->{core_limit},
1362 superpage_size => $search_hash->{check_limit},
1363 superpage_summary => $current_page_summary,
1364 facet_key => $facet_key,
1369 cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1374 sub tag_circulated_records {
1375 my ($auth, $results, $metabib) = @_;
1376 my $e = new_editor(authtoken => $auth);
1377 return $results unless $e->checkauth;
1380 select => { acn => [{ column => 'record', alias => 'tagme' }] },
1381 from => { acp => 'acn' },
1382 where => { id => { in => { from => ['action.usr_visible_circ_copies', $e->requestor->id] } } },
1388 select => { mmsm => [{ column => 'metarecord', alias => 'tagme' }] },
1390 where => { source => { in => $query } },
1395 # Give me the distinct set of bib records that exist in the user's visible circulation history
1396 my $circ_recs = $e->json_query( $query );
1398 # if the record appears in the circ history, push a 1 onto
1399 # the rec array structure to indicate truthiness
1400 for my $rec (@$results) {
1401 push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1407 # creates a unique token to represent the query in the cache
1408 sub search_cache_key {
1410 my $search_hash = shift;
1412 for my $key (sort keys %$search_hash) {
1413 push(@sorted, ($key => $$search_hash{$key}))
1414 unless $key eq 'limit' or
1416 $key eq 'skip_check';
1418 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1419 return $pfx . md5_hex($method . $s);
1422 sub retrieve_cached_facets {
1428 return undef unless ($key and $key =~ /_facets$/);
1430 my $blob = $cache->get_cache($key) || {};
1434 for my $f ( keys %$blob ) {
1435 my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1436 @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1437 for my $s ( @sorted ) {
1438 my ($k) = keys(%$s);
1439 my ($v) = values(%$s);
1440 $$facets{$f}{$k} = $v;
1450 __PACKAGE__->register_method(
1451 method => "retrieve_cached_facets",
1452 api_name => "open-ils.search.facet_cache.retrieve",
1454 desc => 'Returns facet data derived from a specific search based on a key '.
1455 'generated by open-ils.search.biblio.multiclass.staged and friends.',
1458 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1463 desc => 'Two level hash of facet values. Top level key is the facet id defined on the config.metabib_field table. '.
1464 'Second level key is a string facet value. Datum attached to each facet value is the number of distinct records, '.
1465 'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1466 'facet retrieval. These counts are calculated for all superpages that have been checked for visibility.',
1474 # add facets for this search to the facet cache
1475 my($key, $results, $metabib, $ignore) = @_;
1476 my $data = $cache->get_cache($key);
1479 if (!ref($ignore)) {
1480 $ignore = ['identifier']; # ignore the identifier class by default
1483 return undef unless (@$results);
1485 # The query we're constructing
1487 # select mfae.field as id,
1489 # count(distinct mmrsm.appropriate-id-field )
1490 # from metabib.facet_entry mfae
1491 # join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1492 # where mmrsm.appropriate-id-field in IDLIST
1495 my $count_field = $metabib ? 'metarecord' : 'source';
1496 my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1498 mfae => [ { column => 'field', alias => 'id'}, 'value' ],
1500 transform => 'count',
1502 column => $count_field,
1509 mmrsm => { field => 'source', fkey => 'source' },
1510 cmf => { field => 'id', fkey => 'field' }
1514 '+mmrsm' => { $count_field => $results },
1515 '+cmf' => { field_class => { 'not in' => $ignore }, facet_field => 't' }
1520 for my $facet (@$facets) {
1521 next unless ($facet->{value});
1522 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1525 $logger->info("facet compilation: cached with key=$key");
1527 $cache->put_cache($key, $data, $cache_timeout);
1530 sub cache_staged_search_page {
1531 # puts this set of results into the cache
1532 my($key, $page, $summary, $results) = @_;
1533 my $data = $cache->get_cache($key);
1536 summary => $summary,
1540 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1541 $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1543 $cache->put_cache($key, $data, $cache_timeout);
1551 my $start = $offset;
1552 my $end = $offset + $limit - 1;
1554 $logger->debug("searching cache for $key : $start..$end\n");
1556 return undef unless $cache;
1557 my $data = $cache->get_cache($key);
1559 return undef unless $data;
1561 my $count = $data->[0];
1564 return undef unless $offset < $count;
1567 for( my $i = $offset; $i <= $end; $i++ ) {
1568 last unless my $d = $$data[$i];
1569 push( @result, $d );
1572 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1579 my( $key, $count, $data ) = @_;
1580 return undef unless $cache;
1581 $logger->debug("search_cache putting ".
1582 scalar(@$data)." items at key $key with timeout $cache_timeout");
1583 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1587 __PACKAGE__->register_method(
1588 method => "biblio_mrid_to_modsbatch_batch",
1589 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1592 sub biblio_mrid_to_modsbatch_batch {
1593 my( $self, $client, $mrids) = @_;
1594 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1596 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1597 for my $id (@$mrids) {
1598 next unless defined $id;
1599 my ($m) = $method->run($id);
1606 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1607 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1609 __PACKAGE__->register_method(
1610 method => "biblio_mrid_to_modsbatch",
1613 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1614 . "As usual, the .staff version of this method will include otherwise hidden records.",
1616 { desc => 'Metarecord ID', type => 'number' },
1617 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1620 desc => 'MVR Object, event on error',
1626 sub biblio_mrid_to_modsbatch {
1627 my( $self, $client, $mrid, $args) = @_;
1629 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1631 my ($mr, $evt) = _grab_metarecord($mrid);
1632 return $evt unless $mr;
1634 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1635 biblio_mrid_make_modsbatch($self, $client, $mr);
1637 return $mvr unless ref($args);
1639 # Here we find the lead record appropriate for the given filters
1640 # and use that for the title and author of the metarecord
1641 my $format = $$args{format};
1642 my $org = $$args{org};
1643 my $depth = $$args{depth};
1645 return $mvr unless $format or $org or $depth;
1647 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1648 $method = "$method.staff" if $self->api_name =~ /staff/o;
1650 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1652 if( my $mods = $U->record_to_mvr($rec) ) {
1654 $mvr->title( $mods->title );
1655 $mvr->author($mods->author);
1656 $logger->debug("mods_slim updating title and ".
1657 "author in mvr with ".$mods->title." : ".$mods->author);
1663 # converts a metarecord to an mvr
1666 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1667 return Fieldmapper::metabib::virtual_record->new($perl);
1670 # checks to see if a metarecord has mods, if so returns true;
1672 __PACKAGE__->register_method(
1673 method => "biblio_mrid_check_mvr",
1674 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1675 notes => "Takes a metarecord ID or a metarecord object and returns true "
1676 . "if the metarecord already has an mvr associated with it."
1679 sub biblio_mrid_check_mvr {
1680 my( $self, $client, $mrid ) = @_;
1684 if(ref($mrid)) { $mr = $mrid; }
1685 else { ($mr, $evt) = _grab_metarecord($mrid); }
1686 return $evt if $evt;
1688 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1690 return _mr_to_mvr($mr) if $mr->mods();
1694 sub _grab_metarecord {
1696 #my $e = OpenILS::Utils::Editor->new;
1697 my $e = new_editor();
1698 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1703 __PACKAGE__->register_method(
1704 method => "biblio_mrid_make_modsbatch",
1705 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1706 notes => "Takes either a metarecord ID or a metarecord object. "
1707 . "Forces the creations of an mvr for the given metarecord. "
1708 . "The created mvr is returned."
1711 sub biblio_mrid_make_modsbatch {
1712 my( $self, $client, $mrid ) = @_;
1714 #my $e = OpenILS::Utils::Editor->new;
1715 my $e = new_editor();
1722 $mr = $e->retrieve_metabib_metarecord($mrid)
1723 or return $e->event;
1726 my $masterid = $mr->master_record;
1727 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1729 my $ids = $U->storagereq(
1730 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1731 return undef unless @$ids;
1733 my $master = $e->retrieve_biblio_record_entry($masterid)
1734 or return $e->event;
1736 # start the mods batch
1737 my $u = OpenILS::Utils::ModsParser->new();
1738 $u->start_mods_batch( $master->marc );
1740 # grab all of the sub-records and shove them into the batch
1741 my @ids = grep { $_ ne $masterid } @$ids;
1742 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1747 my $r = $e->retrieve_biblio_record_entry($i);
1748 push( @$subrecs, $r ) if $r;
1753 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1754 $u->push_mods_batch( $_->marc ) if $_->marc;
1758 # finish up and send to the client
1759 my $mods = $u->finish_mods_batch();
1760 $mods->doc_id($mrid);
1761 $client->respond_complete($mods);
1764 # now update the mods string in the db
1765 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1768 #$e = OpenILS::Utils::Editor->new(xact => 1);
1769 $e = new_editor(xact => 1);
1770 $e->update_metabib_metarecord($mr)
1771 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1778 # converts a mr id into a list of record ids
1780 foreach (qw/open-ils.search.biblio.metarecord_to_records
1781 open-ils.search.biblio.metarecord_to_records.staff/)
1783 __PACKAGE__->register_method(
1784 method => "biblio_mrid_to_record_ids",
1787 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1788 . "As usual, the .staff version of this method will include otherwise hidden records.",
1790 { desc => 'Metarecord ID', type => 'number' },
1791 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1794 desc => 'Results object like {count => $i, ids =>[...]}',
1802 sub biblio_mrid_to_record_ids {
1803 my( $self, $client, $mrid, $args ) = @_;
1805 my $format = $$args{format};
1806 my $org = $$args{org};
1807 my $depth = $$args{depth};
1809 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1810 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1811 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1813 return { count => scalar(@$recs), ids => $recs };
1817 __PACKAGE__->register_method(
1818 method => "biblio_record_to_marc_html",
1819 api_name => "open-ils.search.biblio.record.html"
1822 __PACKAGE__->register_method(
1823 method => "biblio_record_to_marc_html",
1824 api_name => "open-ils.search.authority.to_html"
1827 # Persistent parsers and setting objects
1828 my $parser = XML::LibXML->new();
1829 my $xslt = XML::LibXSLT->new();
1831 my $slim_marc_sheet;
1832 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1834 sub biblio_record_to_marc_html {
1835 my($self, $client, $recordid, $slim, $marcxml) = @_;
1838 my $dir = $settings_client->config_value("dirs", "xsl");
1841 unless($slim_marc_sheet) {
1842 my $xsl = $settings_client->config_value(
1843 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1845 $xsl = $parser->parse_file("$dir/$xsl");
1846 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1849 $sheet = $slim_marc_sheet;
1853 unless($marc_sheet) {
1854 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1855 my $xsl = $settings_client->config_value(
1856 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1857 $xsl = $parser->parse_file("$dir/$xsl");
1858 $marc_sheet = $xslt->parse_stylesheet($xsl);
1860 $sheet = $marc_sheet;
1865 my $e = new_editor();
1866 if($self->api_name =~ /authority/) {
1867 $record = $e->retrieve_authority_record_entry($recordid)
1868 or return $e->event;
1870 $record = $e->retrieve_biblio_record_entry($recordid)
1871 or return $e->event;
1873 $marcxml = $record->marc;
1876 my $xmldoc = $parser->parse_string($marcxml);
1877 my $html = $sheet->transform($xmldoc);
1878 return $html->documentElement->toString();
1881 __PACKAGE__->register_method(
1882 method => "format_biblio_record_entry",
1883 api_name => "open-ils.search.biblio.record.print",
1885 desc => 'Returns a printable version of the specified bib record',
1887 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1890 desc => q/An action_trigger.event object or error event./,
1895 __PACKAGE__->register_method(
1896 method => "format_biblio_record_entry",
1897 api_name => "open-ils.search.biblio.record.email",
1899 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1901 { desc => 'Authentication token', type => 'string'},
1902 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1905 desc => q/Undefined on success, otherwise an error event./,
1911 sub format_biblio_record_entry {
1912 my($self, $conn, $arg1, $arg2) = @_;
1914 my $for_print = ($self->api_name =~ /print/);
1915 my $for_email = ($self->api_name =~ /email/);
1917 my $e; my $auth; my $bib_id; my $context_org;
1921 $context_org = $arg2 || $U->fetch_org_tree->id;
1922 $e = new_editor(xact => 1);
1923 } elsif ($for_email) {
1926 $e = new_editor(authtoken => $auth, xact => 1);
1927 return $e->die_event unless $e->checkauth;
1928 $context_org = $e->requestor->home_ou;
1932 if (ref $bib_id ne 'ARRAY') {
1933 $bib_ids = [ $bib_id ];
1938 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1939 $bucket->btype('temp');
1940 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1942 $bucket->owner($e->requestor)
1946 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1948 for my $id (@$bib_ids) {
1950 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1952 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1953 $bucket_entry->target_biblio_record_entry($bib);
1954 $bucket_entry->bucket($bucket_obj->id);
1955 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1962 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1964 } elsif ($for_email) {
1966 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1973 __PACKAGE__->register_method(
1974 method => "retrieve_all_copy_statuses",
1975 api_name => "open-ils.search.config.copy_status.retrieve.all"
1978 sub retrieve_all_copy_statuses {
1979 my( $self, $client ) = @_;
1980 return new_editor()->retrieve_all_config_copy_status();
1984 __PACKAGE__->register_method(
1985 method => "copy_counts_per_org",
1986 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1989 __PACKAGE__->register_method(
1990 method => "copy_counts_per_org",
1991 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1994 sub copy_counts_per_org {
1995 my( $self, $client, $record_id ) = @_;
1997 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1999 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
2000 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
2002 my $counts = $apputils->simple_scalar_request(
2003 "open-ils.storage", $method, $record_id );
2005 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
2010 __PACKAGE__->register_method(
2011 method => "copy_count_summary",
2012 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
2013 notes => "returns an array of these: "
2014 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2015 . "where statusx is a copy status name. The statuses are sorted by ID.",
2019 sub copy_count_summary {
2020 my( $self, $client, $rid, $org, $depth ) = @_;
2023 my $data = $U->storagereq(
2024 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
2027 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2029 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2033 __PACKAGE__->register_method(
2034 method => "copy_location_count_summary",
2035 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
2036 notes => "returns an array of these: "
2037 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
2038 . "where statusx is a copy status name. The statuses are sorted by ID.",
2041 sub copy_location_count_summary {
2042 my( $self, $client, $rid, $org, $depth ) = @_;
2045 my $data = $U->storagereq(
2046 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2049 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2051 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2053 || $a->[4] cmp $b->[4]
2057 __PACKAGE__->register_method(
2058 method => "copy_count_location_summary",
2059 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2060 notes => "returns an array of these: "
2061 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2062 . "where statusx is a copy status name. The statuses are sorted by ID."
2065 sub copy_count_location_summary {
2066 my( $self, $client, $rid, $org, $depth ) = @_;
2069 my $data = $U->storagereq(
2070 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2072 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2074 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2079 foreach (qw/open-ils.search.biblio.marc
2080 open-ils.search.biblio.marc.staff/)
2082 __PACKAGE__->register_method(
2083 method => "marc_search",
2086 desc => 'Fetch biblio IDs based on MARC record criteria. '
2087 . 'As usual, the .staff version of the search includes otherwise hidden records',
2090 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2091 'See perldoc ' . __PACKAGE__ . ' for more detail.',
2094 {desc => 'limit (optional)', type => 'number'},
2095 {desc => 'offset (optional)', type => 'number'}
2098 desc => 'Results object like: { "count": $i, "ids": [...] }',
2105 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
2107 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
2109 searches: complex query object (required)
2110 org_unit: The org ID to focus the search at
2111 depth : The org depth
2112 limit : integer search limit default: 10
2113 offset : integer search offset default: 0
2114 sort : What field to sort the results on? [ author | title | pubdate ]
2115 sort_dir: In what direction do we sort? [ asc | desc ]
2117 Additional keys to refine search criteria:
2120 language : Language (code)
2121 lit_form : Literary form
2122 item_form: Item form
2123 item_type: Item type
2124 format : The MARC format
2126 Please note that the specific strings to be used in the "addtional keys" will be entirely
2127 dependent on your loaded data.
2129 All keys except "searches" are optional.
2130 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
2132 For example, an arg hash might look like:
2154 The arghash is eventually passed to the SRF call:
2155 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2157 Presently, search uses the cache unconditionally.
2161 # FIXME: that example above isn't actually tested.
2162 # TODO: docache option?
2164 my( $self, $conn, $args, $limit, $offset, $timeout ) = @_;
2166 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2167 $method .= ".staff" if $self->api_name =~ /staff/;
2168 $method .= ".atomic";
2170 $limit ||= 10; # FIXME: what about $args->{limit} ?
2171 $offset ||= 0; # FIXME: what about $args->{offset} ?
2173 # allow caller to pass in a call timeout since MARC searches
2174 # can take longer than the default 60-second timeout.
2175 # Default to 2 mins. Arbitrarily cap at 5 mins.
2176 $timeout = 120 if !$timeout or $timeout > 300;
2179 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2180 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2182 my $recs = search_cache($ckey, $offset, $limit);
2186 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2187 my $req = $ses->request($method, %$args);
2188 my $resp = $req->recv($timeout);
2190 if($resp and $recs = $resp->content) {
2191 put_cache($ckey, scalar(@$recs), $recs);
2192 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2201 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2202 my @recs = map { $_->[0] } @$recs;
2204 return { ids => \@recs, count => $count };
2208 foreach my $isbn_method (qw/
2209 open-ils.search.biblio.isbn
2210 open-ils.search.biblio.isbn.staff
2212 __PACKAGE__->register_method(
2213 method => "biblio_search_isbn",
2214 api_name => $isbn_method,
2216 desc => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2218 {desc => 'ISBN', type => 'string'}
2221 desc => 'Results object like: { "count": $i, "ids": [...] }',
2228 sub biblio_search_isbn {
2229 my( $self, $client, $isbn ) = @_;
2230 $logger->debug("Searching ISBN $isbn");
2231 # the previous implementation of this method was essentially unlimited,
2232 # so we will set our limit very high and let multiclass.query provide any
2234 # XXX: if making this unlimited is deemed important, we might consider
2235 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2236 # which is functionally deprecated at this point, or a custom call to
2237 # 'open-ils.storage.biblio.multiclass.search_fts'
2239 my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2240 if ($self->api_name =~ m/.staff$/) {
2241 $isbn_method .= '.staff';
2244 my $method = $self->method_lookup($isbn_method);
2245 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2246 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2247 return { ids => \@recs, count => $search_result->{'count'} };
2250 __PACKAGE__->register_method(
2251 method => "biblio_search_isbn_batch",
2252 api_name => "open-ils.search.biblio.isbn_list",
2255 # XXX: see biblio_search_isbn() for note concerning 'limit'
2256 sub biblio_search_isbn_batch {
2257 my( $self, $client, $isbn_list ) = @_;
2258 $logger->debug("Searching ISBNs @$isbn_list");
2259 my @recs = (); my %rec_set = ();
2260 my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2261 foreach my $isbn ( @$isbn_list ) {
2262 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2263 my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2264 foreach my $rec (@recs_subset) {
2265 if (! $rec_set{ $rec }) {
2266 $rec_set{ $rec } = 1;
2271 return { ids => \@recs, count => scalar(@recs) };
2274 foreach my $issn_method (qw/
2275 open-ils.search.biblio.issn
2276 open-ils.search.biblio.issn.staff
2278 __PACKAGE__->register_method(
2279 method => "biblio_search_issn",
2280 api_name => $issn_method,
2282 desc => 'Retrieve biblio IDs for a given ISSN',
2284 {desc => 'ISBN', type => 'string'}
2287 desc => 'Results object like: { "count": $i, "ids": [...] }',
2294 sub biblio_search_issn {
2295 my( $self, $client, $issn ) = @_;
2296 $logger->debug("Searching ISSN $issn");
2297 # the previous implementation of this method was essentially unlimited,
2298 # so we will set our limit very high and let multiclass.query provide any
2300 # XXX: if making this unlimited is deemed important, we might consider
2301 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2302 # which is functionally deprecated at this point, or a custom call to
2303 # 'open-ils.storage.biblio.multiclass.search_fts'
2305 my $issn_method = 'open-ils.search.biblio.multiclass.query';
2306 if ($self->api_name =~ m/.staff$/) {
2307 $issn_method .= '.staff';
2310 my $method = $self->method_lookup($issn_method);
2311 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2312 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2313 return { ids => \@recs, count => $search_result->{'count'} };
2317 __PACKAGE__->register_method(
2318 method => "fetch_mods_by_copy",
2319 api_name => "open-ils.search.biblio.mods_from_copy",
2322 desc => 'Retrieve MODS record given an attached copy ID',
2324 { desc => 'Copy ID', type => 'number' }
2327 desc => 'MODS record, event on error or uncataloged item'
2332 sub fetch_mods_by_copy {
2333 my( $self, $client, $copyid ) = @_;
2334 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2335 return $evt if $evt;
2336 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2337 return $apputils->record_to_mvr($record);
2341 # -------------------------------------------------------------------------------------
2343 __PACKAGE__->register_method(
2344 method => "cn_browse",
2345 api_name => "open-ils.search.callnumber.browse.target",
2346 notes => "Starts a callnumber browse"
2349 __PACKAGE__->register_method(
2350 method => "cn_browse",
2351 api_name => "open-ils.search.callnumber.browse.page_up",
2352 notes => "Returns the previous page of callnumbers",
2355 __PACKAGE__->register_method(
2356 method => "cn_browse",
2357 api_name => "open-ils.search.callnumber.browse.page_down",
2358 notes => "Returns the next page of callnumbers",
2362 # RETURNS array of arrays like so: label, owning_lib, record, id
2364 my( $self, $client, @params ) = @_;
2367 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2368 if( $self->api_name =~ /target/ );
2369 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2370 if( $self->api_name =~ /page_up/ );
2371 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2372 if( $self->api_name =~ /page_down/ );
2374 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2376 # -------------------------------------------------------------------------------------
2378 __PACKAGE__->register_method(
2379 method => "fetch_cn",
2380 api_name => "open-ils.search.callnumber.retrieve",
2382 notes => "retrieves a callnumber based on ID",
2386 my( $self, $client, $id ) = @_;
2388 my $e = new_editor();
2389 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2390 return $evt if $evt;
2394 __PACKAGE__->register_method(
2395 method => "fetch_fleshed_cn",
2396 api_name => "open-ils.search.callnumber.fleshed.retrieve",
2398 notes => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2401 sub fetch_fleshed_cn {
2402 my( $self, $client, $id ) = @_;
2404 my $e = new_editor();
2405 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2406 return $evt if $evt;
2411 __PACKAGE__->register_method(
2412 method => "fetch_copy_by_cn",
2413 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2415 Returns an array of copy ID's by callnumber ID
2416 @param cnid The callnumber ID
2417 @return An array of copy IDs
2421 sub fetch_copy_by_cn {
2422 my( $self, $conn, $cnid ) = @_;
2423 return $U->cstorereq(
2424 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2425 { call_number => $cnid, deleted => 'f' } );
2428 __PACKAGE__->register_method(
2429 method => 'fetch_cn_by_info',
2430 api_name => 'open-ils.search.call_number.retrieve_by_info',
2432 @param label The callnumber label
2433 @param record The record the cn is attached to
2434 @param org The owning library of the cn
2435 @return The callnumber object
2440 sub fetch_cn_by_info {
2441 my( $self, $conn, $label, $record, $org ) = @_;
2442 return $U->cstorereq(
2443 'open-ils.cstore.direct.asset.call_number.search',
2444 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2449 __PACKAGE__->register_method(
2450 method => 'bib_extras',
2451 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2454 __PACKAGE__->register_method(
2455 method => 'bib_extras',
2456 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2457 ctype => 'item_form'
2459 __PACKAGE__->register_method(
2460 method => 'bib_extras',
2461 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2462 ctype => 'item_type',
2464 __PACKAGE__->register_method(
2465 method => 'bib_extras',
2466 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2467 ctype => 'bib_level'
2469 __PACKAGE__->register_method(
2470 method => 'bib_extras',
2471 api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2477 $logger->warn("deprecation warning: " .$self->api_name);
2479 my $e = new_editor();
2481 my $ctype = $self->{ctype};
2482 my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2485 for my $ccvm (@$ccvms) {
2486 my $obj = "Fieldmapper::config::${ctype}_map"->new;
2487 $obj->value($ccvm->value);
2488 $obj->code($ccvm->code);
2489 $obj->description($ccvm->description) if $obj->can('description');
2498 __PACKAGE__->register_method(
2499 method => 'fetch_slim_record',
2500 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2502 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2504 { desc => 'Array of Record IDs', type => 'array' }
2507 desc => 'Array of biblio records, event on error'
2512 sub fetch_slim_record {
2513 my( $self, $conn, $ids ) = @_;
2515 #my $editor = OpenILS::Utils::Editor->new;
2516 my $editor = new_editor();
2519 return $editor->event unless
2520 my $r = $editor->retrieve_biblio_record_entry($_);
2527 __PACKAGE__->register_method(
2528 method => 'rec_hold_parts',
2529 api_name => 'open-ils.search.biblio.record_hold_parts',
2531 Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2535 sub rec_hold_parts {
2536 my( $self, $conn, $args ) = @_;
2538 my $rec = $$args{record};
2539 my $mrec = $$args{metarecord};
2540 my $pickup_lib = $$args{pickup_lib};
2541 my $e = new_editor();
2544 select => {bmp => ['id', 'label']},
2549 select => {'acpm' => ['part']},
2550 from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2552 '+acp' => {'deleted' => 'f'},
2553 '+bre' => {id => $rec}
2559 order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2562 if(defined $pickup_lib) {
2563 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2564 if($hard_boundary) {
2565 my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2566 $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2570 return $e->json_query($query);
2576 __PACKAGE__->register_method(
2577 method => 'rec_to_mr_rec_descriptors',
2578 api_name => 'open-ils.search.metabib.record_to_descriptors',
2580 specialized method...
2581 Given a biblio record id or a metarecord id,
2582 this returns a list of metabib.record_descriptor
2583 objects that live within the same metarecord
2584 @param args Object of args including:
2588 sub rec_to_mr_rec_descriptors {
2589 my( $self, $conn, $args ) = @_;
2591 my $rec = $$args{record};
2592 my $mrec = $$args{metarecord};
2593 my $item_forms = $$args{item_forms};
2594 my $item_types = $$args{item_types};
2595 my $item_lang = $$args{item_lang};
2596 my $pickup_lib = $$args{pickup_lib};
2598 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2600 my $e = new_editor();
2604 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2605 return $e->event unless @$map;
2606 $mrec = $$map[0]->metarecord;
2609 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2610 return $e->event unless @$recs;
2612 my @recs = map { $_->source } @$recs;
2613 my $search = { record => \@recs };
2614 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2615 $search->{item_type} = $item_types if $item_types and @$item_types;
2616 $search->{item_lang} = $item_lang if $item_lang;
2618 my $desc = $e->search_metabib_record_descriptor($search);
2622 select => { 'bre' => ['id'] },
2627 'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2633 '+bre' => { id => \@recs },
2638 "+ccs" => { holdable => 't' },
2639 "+acpl" => { holdable => 't' }
2643 if ($hard_boundary) { # 0 (or "top") is the same as no setting
2644 my $orgs = $e->json_query(
2645 { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2646 ) or return $e->die_event;
2648 $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2651 my $good_records = $e->json_query($query) or return $e->die_event;
2654 for my $d (@$desc) {
2655 if ( grep { $d->record == $_->{id} } @$good_records ) {
2662 return { metarecord => $mrec, descriptors => $desc };
2666 __PACKAGE__->register_method(
2667 method => 'fetch_age_protect',
2668 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2671 sub fetch_age_protect {
2672 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2676 __PACKAGE__->register_method(
2677 method => 'copies_by_cn_label',
2678 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2681 __PACKAGE__->register_method(
2682 method => 'copies_by_cn_label',
2683 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2686 sub copies_by_cn_label {
2687 my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2688 my $e = new_editor();
2689 my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2690 my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2691 my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2692 return [] unless @$cns;
2694 # show all non-deleted copies in the staff client ...
2695 if ($self->api_name =~ /staff$/o) {
2696 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2699 # ... otherwise, grab the copies ...
2700 my $copies = $e->search_asset_copy(
2701 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2702 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2706 # ... and test for location and status visibility
2707 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];