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 sleep);
20 use OpenSRF::EX qw(:try);
21 use Digest::MD5 qw(md5_hex);
27 $Data::Dumper::Indent = 0;
29 use OpenILS::Const qw/:const/;
31 use OpenILS::Application::AppUtils;
32 my $apputils = "OpenILS::Application::AppUtils";
35 my $pfx = "open-ils.search_";
43 $cache = OpenSRF::Utils::Cache->new('global');
44 my $sclient = OpenSRF::Utils::SettingsClient->new();
45 $cache_timeout = $sclient->config_value(
46 "apps", "open-ils.search", "app_settings", "cache_timeout" ) || 300;
48 $superpage_size = $sclient->config_value(
49 "apps", "open-ils.search", "app_settings", "superpage_size" ) || 500;
51 $max_superpages = $sclient->config_value(
52 "apps", "open-ils.search", "app_settings", "max_superpages" ) || 20;
54 $logger->info("Search cache timeout is $cache_timeout, ".
55 " superpage_size is $superpage_size, max_superpages is $max_superpages");
60 # ---------------------------------------------------------------------------
61 # takes a list of record id's and turns the docs into friendly
62 # mods structures. Creates one MODS structure for each doc id.
63 # ---------------------------------------------------------------------------
64 sub _records_to_mods {
70 my $session = OpenSRF::AppSession->create("open-ils.cstore");
71 my $request = $session->request(
72 "open-ils.cstore.direct.biblio.record_entry.search", { id => \@ids } );
74 while( my $resp = $request->recv ) {
75 my $content = $resp->content;
76 next if $content->id == OILS_PRECAT_RECORD;
77 my $u = OpenILS::Utils::ModsParser->new(); # FIXME: we really need a new parser for each object?
78 $u->start_mods_batch( $content->marc );
79 my $mods = $u->finish_mods_batch();
80 $mods->doc_id($content->id());
81 $mods->tcn($content->tcn_value);
85 $session->disconnect();
89 __PACKAGE__->register_method(
90 method => "record_id_to_mods",
91 api_name => "open-ils.search.biblio.record.mods.retrieve",
94 desc => "Provide ID, we provide the MODS object with copy count. "
95 . "Note: this method does NOT take an array of IDs like mods_slim.retrieve", # FIXME: do it here too
97 { desc => 'Record ID', type => 'number' }
100 desc => 'MODS object', type => 'object'
105 # converts a record into a mods object with copy counts attached
106 sub record_id_to_mods {
108 my( $self, $client, $org_id, $id ) = @_;
110 my $mods_list = _records_to_mods( $id );
111 my $mods_obj = $mods_list->[0];
112 my $cmethod = $self->method_lookup("open-ils.search.biblio.record.copy_count");
113 my ($count) = $cmethod->run($org_id, $id);
114 $mods_obj->copy_count($count);
121 __PACKAGE__->register_method(
122 method => "record_id_to_mods_slim",
123 api_name => "open-ils.search.biblio.record.mods_slim.retrieve",
127 desc => "Provide ID(s), we provide the MODS",
129 { desc => 'Record ID or array of IDs' }
132 desc => 'MODS object(s), event on error'
137 # converts a record into a mods object with NO copy counts attached
138 sub record_id_to_mods_slim {
139 my( $self, $client, $id ) = @_;
140 return undef unless defined $id;
142 if(ref($id) and ref($id) == 'ARRAY') {
143 return _records_to_mods( @$id );
145 my $mods_list = _records_to_mods( $id );
146 my $mods_obj = $mods_list->[0];
147 return OpenILS::Event->new('BIBLIO_RECORD_ENTRY_NOT_FOUND') unless $mods_obj;
153 __PACKAGE__->register_method(
154 method => "record_id_to_mods_slim_batch",
155 api_name => "open-ils.search.biblio.record.mods_slim.batch.retrieve",
158 sub record_id_to_mods_slim_batch {
159 my($self, $conn, $id_list) = @_;
160 $conn->respond(_records_to_mods($_)->[0]) for @$id_list;
165 # Returns the number of copies attached to a record based on org location
166 __PACKAGE__->register_method(
167 method => "record_id_to_copy_count",
168 api_name => "open-ils.search.biblio.record.copy_count",
170 desc => q/Returns a copy summary for the given record for the context org
171 unit and all ancestor org units/,
173 {desc => 'Context org unit id', type => 'number'},
174 {desc => 'Record ID', type => 'number'}
177 desc => q/summary object per org unit in the set, where the set
178 includes the context org unit and all parent org units.
179 Object includes the keys "transcendant", "count", "org_unit", "depth",
180 "unshadow", "available". Each is a count, except "org_unit" which is
181 the context org unit and "depth" which is the depth of the context org unit
188 __PACKAGE__->register_method(
189 method => "record_id_to_copy_count",
190 api_name => "open-ils.search.biblio.record.copy_count.staff",
193 desc => q/Returns a copy summary for the given record for the context org
194 unit and all ancestor org units/,
196 {desc => 'Context org unit id', type => 'number'},
197 {desc => 'Record ID', type => 'number'}
200 desc => q/summary object per org unit in the set, where the set
201 includes the context org unit and all parent org units.
202 Object includes the keys "transcendant", "count", "org_unit", "depth",
203 "unshadow", "available". Each is a count, except "org_unit" which is
204 the context org unit and "depth" which is the depth of the context org unit
211 __PACKAGE__->register_method(
212 method => "record_id_to_copy_count",
213 api_name => "open-ils.search.biblio.metarecord.copy_count",
215 desc => q/Returns a copy summary for the given record for the context org
216 unit and all ancestor org units/,
218 {desc => 'Context org unit id', type => 'number'},
219 {desc => 'Record ID', type => 'number'}
222 desc => q/summary object per org unit in the set, where the set
223 includes the context org unit and all parent org units.
224 Object includes the keys "transcendant", "count", "org_unit", "depth",
225 "unshadow", "available". Each is a count, except "org_unit" which is
226 the context org unit and "depth" which is the depth of the context org unit
233 __PACKAGE__->register_method(
234 method => "record_id_to_copy_count",
235 api_name => "open-ils.search.biblio.metarecord.copy_count.staff",
237 desc => q/Returns a copy summary for the given record for the context org
238 unit and all ancestor org units/,
240 {desc => 'Context org unit id', type => 'number'},
241 {desc => 'Record ID', type => 'number'}
244 desc => q/summary object per org unit in the set, where the set
245 includes the context org unit and all parent org units.
246 Object includes the keys "transcendant", "count", "org_unit", "depth",
247 "unshadow", "available". Each is a count, except "org_unit" which is
248 the context org unit and "depth" which is the depth of the context org
249 unit. "depth" is always -1 when the count from a lasso search is
250 performed, since depth doesn't mean anything in a lasso context.
257 sub record_id_to_copy_count {
258 my( $self, $client, $org_id, $record_id ) = @_;
260 return [] unless $record_id;
262 my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
263 my $staff = $self->api_name =~ /staff/ ? 't' : 'f';
265 my $data = $U->cstorereq(
266 "open-ils.cstore.json_query.atomic",
267 { from => ['asset.' . $key . '_copy_count' => $org_id => $record_id => $staff] }
271 for my $d ( @$data ) { # fix up the key name change required by stored-proc version
272 $$d{count} = delete $$d{visible};
276 return [ sort { $a->{depth} <=> $b->{depth} } @count ];
279 __PACKAGE__->register_method(
280 method => "record_has_holdable_copy",
281 api_name => "open-ils.search.biblio.record.has_holdable_copy",
283 desc => q/Returns a boolean indicating if a record has any holdable copies./,
285 {desc => 'Record ID', type => 'number'}
288 desc => q/bool indicating if the record has any holdable copies/,
294 __PACKAGE__->register_method(
295 method => "record_has_holdable_copy",
296 api_name => "open-ils.search.biblio.metarecord.has_holdable_copy",
298 desc => q/Returns a boolean indicating if a record has any holdable copies./,
300 {desc => 'Record ID', type => 'number'}
303 desc => q/bool indicating if the record has any holdable copies/,
309 sub record_has_holdable_copy {
310 my($self, $client, $record_id ) = @_;
312 return 0 unless $record_id;
314 my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
316 my $data = $U->cstorereq(
317 "open-ils.cstore.json_query.atomic",
318 { from => ['asset.' . $key . '_has_holdable_copy' => $record_id ] }
321 return ${@$data[0]}{'asset.' . $key . '_has_holdable_copy'} eq 't';
325 __PACKAGE__->register_method(
326 method => "biblio_search_tcn",
327 api_name => "open-ils.search.biblio.tcn",
330 desc => "Retrieve related record ID(s) given a TCN",
332 { desc => 'TCN', type => 'string' },
333 { desc => 'Flag indicating to include deleted records', type => 'string' }
336 desc => 'Results object like: { "count": $i, "ids": [...] }',
343 sub biblio_search_tcn {
345 my( $self, $client, $tcn, $include_deleted ) = @_;
347 $tcn =~ s/^\s+|\s+$//og;
349 my $e = new_editor();
350 my $search = {tcn_value => $tcn};
351 $search->{deleted} = 'f' unless $include_deleted;
352 my $recs = $e->search_biblio_record_entry( $search, {idlist =>1} );
354 return { count => scalar(@$recs), ids => $recs };
358 # --------------------------------------------------------------------------------
360 __PACKAGE__->register_method(
361 method => "biblio_barcode_to_copy",
362 api_name => "open-ils.search.asset.copy.find_by_barcode",
364 sub biblio_barcode_to_copy {
365 my( $self, $client, $barcode ) = @_;
366 my( $copy, $evt ) = $U->fetch_copy_by_barcode($barcode);
371 __PACKAGE__->register_method(
372 method => "biblio_id_to_copy",
373 api_name => "open-ils.search.asset.copy.batch.retrieve",
375 sub biblio_id_to_copy {
376 my( $self, $client, $ids ) = @_;
377 $logger->info("Fetching copies @$ids");
378 return $U->cstorereq(
379 "open-ils.cstore.direct.asset.copy.search.atomic", { id => $ids } );
383 __PACKAGE__->register_method(
384 method => "biblio_id_to_uris",
385 api_name=> "open-ils.search.asset.uri.retrieve_by_bib",
389 @param BibID Which bib record contains the URIs
390 @param OrgID Where to look for URIs
391 @param OrgDepth Range adjustment for OrgID
392 @return A stream or list of 'auri' objects
396 sub biblio_id_to_uris {
397 my( $self, $client, $bib, $org, $depth ) = @_;
398 die "Org ID required" unless defined($org);
399 die "Bib ID required" unless defined($bib);
402 push @params, $depth if (defined $depth);
404 my $ids = $U->cstorereq( "open-ils.cstore.json_query.atomic",
405 { select => { auri => [ 'id' ] },
409 field => 'call_number',
415 filter => { active => 't' }
426 select => { aou => [ { column => 'id', transform => 'actor.org_unit_descendants', params => \@params, result_field => 'id' } ] },
428 where => { id => $org },
438 my $uris = $U->cstorereq(
439 "open-ils.cstore.direct.asset.uri.search.atomic",
440 { id => [ map { (values %$_) } @$ids ] }
443 $client->respond($_) for (@$uris);
449 __PACKAGE__->register_method(
450 method => "copy_retrieve",
451 api_name => "open-ils.search.asset.copy.retrieve",
454 desc => 'Retrieve a copy object based on the Copy ID',
456 { desc => 'Copy ID', type => 'number'}
459 desc => 'Copy object, event on error'
465 my( $self, $client, $cid ) = @_;
466 my( $copy, $evt ) = $U->fetch_copy($cid);
467 return $evt || $copy;
470 __PACKAGE__->register_method(
471 method => "volume_retrieve",
472 api_name => "open-ils.search.asset.call_number.retrieve"
474 sub volume_retrieve {
475 my( $self, $client, $vid ) = @_;
476 my $e = new_editor();
477 my $vol = $e->retrieve_asset_call_number($vid) or return $e->event;
481 __PACKAGE__->register_method(
482 method => "fleshed_copy_retrieve_batch",
483 api_name => "open-ils.search.asset.copy.fleshed.batch.retrieve",
487 sub fleshed_copy_retrieve_batch {
488 my( $self, $client, $ids ) = @_;
489 $logger->info("Fetching fleshed copies @$ids");
490 return $U->cstorereq(
491 "open-ils.cstore.direct.asset.copy.search.atomic",
494 flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries parts / ] }
499 __PACKAGE__->register_method(
500 method => "fleshed_copy_retrieve",
501 api_name => "open-ils.search.asset.copy.fleshed.retrieve",
504 sub fleshed_copy_retrieve {
505 my( $self, $client, $id ) = @_;
506 my( $c, $e) = $U->fetch_fleshed_copy($id);
511 __PACKAGE__->register_method(
512 method => 'fleshed_by_barcode',
513 api_name => "open-ils.search.asset.copy.fleshed2.find_by_barcode",
516 sub fleshed_by_barcode {
517 my( $self, $conn, $barcode ) = @_;
518 my $e = new_editor();
519 my $copyid = $e->search_asset_copy(
520 {barcode => $barcode, deleted => 'f'}, {idlist=>1})->[0]
522 return fleshed_copy_retrieve2( $self, $conn, $copyid);
526 __PACKAGE__->register_method(
527 method => "fleshed_copy_retrieve2",
528 api_name => "open-ils.search.asset.copy.fleshed2.retrieve",
532 sub fleshed_copy_retrieve2 {
533 my( $self, $client, $id ) = @_;
534 my $e = new_editor();
535 my $copy = $e->retrieve_asset_copy(
542 qw/ location status stat_cat_entry_copy_maps notes age_protect parts peer_record_maps /
544 ascecm => [qw/ stat_cat stat_cat_entry /],
548 ) or return $e->event;
550 # For backwards compatibility
551 #$copy->stat_cat_entries($copy->stat_cat_entry_copy_maps);
553 if( $copy->status->id == OILS_COPY_STATUS_CHECKED_OUT ) {
555 $e->search_action_circulation(
557 { target_copy => $copy->id },
559 order_by => { circ => 'xact_start desc' },
571 __PACKAGE__->register_method(
572 method => 'flesh_copy_custom',
573 api_name => 'open-ils.search.asset.copy.fleshed.custom',
577 sub flesh_copy_custom {
578 my( $self, $conn, $copyid, $fields ) = @_;
579 my $e = new_editor();
580 my $copy = $e->retrieve_asset_copy(
590 ) or return $e->event;
595 __PACKAGE__->register_method(
596 method => "biblio_barcode_to_title",
597 api_name => "open-ils.search.biblio.find_by_barcode",
600 sub biblio_barcode_to_title {
601 my( $self, $client, $barcode ) = @_;
603 my $title = $apputils->simple_scalar_request(
605 "open-ils.storage.biblio.record_entry.retrieve_by_barcode", $barcode );
607 return { ids => [ $title->id ], count => 1 } if $title;
608 return { count => 0 };
611 __PACKAGE__->register_method(
612 method => 'title_id_by_item_barcode',
613 api_name => 'open-ils.search.bib_id.by_barcode',
616 desc => 'Retrieve bib record id associated with the copy identified by the given barcode',
618 { desc => 'Item barcode', type => 'string' }
621 desc => 'Bib record id.'
626 __PACKAGE__->register_method(
627 method => 'title_id_by_item_barcode',
628 api_name => 'open-ils.search.multi_home.bib_ids.by_barcode',
631 desc => 'Retrieve bib record ids associated with the copy identified by the given barcode. This includes peer bibs for Multi-Home items.',
633 { desc => 'Item barcode', type => 'string' }
636 desc => 'Array of bib record ids. First element is the native bib for the item.'
642 sub title_id_by_item_barcode {
643 my( $self, $conn, $barcode ) = @_;
644 my $e = new_editor();
645 my $copies = $e->search_asset_copy(
647 { deleted => 'f', barcode => $barcode },
651 acp => [ 'call_number' ],
658 return $e->event unless @$copies;
660 if( $self->api_name =~ /multi_home/ ) {
661 my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
663 { target_copy => $$copies[0]->id }
666 my @temp = map { $_->peer_record } @{ $multi_home_list };
667 unshift @temp, $$copies[0]->call_number->record->id;
670 return $$copies[0]->call_number->record->id;
674 __PACKAGE__->register_method(
675 method => 'find_peer_bibs',
676 api_name => 'open-ils.search.peer_bibs.test',
679 desc => 'Tests to see if the specified record is a peer record.',
681 { desc => 'Biblio record entry Id', type => 'number' }
684 desc => 'True if specified id can be found in biblio.peer_bib_copy_map.peer_record.',
690 __PACKAGE__->register_method(
691 method => 'find_peer_bibs',
692 api_name => 'open-ils.search.peer_bibs',
695 desc => 'Return acps and mvrs for multi-home items linked to specified peer record.',
697 { desc => 'Biblio record entry Id', type => 'number' }
700 desc => '{ records => Array of mvrs, items => array of acps }',
707 my( $self, $client, $doc_id ) = @_;
708 my $e = new_editor();
710 my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
712 { peer_record => $doc_id },
716 bpbcm => [ 'target_copy', 'peer_type' ],
717 acp => [ 'call_number', 'location', 'status', 'peer_record_maps' ]
723 if ($self->api_name =~ /test/) {
724 return scalar( @{$multi_home_list} ) > 0 ? 1 : 0;
727 if (scalar(@{$multi_home_list})==0) {
731 # create a unique hash of the primary record MVRs for foreign copies
732 # XXX PLEASE let's change to unAPI2 (supports foreign copies) in the TT opac?!?
734 ($_->target_copy->call_number->record, _records_to_mods( $_->target_copy->call_number->record )->[0])
737 # set the foreign_copy_maps field to an empty array
738 map { $rec_hash{$_}->foreign_copy_maps([]) } keys( %rec_hash );
740 # push the maps onto the correct MVRs
741 for (@$multi_home_list) {
743 @{$rec_hash{ $_->target_copy->call_number->record }->foreign_copy_maps()},
748 return [sort {$a->title cmp $b->title} values(%rec_hash)];
751 __PACKAGE__->register_method(
752 method => "biblio_copy_to_mods",
753 api_name => "open-ils.search.biblio.copy.mods.retrieve",
756 # takes a copy object and returns it fleshed mods object
757 sub biblio_copy_to_mods {
758 my( $self, $client, $copy ) = @_;
760 my $volume = $U->cstorereq(
761 "open-ils.cstore.direct.asset.call_number.retrieve",
762 $copy->call_number() );
764 my $mods = _records_to_mods($volume->record());
765 $mods = shift @$mods;
766 $volume->copies([$copy]);
767 push @{$mods->call_numbers()}, $volume;
775 OpenILS::Application::Search::Biblio
781 =head3 open-ils.search.biblio.multiclass.query (arghash, query, docache)
783 For arghash and docache, see B<open-ils.search.biblio.multiclass>.
785 The query argument is a string, but built like a hash with key: value pairs.
786 Recognized search keys include:
788 keyword (kw) - search keyword(s) *
789 author (au) - search author(s) *
790 name (au) - same as author *
791 title (ti) - search title *
792 subject (su) - search subject *
793 series (se) - search series *
794 lang - limit by language (specify multiple langs with lang:l1 lang:l2 ...)
795 site - search at specified org unit, corresponds to actor.org_unit.shortname
796 pref_ou - extend search to specified org unit, corresponds to actor.org_unit.shortname
797 sort - sort type (title, author, pubdate)
798 dir - sort direction (asc, desc)
799 available - if set to anything other than "false" or "0", limits to available items
801 * Searching keyword, author, title, subject, and series supports additional search
802 subclasses, specified with a "|". For example, C<title|proper:gone with the wind>.
804 For more, see B<config.metabib_field>.
808 foreach (qw/open-ils.search.biblio.multiclass.query
809 open-ils.search.biblio.multiclass.query.staff
810 open-ils.search.metabib.multiclass.query
811 open-ils.search.metabib.multiclass.query.staff/)
813 __PACKAGE__->register_method(
815 method => 'multiclass_query',
817 desc => 'Perform a search query. The .staff version of the call includes otherwise hidden hits.',
819 {name => 'arghash', desc => 'Arg hash (see open-ils.search.biblio.multiclass)', type => 'object'},
820 {name => 'query', desc => 'Raw human-readable query (see perldoc '. __PACKAGE__ .')', type => 'string'},
821 {name => 'docache', desc => 'Flag for caching (see open-ils.search.biblio.multiclass)', type => 'object'},
824 desc => 'Search results from query, like: { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
825 type => 'object', # TODO: update as miker's new elements are included
831 sub multiclass_query {
832 # arghash only really supports limit/offset anymore
833 my($self, $conn, $arghash, $query, $docache) = @_;
837 $query =~ s/^\s+//go;
838 $query =~ s/\s+/ /go;
839 $arghash->{query} = $query
842 $logger->debug("initial search query => $query") if $query;
844 (my $method = $self->api_name) =~ s/\.query/.staged/o;
845 return $self->method_lookup($method)->dispatch($arghash, $docache);
849 __PACKAGE__->register_method(
850 method => 'cat_search_z_style_wrapper',
851 api_name => 'open-ils.search.biblio.zstyle',
853 signature => q/@see open-ils.search.biblio.multiclass/
856 __PACKAGE__->register_method(
857 method => 'cat_search_z_style_wrapper',
858 api_name => 'open-ils.search.biblio.zstyle.staff',
860 signature => q/@see open-ils.search.biblio.multiclass/
863 sub cat_search_z_style_wrapper {
866 my $authtoken = shift;
869 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
871 my $ou = $cstore->request(
872 'open-ils.cstore.direct.actor.org_unit.search',
873 { parent_ou => undef }
876 my $result = { service => 'native-evergreen-catalog', records => [] };
877 my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
879 $$searchhash{searches}{title}{term} = $$args{search}{title} if $$args{search}{title};
880 $$searchhash{searches}{author}{term} = $$args{search}{author} if $$args{search}{author};
881 $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
882 $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
883 $$searchhash{searches}{'identifier|isbn'}{term} = $$args{search}{isbn} if $$args{search}{isbn};
884 $$searchhash{searches}{'identifier|issn'}{term} = $$args{search}{issn} if $$args{search}{issn};
885 $$searchhash{searches}{'identifier|upc'}{term} = $$args{search}{upc} if $$args{search}{upc};
887 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
888 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
889 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
890 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
892 my $method = 'open-ils.search.biblio.multiclass.staged';
893 $method .= '.staff' if $self->api_name =~ /staff$/;
895 my ($list) = $self->method_lookup($method)->run( $searchhash );
897 if ($list->{count} > 0 and @{$list->{ids}}) {
898 $result->{count} = $list->{count};
900 my $records = $cstore->request(
901 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
902 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
905 for my $rec ( @$records ) {
907 my $u = OpenILS::Utils::ModsParser->new();
908 $u->start_mods_batch( $rec->marc );
909 my $mods = $u->finish_mods_batch();
911 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
917 $cstore->disconnect();
921 # ----------------------------------------------------------------------------
922 # These are the main OPAC search methods
923 # ----------------------------------------------------------------------------
925 __PACKAGE__->register_method(
926 method => 'the_quest_for_knowledge',
927 api_name => 'open-ils.search.biblio.multiclass',
929 desc => "Performs a multi class biblio or metabib search",
932 desc => "A search hash with keys: "
933 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
934 . "See perldoc " . __PACKAGE__ . " for more detail",
938 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
943 desc => 'An object of the form: '
944 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
949 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
951 The search-hash argument can have the following elements:
953 searches: { "$class" : "$value", ...} [REQUIRED]
954 org_unit: The org id to focus the search at
955 depth : The org depth
956 limit : The search limit default: 10
957 offset : The search offset default: 0
958 format : The MARC format
959 sort : What field to sort the results on? [ author | title | pubdate ]
960 sort_dir: What direction do we sort? [ asc | desc ]
961 tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
962 will be tagged with an additional value ("1") as the last value in the record ID array for
963 each record. Requires the 'authtoken'
964 authtoken : Authentication token string; When actions are performed that require a user login
965 (e.g. tagging circulated records), the authentication token is required
967 The searches element is required, must have a hashref value, and the hashref must contain at least one
968 of the following classes as a key:
976 The value paired with a key is the associated search string.
978 The docache argument enables/disables searching and saving results in cache (default OFF).
980 The return object, if successful, will look like:
982 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
986 __PACKAGE__->register_method(
987 method => 'the_quest_for_knowledge',
988 api_name => 'open-ils.search.biblio.multiclass.staff',
989 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
991 __PACKAGE__->register_method(
992 method => 'the_quest_for_knowledge',
993 api_name => 'open-ils.search.metabib.multiclass',
994 signature => q/@see open-ils.search.biblio.multiclass/
996 __PACKAGE__->register_method(
997 method => 'the_quest_for_knowledge',
998 api_name => 'open-ils.search.metabib.multiclass.staff',
999 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1002 sub the_quest_for_knowledge {
1003 my( $self, $conn, $searchhash, $docache ) = @_;
1005 return { count => 0 } unless $searchhash and
1006 ref $searchhash->{searches} eq 'HASH';
1008 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
1012 if($self->api_name =~ /metabib/) {
1014 $method =~ s/biblio/metabib/o;
1017 # do some simple sanity checking
1018 if(!$searchhash->{searches} or
1019 ( !grep { /^(?:title|author|subject|series|keyword|identifier\|is[bs]n)/ } keys %{$searchhash->{searches}} ) ) {
1020 return { count => 0 };
1023 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
1024 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
1025 my $end = $offset + $limit - 1;
1027 my $maxlimit = 5000;
1028 $searchhash->{offset} = 0; # possible user value overwritten in hash
1029 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
1031 return { count => 0 } if $offset > $maxlimit;
1034 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
1035 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
1036 my $ckey = $pfx . md5_hex($method . $s);
1038 $logger->info("bib search for: $s");
1040 $searchhash->{limit} -= $offset;
1044 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1048 $method .= ".staff" if($self->api_name =~ /staff/);
1049 $method .= ".atomic";
1051 for (keys %$searchhash) {
1052 delete $$searchhash{$_}
1053 unless defined $$searchhash{$_};
1056 $result = $U->storagereq( $method, %$searchhash );
1060 $docache = 0; # results came FROM cache, so we don't write back
1063 return {count => 0} unless ($result && $$result[0]);
1067 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1070 # If we didn't get this data from the cache, put it into the cache
1071 # then return the correct offset of records
1072 $logger->debug("putting search cache $ckey\n");
1073 put_cache($ckey, $count, \@recs);
1077 # if we have the full set of data, trim out
1078 # the requested chunk based on limit and offset
1080 for ($offset..$end) {
1081 last unless $recs[$_];
1082 push(@t, $recs[$_]);
1087 return { ids => \@recs, count => $count };
1091 __PACKAGE__->register_method(
1092 method => 'staged_search',
1093 api_name => 'open-ils.search.biblio.multiclass.staged',
1095 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1096 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1099 desc => "A search hash with keys: "
1100 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1101 . "See perldoc " . __PACKAGE__ . " for more detail",
1105 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1110 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1111 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1116 __PACKAGE__->register_method(
1117 method => 'staged_search',
1118 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1119 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1121 __PACKAGE__->register_method(
1122 method => 'staged_search',
1123 api_name => 'open-ils.search.metabib.multiclass.staged',
1124 signature => q/@see open-ils.search.biblio.multiclass.staged/
1126 __PACKAGE__->register_method(
1127 method => 'staged_search',
1128 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1129 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1132 my $estimation_strategy;
1134 my($self, $conn, $search_hash, $docache) = @_;
1136 my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1138 my $method = $IAmMetabib?
1139 'open-ils.storage.metabib.multiclass.staged.search_fts':
1140 'open-ils.storage.biblio.multiclass.staged.search_fts';
1142 $method .= '.staff' if $self->api_name =~ /staff$/;
1143 $method .= '.atomic';
1145 if (!$search_hash->{query}) {
1146 return {count => 0} unless (
1148 $search_hash->{searches} and
1149 scalar( keys %{$search_hash->{searches}} ));
1152 my $search_duration;
1153 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1154 my $user_limit = $search_hash->{limit} || 10;
1155 my $ignore_facet_classes = $search_hash->{ignore_facet_classes};
1156 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1157 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1160 # we're grabbing results on a per-superpage basis, which means the
1161 # limit and offset should coincide with superpage boundaries
1162 $search_hash->{offset} = 0;
1163 $search_hash->{limit} = $superpage_size;
1165 # force a well-known check_limit
1166 $search_hash->{check_limit} = $superpage_size;
1167 # restrict total tested to superpage size * number of superpages
1168 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1170 # Set the configured estimation strategy, defaults to 'inclusion'.
1171 unless ($estimation_strategy) {
1172 $estimation_strategy = OpenSRF::Utils::SettingsClient
1175 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1178 $search_hash->{estimation_strategy} = $estimation_strategy;
1180 # pull any existing results from the cache
1181 my $key = search_cache_key($method, $search_hash);
1182 my $facet_key = $key.'_facets';
1183 my $cache_data = $cache->get_cache($key) || {};
1185 # First, we want to make sure that someone else isn't currently trying to perform exactly
1186 # this same search. The point is to allow just one instance of a search to fill the needs
1187 # of all concurrent, identical searches. This will avoid spammy searches killing the
1188 # database without requiring admins to start locking some IP addresses out entirely.
1190 # There's still a tiny race condition where 2 might run, but without sigificantly more code
1191 # and complexity, this is close to the best we can do.
1193 if ($cache_data->{running}) { # someone is already doing the search...
1194 my $stop_looping = time() + $cache_timeout;
1195 while ( sleep(1) and time() < $stop_looping ) { # sleep for a second ... maybe they'll finish
1196 $cache_data = $cache->get_cache($key) || {};
1197 last if (!$cache_data->{running});
1199 } elsif (!$cache_data->{0}) { # we're the first ... let's give it a try
1200 $cache->put_cache($key, { running => $$ }, $cache_timeout / 3);
1203 # keep retrieving results until we find enough to
1204 # fulfill the user-specified limit and offset
1205 my $all_results = [];
1206 my $page; # current superpage
1207 my $current_page_summary = {};
1208 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1211 for($page = 0; $page < $max_superpages; $page++) {
1213 my $data = $cache_data->{$page};
1217 $logger->debug("staged search: analyzing superpage $page");
1220 # this window of results is already cached
1221 $logger->debug("staged search: found cached results");
1222 $summary = $data->{summary};
1223 $results = $data->{results};
1226 # retrieve the window of results from the database
1227 $logger->debug("staged search: fetching results from the database");
1228 $search_hash->{skip_check} = $page * $superpage_size;
1229 $search_hash->{return_query} = $page == 0 ? 1 : 0;
1232 $results = $U->storagereq($method, %$search_hash);
1233 $search_duration = time - $start;
1234 $summary = shift(@$results) if $results;
1237 $logger->info("search timed out: duration=$search_duration: params=".
1238 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1239 return {count => 0};
1242 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1244 # Create backwards-compatible result structures
1246 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}, $_->{rel}, $_->{record}]} @$results];
1248 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}]} @$results];
1251 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1252 $results = [grep {defined $_->[0]} @$results];
1253 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1256 tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib)
1257 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1259 $current_page_summary = $summary;
1261 # add the new set of results to the set under construction
1262 push(@$all_results, @$results);
1264 my $current_count = scalar(@$all_results);
1266 if ($page == 0) { # all summaries are the same, just get the first
1267 for (keys %$summary) {
1268 $global_summary->{$_} = $summary->{$_};
1272 # we've found all the possible hits
1273 last if $current_count == $summary->{visible};
1275 # we've found enough results to satisfy the requested limit/offset
1276 last if $current_count >= ($user_limit + $user_offset);
1278 # we've scanned all possible hits
1279 last if($summary->{checked} < $superpage_size);
1282 # Let other backends grab our data now that we're done.
1283 $cache_data = $cache->get_cache($key);
1284 if ($$cache_data{running} and $$cache_data{running} == $$) {
1285 delete $$cache_data{running};
1286 $cache->put_cache($key, $cache_data, $cache_timeout);
1289 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1291 $conn->respond_complete(
1293 global_summary => $global_summary,
1294 count => $global_summary->{visible},
1295 core_limit => $search_hash->{core_limit},
1297 superpage_size => $search_hash->{check_limit},
1298 superpage_summary => $current_page_summary,
1299 facet_key => $facet_key,
1304 $logger->info("Completed canonicalized search is: $$global_summary{canonicalized_query}");
1306 return cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1309 sub fetch_display_fields {
1312 my $highlight_map = shift;
1316 $conn->respond_complete;
1320 my $hl_map_string = "";
1321 if (ref($highlight_map) =~ /HASH/) {
1322 for my $tsq (keys %$highlight_map) {
1323 my $field_list = join(',', @{$$highlight_map{$tsq}});
1324 $hl_map_string .= ' || ' if $hl_map_string;
1325 $hl_map_string .= "hstore(($tsq)\:\:TEXT,'$field_list')";
1329 my $e = new_editor();
1331 for my $record ( @records ) {
1332 next unless ($record && $hl_map_string);
1335 {from => ['search.highlight_display_fields', $record, $hl_map_string]}
1342 __PACKAGE__->register_method(
1343 method => 'fetch_display_fields',
1344 api_name => 'open-ils.search.fetch.metabib.display_field.highlight',
1349 sub tag_circulated_records {
1350 my ($auth, $results, $metabib) = @_;
1351 my $e = new_editor(authtoken => $auth);
1352 return $results unless $e->checkauth;
1355 select => { acn => [{ column => 'record', alias => 'tagme' }] },
1356 from => { auch => { acp => { join => 'acn' }} },
1357 where => { usr => $e->requestor->id },
1363 select => { mmrsm => [{ column => 'metarecord', alias => 'tagme' }] },
1365 where => { source => { in => $query } },
1370 # Give me the distinct set of bib records that exist in the user's visible circulation history
1371 my $circ_recs = $e->json_query( $query );
1373 # if the record appears in the circ history, push a 1 onto
1374 # the rec array structure to indicate truthiness
1375 for my $rec (@$results) {
1376 push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1382 # creates a unique token to represent the query in the cache
1383 sub search_cache_key {
1385 my $search_hash = shift;
1387 for my $key (sort keys %$search_hash) {
1388 push(@sorted, ($key => $$search_hash{$key}))
1389 unless $key eq 'limit' or
1391 $key eq 'skip_check';
1393 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1394 return $pfx . md5_hex($method . $s);
1397 sub retrieve_cached_facets {
1403 return undef unless ($key and $key =~ /_facets$/);
1406 local $SIG{ALRM} = sub {die};
1407 alarm(10); # we'll sleep for as much as 10s
1409 die if $cache->get_cache($key . '_COMPLETE');
1410 } while (sleep(0.05));
1415 my $blob = $cache->get_cache($key) || {};
1419 for my $f ( keys %$blob ) {
1420 my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1421 @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1422 for my $s ( @sorted ) {
1423 my ($k) = keys(%$s);
1424 my ($v) = values(%$s);
1425 $$facets{$f}{$k} = $v;
1435 __PACKAGE__->register_method(
1436 method => "retrieve_cached_facets",
1437 api_name => "open-ils.search.facet_cache.retrieve",
1439 desc => 'Returns facet data derived from a specific search based on a key '.
1440 'generated by open-ils.search.biblio.multiclass.staged and friends.',
1443 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1448 desc => 'Two level hash of facet values. Top level key is the facet id defined on the config.metabib_field table. '.
1449 'Second level key is a string facet value. Datum attached to each facet value is the number of distinct records, '.
1450 'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1451 'facet retrieval. These counts are calculated for all superpages that have been checked for visibility.',
1459 # add facets for this search to the facet cache
1460 my($key, $results, $metabib, $ignore) = @_;
1461 my $data = $cache->get_cache($key);
1464 return undef unless (@$results);
1466 my $facets_function = $metabib ? 'search.facets_for_metarecord_set'
1467 : 'search.facets_for_record_set';
1468 my $results_str = '{' . join(',', @$results) . '}';
1469 my $ignore_str = ref($ignore) ? '{' . join(',', @$ignore) . '}'
1472 from => [ $facets_function, $ignore_str, $results_str ]
1475 my $facets = OpenILS::Utils::CStoreEditor->new->json_query($query, {substream => 1});
1477 for my $facet (@$facets) {
1478 next unless ($facet->{value});
1479 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1482 $logger->info("facet compilation: cached with key=$key");
1484 $cache->put_cache($key, $data, $cache_timeout);
1485 $cache->put_cache($key.'_COMPLETE', 1, $cache_timeout);
1488 sub cache_staged_search_page {
1489 # puts this set of results into the cache
1490 my($key, $page, $summary, $results) = @_;
1491 my $data = $cache->get_cache($key);
1494 summary => $summary,
1498 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1499 ($summary->{estimated_hit_count} || "none") .
1500 ", visible=" . ($summary->{visible} || "none")
1503 $cache->put_cache($key, $data, $cache_timeout);
1511 my $start = $offset;
1512 my $end = $offset + $limit - 1;
1514 $logger->debug("searching cache for $key : $start..$end\n");
1516 return undef unless $cache;
1517 my $data = $cache->get_cache($key);
1519 return undef unless $data;
1521 my $count = $data->[0];
1524 return undef unless $offset < $count;
1527 for( my $i = $offset; $i <= $end; $i++ ) {
1528 last unless my $d = $$data[$i];
1529 push( @result, $d );
1532 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1539 my( $key, $count, $data ) = @_;
1540 return undef unless $cache;
1541 $logger->debug("search_cache putting ".
1542 scalar(@$data)." items at key $key with timeout $cache_timeout");
1543 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1547 __PACKAGE__->register_method(
1548 method => "biblio_mrid_to_modsbatch_batch",
1549 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1552 sub biblio_mrid_to_modsbatch_batch {
1553 my( $self, $client, $mrids) = @_;
1554 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1556 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1557 for my $id (@$mrids) {
1558 next unless defined $id;
1559 my ($m) = $method->run($id);
1566 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1567 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1569 __PACKAGE__->register_method(
1570 method => "biblio_mrid_to_modsbatch",
1573 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1574 . "As usual, the .staff version of this method will include otherwise hidden records.",
1576 { desc => 'Metarecord ID', type => 'number' },
1577 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1580 desc => 'MVR Object, event on error',
1586 sub biblio_mrid_to_modsbatch {
1587 my( $self, $client, $mrid, $args) = @_;
1589 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1591 my ($mr, $evt) = _grab_metarecord($mrid);
1592 return $evt unless $mr;
1594 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1595 biblio_mrid_make_modsbatch($self, $client, $mr);
1597 return $mvr unless ref($args);
1599 # Here we find the lead record appropriate for the given filters
1600 # and use that for the title and author of the metarecord
1601 my $format = $$args{format};
1602 my $org = $$args{org};
1603 my $depth = $$args{depth};
1605 return $mvr unless $format or $org or $depth;
1607 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1608 $method = "$method.staff" if $self->api_name =~ /staff/o;
1610 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1612 if( my $mods = $U->record_to_mvr($rec) ) {
1614 $mvr->title( $mods->title );
1615 $mvr->author($mods->author);
1616 $logger->debug("mods_slim updating title and ".
1617 "author in mvr with ".$mods->title." : ".$mods->author);
1623 # converts a metarecord to an mvr
1626 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1627 return Fieldmapper::metabib::virtual_record->new($perl);
1630 # checks to see if a metarecord has mods, if so returns true;
1632 __PACKAGE__->register_method(
1633 method => "biblio_mrid_check_mvr",
1634 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1635 notes => "Takes a metarecord ID or a metarecord object and returns true "
1636 . "if the metarecord already has an mvr associated with it."
1639 sub biblio_mrid_check_mvr {
1640 my( $self, $client, $mrid ) = @_;
1644 if(ref($mrid)) { $mr = $mrid; }
1645 else { ($mr, $evt) = _grab_metarecord($mrid); }
1646 return $evt if $evt;
1648 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1650 return _mr_to_mvr($mr) if $mr->mods();
1654 sub _grab_metarecord {
1656 my $e = new_editor();
1657 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1662 __PACKAGE__->register_method(
1663 method => "biblio_mrid_make_modsbatch",
1664 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1665 notes => "Takes either a metarecord ID or a metarecord object. "
1666 . "Forces the creations of an mvr for the given metarecord. "
1667 . "The created mvr is returned."
1670 sub biblio_mrid_make_modsbatch {
1671 my( $self, $client, $mrid ) = @_;
1673 my $e = new_editor();
1680 $mr = $e->retrieve_metabib_metarecord($mrid)
1681 or return $e->event;
1684 my $masterid = $mr->master_record;
1685 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1687 my $ids = $U->storagereq(
1688 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1689 return undef unless @$ids;
1691 my $master = $e->retrieve_biblio_record_entry($masterid)
1692 or return $e->event;
1694 # start the mods batch
1695 my $u = OpenILS::Utils::ModsParser->new();
1696 $u->start_mods_batch( $master->marc );
1698 # grab all of the sub-records and shove them into the batch
1699 my @ids = grep { $_ ne $masterid } @$ids;
1700 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1705 my $r = $e->retrieve_biblio_record_entry($i);
1706 push( @$subrecs, $r ) if $r;
1711 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1712 $u->push_mods_batch( $_->marc ) if $_->marc;
1716 # finish up and send to the client
1717 my $mods = $u->finish_mods_batch();
1718 $mods->doc_id($mrid);
1719 $client->respond_complete($mods);
1722 # now update the mods string in the db
1723 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1726 $e = new_editor(xact => 1);
1727 $e->update_metabib_metarecord($mr)
1728 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1735 # converts a mr id into a list of record ids
1737 foreach (qw/open-ils.search.biblio.metarecord_to_records
1738 open-ils.search.biblio.metarecord_to_records.staff/)
1740 __PACKAGE__->register_method(
1741 method => "biblio_mrid_to_record_ids",
1744 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1745 . "As usual, the .staff version of this method will include otherwise hidden records.",
1747 { desc => 'Metarecord ID', type => 'number' },
1748 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1751 desc => 'Results object like {count => $i, ids =>[...]}',
1759 sub biblio_mrid_to_record_ids {
1760 my( $self, $client, $mrid, $args ) = @_;
1762 my $format = $$args{format};
1763 my $org = $$args{org};
1764 my $depth = $$args{depth};
1766 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1767 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1768 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1770 return { count => scalar(@$recs), ids => $recs };
1774 __PACKAGE__->register_method(
1775 method => "biblio_record_to_marc_html",
1776 api_name => "open-ils.search.biblio.record.html"
1779 __PACKAGE__->register_method(
1780 method => "biblio_record_to_marc_html",
1781 api_name => "open-ils.search.authority.to_html"
1784 # Persistent parsers and setting objects
1785 my $parser = XML::LibXML->new();
1786 my $xslt = XML::LibXSLT->new();
1788 my $slim_marc_sheet;
1789 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1791 sub biblio_record_to_marc_html {
1792 my($self, $client, $recordid, $slim, $marcxml) = @_;
1795 my $dir = $settings_client->config_value("dirs", "xsl");
1798 unless($slim_marc_sheet) {
1799 my $xsl = $settings_client->config_value(
1800 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1802 $xsl = $parser->parse_file("$dir/$xsl");
1803 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1806 $sheet = $slim_marc_sheet;
1810 unless($marc_sheet) {
1811 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1812 my $xsl = $settings_client->config_value(
1813 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1814 $xsl = $parser->parse_file("$dir/$xsl");
1815 $marc_sheet = $xslt->parse_stylesheet($xsl);
1817 $sheet = $marc_sheet;
1822 my $e = new_editor();
1823 if($self->api_name =~ /authority/) {
1824 $record = $e->retrieve_authority_record_entry($recordid)
1825 or return $e->event;
1827 $record = $e->retrieve_biblio_record_entry($recordid)
1828 or return $e->event;
1830 $marcxml = $record->marc;
1833 my $xmldoc = $parser->parse_string($marcxml);
1834 my $html = $sheet->transform($xmldoc);
1835 return $html->documentElement->toString();
1838 __PACKAGE__->register_method(
1839 method => "format_biblio_record_entry",
1840 api_name => "open-ils.search.biblio.record.print",
1842 desc => 'Returns a printable version of the specified bib record',
1844 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1847 desc => q/An action_trigger.event object or error event./,
1852 __PACKAGE__->register_method(
1853 method => "format_biblio_record_entry",
1854 api_name => "open-ils.search.biblio.record.email",
1856 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1858 { desc => 'Authentication token', type => 'string'},
1859 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1862 desc => q/Undefined on success, otherwise an error event./,
1868 sub format_biblio_record_entry {
1869 my($self, $conn, $arg1, $arg2) = @_;
1871 my $for_print = ($self->api_name =~ /print/);
1872 my $for_email = ($self->api_name =~ /email/);
1874 my $e; my $auth; my $bib_id; my $context_org;
1878 $context_org = $arg2 || $U->get_org_tree->id;
1879 $e = new_editor(xact => 1);
1880 } elsif ($for_email) {
1883 $e = new_editor(authtoken => $auth, xact => 1);
1884 return $e->die_event unless $e->checkauth;
1885 $context_org = $e->requestor->home_ou;
1889 if (ref $bib_id ne 'ARRAY') {
1890 $bib_ids = [ $bib_id ];
1895 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1896 $bucket->btype('temp');
1897 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1899 $bucket->owner($e->requestor)
1903 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1905 for my $id (@$bib_ids) {
1907 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1909 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1910 $bucket_entry->target_biblio_record_entry($bib);
1911 $bucket_entry->bucket($bucket_obj->id);
1912 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1919 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1921 } elsif ($for_email) {
1923 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1930 __PACKAGE__->register_method(
1931 method => "retrieve_all_copy_statuses",
1932 api_name => "open-ils.search.config.copy_status.retrieve.all"
1935 sub retrieve_all_copy_statuses {
1936 my( $self, $client ) = @_;
1937 return new_editor()->retrieve_all_config_copy_status();
1941 __PACKAGE__->register_method(
1942 method => "copy_counts_per_org",
1943 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1946 __PACKAGE__->register_method(
1947 method => "copy_counts_per_org",
1948 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1951 sub copy_counts_per_org {
1952 my( $self, $client, $record_id ) = @_;
1954 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1956 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1957 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1959 my $counts = $apputils->simple_scalar_request(
1960 "open-ils.storage", $method, $record_id );
1962 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1967 __PACKAGE__->register_method(
1968 method => "copy_count_summary",
1969 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1970 notes => "returns an array of these: "
1971 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
1972 . "where statusx is a copy status name. The statuses are sorted by ID.",
1976 sub copy_count_summary {
1977 my( $self, $client, $rid, $org, $depth ) = @_;
1980 my $data = $U->storagereq(
1981 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1984 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
1986 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
1990 __PACKAGE__->register_method(
1991 method => "copy_location_count_summary",
1992 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1993 notes => "returns an array of these: "
1994 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
1995 . "where statusx is a copy status name. The statuses are sorted by ID.",
1998 sub copy_location_count_summary {
1999 my( $self, $client, $rid, $org, $depth ) = @_;
2002 my $data = $U->storagereq(
2003 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2006 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2008 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2010 || $a->[4] cmp $b->[4]
2014 __PACKAGE__->register_method(
2015 method => "copy_count_location_summary",
2016 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2017 notes => "returns an array of these: "
2018 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2019 . "where statusx is a copy status name. The statuses are sorted by ID."
2022 sub copy_count_location_summary {
2023 my( $self, $client, $rid, $org, $depth ) = @_;
2026 my $data = $U->storagereq(
2027 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2029 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2031 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2036 foreach (qw/open-ils.search.biblio.marc
2037 open-ils.search.biblio.marc.staff/)
2039 __PACKAGE__->register_method(
2040 method => "marc_search",
2043 desc => 'Fetch biblio IDs based on MARC record criteria. '
2044 . 'As usual, the .staff version of the search includes otherwise hidden records',
2047 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2048 'See perldoc ' . __PACKAGE__ . ' for more detail.',
2051 {desc => 'timeout (optional)', type => 'number'}
2054 desc => 'Results object like: { "count": $i, "ids": [...] }',
2061 =head3 open-ils.search.biblio.marc (arghash, timeout)
2063 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
2065 searches: complex query object (required)
2066 org_unit: The org ID to focus the search at
2067 depth : The org depth
2068 limit : integer search limit default: 10
2069 offset : integer search offset default: 0
2070 sort : What field to sort the results on? [ author | title | pubdate ]
2071 sort_dir: In what direction do we sort? [ asc | desc ]
2073 Additional keys to refine search criteria:
2076 language : Language (code)
2077 lit_form : Literary form
2078 item_form: Item form
2079 item_type: Item type
2080 format : The MARC format
2082 Please note that the specific strings to be used in the "addtional keys" will be entirely
2083 dependent on your loaded data.
2085 All keys except "searches" are optional.
2086 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
2088 For example, an arg hash might look like:
2110 The arghash is eventually passed to the SRF call:
2111 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2113 Presently, search uses the cache unconditionally.
2117 # FIXME: that example above isn't actually tested.
2118 # FIXME: sort and limit added. item_type not tested yet.
2119 # TODO: docache option?
2121 my( $self, $conn, $args, $timeout ) = @_;
2123 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2124 $method .= ".staff" if $self->api_name =~ /staff/;
2125 $method .= ".atomic";
2127 my $limit = $args->{limit} || 10;
2128 my $offset = $args->{offset} || 0;
2130 # allow caller to pass in a call timeout since MARC searches
2131 # can take longer than the default 60-second timeout.
2132 # Default to 2 mins. Arbitrarily cap at 5 mins.
2133 $timeout = 120 if !$timeout or $timeout > 300;
2136 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2137 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2139 my $recs = search_cache($ckey, $offset, $limit);
2143 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2144 my $req = $ses->request($method, %$args);
2145 my $resp = $req->recv($timeout);
2147 if($resp and $recs = $resp->content) {
2148 put_cache($ckey, scalar(@$recs), $recs);
2157 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2158 my @recs = map { $_->[0] } @$recs;
2160 return { ids => \@recs, count => $count };
2164 foreach my $isbn_method (qw/
2165 open-ils.search.biblio.isbn
2166 open-ils.search.biblio.isbn.staff
2168 __PACKAGE__->register_method(
2169 method => "biblio_search_isbn",
2170 api_name => $isbn_method,
2172 desc => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2174 {desc => 'ISBN', type => 'string'}
2177 desc => 'Results object like: { "count": $i, "ids": [...] }',
2184 sub biblio_search_isbn {
2185 my( $self, $client, $isbn ) = @_;
2186 $logger->debug("Searching ISBN $isbn");
2187 # the previous implementation of this method was essentially unlimited,
2188 # so we will set our limit very high and let multiclass.query provide any
2190 # XXX: if making this unlimited is deemed important, we might consider
2191 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2192 # which is functionally deprecated at this point, or a custom call to
2193 # 'open-ils.storage.biblio.multiclass.search_fts'
2195 my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2196 if ($self->api_name =~ m/.staff$/) {
2197 $isbn_method .= '.staff';
2200 my $method = $self->method_lookup($isbn_method);
2201 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2202 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2203 return { ids => \@recs, count => $search_result->{'count'} };
2206 __PACKAGE__->register_method(
2207 method => "biblio_search_isbn_batch",
2208 api_name => "open-ils.search.biblio.isbn_list",
2211 # XXX: see biblio_search_isbn() for note concerning 'limit'
2212 sub biblio_search_isbn_batch {
2213 my( $self, $client, $isbn_list ) = @_;
2214 $logger->debug("Searching ISBNs @$isbn_list");
2215 my @recs = (); my %rec_set = ();
2216 my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2217 foreach my $isbn ( @$isbn_list ) {
2218 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2219 my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2220 foreach my $rec (@recs_subset) {
2221 if (! $rec_set{ $rec }) {
2222 $rec_set{ $rec } = 1;
2227 return { ids => \@recs, count => scalar(@recs) };
2230 foreach my $issn_method (qw/
2231 open-ils.search.biblio.issn
2232 open-ils.search.biblio.issn.staff
2234 __PACKAGE__->register_method(
2235 method => "biblio_search_issn",
2236 api_name => $issn_method,
2238 desc => 'Retrieve biblio IDs for a given ISSN',
2240 {desc => 'ISBN', type => 'string'}
2243 desc => 'Results object like: { "count": $i, "ids": [...] }',
2250 sub biblio_search_issn {
2251 my( $self, $client, $issn ) = @_;
2252 $logger->debug("Searching ISSN $issn");
2253 # the previous implementation of this method was essentially unlimited,
2254 # so we will set our limit very high and let multiclass.query provide any
2256 # XXX: if making this unlimited is deemed important, we might consider
2257 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2258 # which is functionally deprecated at this point, or a custom call to
2259 # 'open-ils.storage.biblio.multiclass.search_fts'
2261 my $issn_method = 'open-ils.search.biblio.multiclass.query';
2262 if ($self->api_name =~ m/.staff$/) {
2263 $issn_method .= '.staff';
2266 my $method = $self->method_lookup($issn_method);
2267 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2268 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2269 return { ids => \@recs, count => $search_result->{'count'} };
2273 __PACKAGE__->register_method(
2274 method => "fetch_mods_by_copy",
2275 api_name => "open-ils.search.biblio.mods_from_copy",
2278 desc => 'Retrieve MODS record given an attached copy ID',
2280 { desc => 'Copy ID', type => 'number' }
2283 desc => 'MODS record, event on error or uncataloged item'
2288 sub fetch_mods_by_copy {
2289 my( $self, $client, $copyid ) = @_;
2290 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2291 return $evt if $evt;
2292 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2293 return $apputils->record_to_mvr($record);
2297 # -------------------------------------------------------------------------------------
2299 __PACKAGE__->register_method(
2300 method => "cn_browse",
2301 api_name => "open-ils.search.callnumber.browse.target",
2302 notes => "Starts a callnumber browse"
2305 __PACKAGE__->register_method(
2306 method => "cn_browse",
2307 api_name => "open-ils.search.callnumber.browse.page_up",
2308 notes => "Returns the previous page of callnumbers",
2311 __PACKAGE__->register_method(
2312 method => "cn_browse",
2313 api_name => "open-ils.search.callnumber.browse.page_down",
2314 notes => "Returns the next page of callnumbers",
2318 # RETURNS array of arrays like so: label, owning_lib, record, id
2320 my( $self, $client, @params ) = @_;
2323 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2324 if( $self->api_name =~ /target/ );
2325 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2326 if( $self->api_name =~ /page_up/ );
2327 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2328 if( $self->api_name =~ /page_down/ );
2330 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2332 # -------------------------------------------------------------------------------------
2334 __PACKAGE__->register_method(
2335 method => "fetch_cn",
2336 api_name => "open-ils.search.callnumber.retrieve",
2338 notes => "retrieves a callnumber based on ID",
2342 my( $self, $client, $id ) = @_;
2344 my $e = new_editor();
2345 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2346 return $evt if $evt;
2350 __PACKAGE__->register_method(
2351 method => "fetch_fleshed_cn",
2352 api_name => "open-ils.search.callnumber.fleshed.retrieve",
2354 notes => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2357 sub fetch_fleshed_cn {
2358 my( $self, $client, $id ) = @_;
2360 my $e = new_editor();
2361 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2362 return $evt if $evt;
2367 __PACKAGE__->register_method(
2368 method => "fetch_copy_by_cn",
2369 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2371 Returns an array of copy ID's by callnumber ID
2372 @param cnid The callnumber ID
2373 @return An array of copy IDs
2377 sub fetch_copy_by_cn {
2378 my( $self, $conn, $cnid ) = @_;
2379 return $U->cstorereq(
2380 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2381 { call_number => $cnid, deleted => 'f' } );
2384 __PACKAGE__->register_method(
2385 method => 'fetch_cn_by_info',
2386 api_name => 'open-ils.search.call_number.retrieve_by_info',
2388 @param label The callnumber label
2389 @param record The record the cn is attached to
2390 @param org The owning library of the cn
2391 @return The callnumber object
2396 sub fetch_cn_by_info {
2397 my( $self, $conn, $label, $record, $org ) = @_;
2398 return $U->cstorereq(
2399 'open-ils.cstore.direct.asset.call_number.search',
2400 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2405 __PACKAGE__->register_method(
2406 method => 'bib_extras',
2407 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2410 __PACKAGE__->register_method(
2411 method => 'bib_extras',
2412 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2413 ctype => 'item_form'
2415 __PACKAGE__->register_method(
2416 method => 'bib_extras',
2417 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2418 ctype => 'item_type',
2420 __PACKAGE__->register_method(
2421 method => 'bib_extras',
2422 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2423 ctype => 'bib_level'
2425 __PACKAGE__->register_method(
2426 method => 'bib_extras',
2427 api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2433 $logger->warn("deprecation warning: " .$self->api_name);
2435 my $e = new_editor();
2437 my $ctype = $self->{ctype};
2438 my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2441 for my $ccvm (@$ccvms) {
2442 my $obj = "Fieldmapper::config::${ctype}_map"->new;
2443 $obj->value($ccvm->value);
2444 $obj->code($ccvm->code);
2445 $obj->description($ccvm->description) if $obj->can('description');
2454 __PACKAGE__->register_method(
2455 method => 'fetch_slim_record',
2456 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2458 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2460 { desc => 'Array of Record IDs', type => 'array' }
2463 desc => 'Array of biblio records, event on error'
2468 sub fetch_slim_record {
2469 my( $self, $conn, $ids ) = @_;
2471 my $editor = new_editor();
2474 return $editor->event unless
2475 my $r = $editor->retrieve_biblio_record_entry($_);
2482 __PACKAGE__->register_method(
2483 method => 'rec_hold_parts',
2484 api_name => 'open-ils.search.biblio.record_hold_parts',
2486 Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2490 sub rec_hold_parts {
2491 my( $self, $conn, $args ) = @_;
2493 my $rec = $$args{record};
2494 my $mrec = $$args{metarecord};
2495 my $pickup_lib = $$args{pickup_lib};
2496 my $e = new_editor();
2499 select => {bmp => ['id', 'label']},
2504 select => {'acpm' => ['part']},
2505 from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2507 '+acp' => {'deleted' => 'f'},
2508 '+bre' => {id => $rec}
2515 order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2518 if(defined $pickup_lib) {
2519 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2520 if($hard_boundary) {
2521 my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2522 $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2526 return $e->json_query($query);
2532 __PACKAGE__->register_method(
2533 method => 'rec_to_mr_rec_descriptors',
2534 api_name => 'open-ils.search.metabib.record_to_descriptors',
2536 specialized method...
2537 Given a biblio record id or a metarecord id,
2538 this returns a list of metabib.record_descriptor
2539 objects that live within the same metarecord
2540 @param args Object of args including:
2544 sub rec_to_mr_rec_descriptors {
2545 my( $self, $conn, $args ) = @_;
2547 my $rec = $$args{record};
2548 my $mrec = $$args{metarecord};
2549 my $item_forms = $$args{item_forms};
2550 my $item_types = $$args{item_types};
2551 my $item_lang = $$args{item_lang};
2552 my $pickup_lib = $$args{pickup_lib};
2554 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2556 my $e = new_editor();
2560 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2561 return $e->event unless @$map;
2562 $mrec = $$map[0]->metarecord;
2565 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2566 return $e->event unless @$recs;
2568 my @recs = map { $_->source } @$recs;
2569 my $search = { record => \@recs };
2570 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2571 $search->{item_type} = $item_types if $item_types and @$item_types;
2572 $search->{item_lang} = $item_lang if $item_lang;
2574 my $desc = $e->search_metabib_record_descriptor($search);
2578 select => { 'bre' => ['id'] },
2583 'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2589 '+bre' => { id => \@recs },
2594 "+ccs" => { holdable => 't' },
2595 "+acpl" => { holdable => 't', deleted => 'f' }
2599 if ($hard_boundary) { # 0 (or "top") is the same as no setting
2600 my $orgs = $e->json_query(
2601 { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2602 ) or return $e->die_event;
2604 $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2607 my $good_records = $e->json_query($query) or return $e->die_event;
2610 for my $d (@$desc) {
2611 if ( grep { $d->record == $_->{id} } @$good_records ) {
2618 return { metarecord => $mrec, descriptors => $desc };
2622 __PACKAGE__->register_method(
2623 method => 'fetch_age_protect',
2624 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2627 sub fetch_age_protect {
2628 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2632 __PACKAGE__->register_method(
2633 method => 'copies_by_cn_label',
2634 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2637 __PACKAGE__->register_method(
2638 method => 'copies_by_cn_label',
2639 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2642 sub copies_by_cn_label {
2643 my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2644 my $e = new_editor();
2645 my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2646 my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2647 my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2648 return [] unless @$cns;
2650 # show all non-deleted copies in the staff client ...
2651 if ($self->api_name =~ /staff$/o) {
2652 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2655 # ... otherwise, grab the copies ...
2656 my $copies = $e->search_asset_copy(
2657 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2658 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2662 # ... and test for location and status visibility
2663 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2666 __PACKAGE__->register_method(
2667 method => 'bib_copies',
2668 api_name => 'open-ils.search.bib.copies',
2671 __PACKAGE__->register_method(
2672 method => 'bib_copies',
2673 api_name => 'open-ils.search.bib.copies.staff',
2678 my ($self, $client, $rec_id, $org, $depth, $limit, $offset, $pref_ou) = @_;
2679 my $is_staff = ($self->api_name =~ /staff/);
2681 my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
2682 my $req = $cstore->request(
2683 'open-ils.cstore.json_query', mk_copy_query(
2684 $rec_id, $org, $depth, $limit, $offset, $pref_ou, $is_staff));
2687 while ($resp = $req->recv) {
2688 $client->respond($resp->content);
2694 # TODO: this comes almost directly from WWW/EGCatLoader/Record.pm
2700 my $copy_limit = shift;
2701 my $copy_offset = shift;
2702 my $pref_ou = shift;
2703 my $is_staff = shift;
2705 my $query = $U->basic_opac_copy_query(
2706 $rec_id, undef, undef, $copy_limit, $copy_offset, $is_staff
2709 if ($org) { # TODO: root org test
2710 # no need to add the org join filter if we're not actually filtering
2711 $query->{from}->{acp}->[1] = { aou => {
2717 select => {aou => [{
2719 transform => 'actor.org_unit_descendants',
2720 result_field => 'id',
2724 where => {id => $org}
2731 # Unsure if we want these in the shared function, leaving here for now
2732 unshift(@{$query->{order_by}},
2733 { class => "aou", field => 'id',
2734 transform => 'evergreen.rank_ou', params => [$org, $pref_ou]
2737 push(@{$query->{order_by}},
2738 { class => "acp", field => 'id',
2739 transform => 'evergreen.rank_cp'