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_";
39 my $max_search_results;
42 $cache = OpenSRF::Utils::Cache->new('global');
43 my $sclient = OpenSRF::Utils::SettingsClient->new();
44 $cache_timeout = $sclient->config_value(
45 "apps", "open-ils.search", "app_settings", "cache_timeout" ) || 300;
47 my $superpage_size = $sclient->config_value(
48 "apps", "open-ils.search", "app_settings", "superpage_size" ) || 500;
50 my $max_superpages = $sclient->config_value(
51 "apps", "open-ils.search", "app_settings", "max_superpages" ) || 20;
53 $max_search_results = $sclient->config_value(
54 "apps", "open-ils.search", "app_settings", "max_search_results" ) || ($superpage_size * $max_superpages);
56 $logger->info("Search cache timeout is $cache_timeout, ".
57 " max_search_results is $max_search_results");
62 # ---------------------------------------------------------------------------
63 # takes a list of record id's and turns the docs into friendly
64 # mods structures. Creates one MODS structure for each doc id.
65 # ---------------------------------------------------------------------------
66 sub _records_to_mods {
72 my $session = OpenSRF::AppSession->create("open-ils.cstore");
73 my $request = $session->request(
74 "open-ils.cstore.direct.biblio.record_entry.search", { id => \@ids } );
76 while( my $resp = $request->recv ) {
77 my $content = $resp->content;
78 next if $content->id == OILS_PRECAT_RECORD;
79 my $u = OpenILS::Utils::ModsParser->new(); # FIXME: we really need a new parser for each object?
80 $u->start_mods_batch( $content->marc );
81 my $mods = $u->finish_mods_batch();
82 $mods->doc_id($content->id());
83 $mods->tcn($content->tcn_value);
87 $session->disconnect();
91 __PACKAGE__->register_method(
92 method => "record_id_to_mods",
93 api_name => "open-ils.search.biblio.record.mods.retrieve",
96 desc => "Provide ID, we provide the MODS object with copy count. "
97 . "Note: this method does NOT take an array of IDs like mods_slim.retrieve", # FIXME: do it here too
99 { desc => 'Record ID', type => 'number' }
102 desc => 'MODS object', type => 'object'
107 # converts a record into a mods object with copy counts attached
108 sub record_id_to_mods {
110 my( $self, $client, $org_id, $id ) = @_;
112 my $mods_list = _records_to_mods( $id );
113 my $mods_obj = $mods_list->[0];
114 my $cmethod = $self->method_lookup("open-ils.search.biblio.record.copy_count");
115 my ($count) = $cmethod->run($org_id, $id);
116 $mods_obj->copy_count($count);
123 __PACKAGE__->register_method(
124 method => "record_id_to_mods_slim",
125 api_name => "open-ils.search.biblio.record.mods_slim.retrieve",
129 desc => "Provide ID(s), we provide the MODS",
131 { desc => 'Record ID or array of IDs' }
134 desc => 'MODS object(s), event on error'
139 # converts a record into a mods object with NO copy counts attached
140 sub record_id_to_mods_slim {
141 my( $self, $client, $id ) = @_;
142 return undef unless defined $id;
144 if(ref($id) and ref($id) == 'ARRAY') {
145 return _records_to_mods( @$id );
147 my $mods_list = _records_to_mods( $id );
148 my $mods_obj = $mods_list->[0];
149 return OpenILS::Event->new('BIBLIO_RECORD_ENTRY_NOT_FOUND') unless $mods_obj;
155 __PACKAGE__->register_method(
156 method => "record_id_to_mods_slim_batch",
157 api_name => "open-ils.search.biblio.record.mods_slim.batch.retrieve",
160 sub record_id_to_mods_slim_batch {
161 my($self, $conn, $id_list) = @_;
162 $conn->respond(_records_to_mods($_)->[0]) for @$id_list;
167 # Returns the number of copies attached to a record based on org location
168 __PACKAGE__->register_method(
169 method => "record_id_to_copy_count",
170 api_name => "open-ils.search.biblio.record.copy_count",
172 desc => q/Returns a copy summary for the given record for the context org
173 unit and all ancestor org units/,
175 {desc => 'Context org unit id', type => 'number'},
176 {desc => 'Record ID', type => 'number'}
179 desc => q/summary object per org unit in the set, where the set
180 includes the context org unit and all parent org units.
181 Object includes the keys "transcendant", "count", "org_unit", "depth",
182 "unshadow", "available". Each is a count, except "org_unit" which is
183 the context org unit and "depth" which is the depth of the context org unit
190 __PACKAGE__->register_method(
191 method => "record_id_to_copy_count",
192 api_name => "open-ils.search.biblio.record.copy_count.staff",
195 desc => q/Returns a copy summary for the given record for the context org
196 unit and all ancestor org units/,
198 {desc => 'Context org unit id', type => 'number'},
199 {desc => 'Record ID', type => 'number'}
202 desc => q/summary object per org unit in the set, where the set
203 includes the context org unit and all parent org units.
204 Object includes the keys "transcendant", "count", "org_unit", "depth",
205 "unshadow", "available". Each is a count, except "org_unit" which is
206 the context org unit and "depth" which is the depth of the context org unit
213 __PACKAGE__->register_method(
214 method => "record_id_to_copy_count",
215 api_name => "open-ils.search.biblio.metarecord.copy_count",
217 desc => q/Returns a copy summary for the given record for the context org
218 unit and all ancestor org units/,
220 {desc => 'Context org unit id', type => 'number'},
221 {desc => 'Record ID', type => 'number'}
224 desc => q/summary object per org unit in the set, where the set
225 includes the context org unit and all parent org units.
226 Object includes the keys "transcendant", "count", "org_unit", "depth",
227 "unshadow", "available". Each is a count, except "org_unit" which is
228 the context org unit and "depth" which is the depth of the context org unit
235 __PACKAGE__->register_method(
236 method => "record_id_to_copy_count",
237 api_name => "open-ils.search.biblio.metarecord.copy_count.staff",
239 desc => q/Returns a copy summary for the given record for the context org
240 unit and all ancestor org units/,
242 {desc => 'Context org unit id', type => 'number'},
243 {desc => 'Record ID', type => 'number'}
246 desc => q/summary object per org unit in the set, where the set
247 includes the context org unit and all parent org units.
248 Object includes the keys "transcendant", "count", "org_unit", "depth",
249 "unshadow", "available". Each is a count, except "org_unit" which is
250 the context org unit and "depth" which is the depth of the context org
251 unit. "depth" is always -1 when the count from a lasso search is
252 performed, since depth doesn't mean anything in a lasso context.
259 sub record_id_to_copy_count {
260 my( $self, $client, $org_id, $record_id ) = @_;
262 return [] unless $record_id;
264 my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
265 my $staff = $self->api_name =~ /staff/ ? 't' : 'f';
267 my $data = $U->cstorereq(
268 "open-ils.cstore.json_query.atomic",
269 { from => ['asset.' . $key . '_copy_count' => $org_id => $record_id => $staff] }
273 for my $d ( @$data ) { # fix up the key name change required by stored-proc version
274 $$d{count} = delete $$d{visible};
278 return [ sort { $a->{depth} <=> $b->{depth} } @count ];
281 __PACKAGE__->register_method(
282 method => "record_has_holdable_copy",
283 api_name => "open-ils.search.biblio.record.has_holdable_copy",
285 desc => q/Returns a boolean indicating if a record has any holdable copies./,
287 {desc => 'Record ID', type => 'number'}
290 desc => q/bool indicating if the record has any holdable copies/,
296 __PACKAGE__->register_method(
297 method => "record_has_holdable_copy",
298 api_name => "open-ils.search.biblio.metarecord.has_holdable_copy",
300 desc => q/Returns a boolean indicating if a record has any holdable copies./,
302 {desc => 'Record ID', type => 'number'}
305 desc => q/bool indicating if the record has any holdable copies/,
311 sub record_has_holdable_copy {
312 my($self, $client, $record_id ) = @_;
314 return 0 unless $record_id;
316 my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
318 my $data = $U->cstorereq(
319 "open-ils.cstore.json_query.atomic",
320 { from => ['asset.' . $key . '_has_holdable_copy' => $record_id ] }
323 return ${@$data[0]}{'asset.' . $key . '_has_holdable_copy'} eq 't';
327 __PACKAGE__->register_method(
328 method => "biblio_search_tcn",
329 api_name => "open-ils.search.biblio.tcn",
332 desc => "Retrieve related record ID(s) given a TCN",
334 { desc => 'TCN', type => 'string' },
335 { desc => 'Flag indicating to include deleted records', type => 'string' }
338 desc => 'Results object like: { "count": $i, "ids": [...] }',
345 sub biblio_search_tcn {
347 my( $self, $client, $tcn, $include_deleted ) = @_;
349 $tcn =~ s/^\s+|\s+$//og;
351 my $e = new_editor();
352 my $search = {tcn_value => $tcn};
353 $search->{deleted} = 'f' unless $include_deleted;
354 my $recs = $e->search_biblio_record_entry( $search, {idlist =>1} );
356 return { count => scalar(@$recs), ids => $recs };
360 # --------------------------------------------------------------------------------
362 __PACKAGE__->register_method(
363 method => "biblio_barcode_to_copy",
364 api_name => "open-ils.search.asset.copy.find_by_barcode",
366 sub biblio_barcode_to_copy {
367 my( $self, $client, $barcode ) = @_;
368 my( $copy, $evt ) = $U->fetch_copy_by_barcode($barcode);
373 __PACKAGE__->register_method(
374 method => "biblio_id_to_copy",
375 api_name => "open-ils.search.asset.copy.batch.retrieve",
377 sub biblio_id_to_copy {
378 my( $self, $client, $ids ) = @_;
379 $logger->info("Fetching copies @$ids");
380 return $U->cstorereq(
381 "open-ils.cstore.direct.asset.copy.search.atomic", { id => $ids } );
385 __PACKAGE__->register_method(
386 method => "biblio_id_to_uris",
387 api_name=> "open-ils.search.asset.uri.retrieve_by_bib",
391 @param BibID Which bib record contains the URIs
392 @param OrgID Where to look for URIs
393 @param OrgDepth Range adjustment for OrgID
394 @return A stream or list of 'auri' objects
398 sub biblio_id_to_uris {
399 my( $self, $client, $bib, $org, $depth ) = @_;
400 die "Org ID required" unless defined($org);
401 die "Bib ID required" unless defined($bib);
404 push @params, $depth if (defined $depth);
406 my $ids = $U->cstorereq( "open-ils.cstore.json_query.atomic",
407 { select => { auri => [ 'id' ] },
411 field => 'call_number',
417 filter => { active => 't' }
428 select => { aou => [ { column => 'id', transform => 'actor.org_unit_descendants', params => \@params, result_field => 'id' } ] },
430 where => { id => $org },
440 my $uris = $U->cstorereq(
441 "open-ils.cstore.direct.asset.uri.search.atomic",
442 { id => [ map { (values %$_) } @$ids ] }
445 $client->respond($_) for (@$uris);
451 __PACKAGE__->register_method(
452 method => "copy_retrieve",
453 api_name => "open-ils.search.asset.copy.retrieve",
456 desc => 'Retrieve a copy object based on the Copy ID',
458 { desc => 'Copy ID', type => 'number'}
461 desc => 'Copy object, event on error'
467 my( $self, $client, $cid ) = @_;
468 my( $copy, $evt ) = $U->fetch_copy($cid);
469 return $evt || $copy;
472 __PACKAGE__->register_method(
473 method => "volume_retrieve",
474 api_name => "open-ils.search.asset.call_number.retrieve"
476 sub volume_retrieve {
477 my( $self, $client, $vid ) = @_;
478 my $e = new_editor();
479 my $vol = $e->retrieve_asset_call_number($vid) or return $e->event;
483 __PACKAGE__->register_method(
484 method => "fleshed_copy_retrieve_batch",
485 api_name => "open-ils.search.asset.copy.fleshed.batch.retrieve",
489 sub fleshed_copy_retrieve_batch {
490 my( $self, $client, $ids ) = @_;
491 $logger->info("Fetching fleshed copies @$ids");
492 return $U->cstorereq(
493 "open-ils.cstore.direct.asset.copy.search.atomic",
496 flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries parts / ] }
501 __PACKAGE__->register_method(
502 method => "fleshed_copy_retrieve",
503 api_name => "open-ils.search.asset.copy.fleshed.retrieve",
506 sub fleshed_copy_retrieve {
507 my( $self, $client, $id ) = @_;
508 my( $c, $e) = $U->fetch_fleshed_copy($id);
513 __PACKAGE__->register_method(
514 method => 'fleshed_by_barcode',
515 api_name => "open-ils.search.asset.copy.fleshed2.find_by_barcode",
518 sub fleshed_by_barcode {
519 my( $self, $conn, $barcode ) = @_;
520 my $e = new_editor();
521 my $copyid = $e->search_asset_copy(
522 {barcode => $barcode, deleted => 'f'}, {idlist=>1})->[0]
524 return fleshed_copy_retrieve2( $self, $conn, $copyid);
528 __PACKAGE__->register_method(
529 method => "fleshed_copy_retrieve2",
530 api_name => "open-ils.search.asset.copy.fleshed2.retrieve",
534 sub fleshed_copy_retrieve2 {
535 my( $self, $client, $id ) = @_;
536 my $e = new_editor();
537 my $copy = $e->retrieve_asset_copy(
544 qw/ location status stat_cat_entry_copy_maps notes age_protect parts peer_record_maps /
546 ascecm => [qw/ stat_cat stat_cat_entry /],
550 ) or return $e->event;
552 # For backwards compatibility
553 #$copy->stat_cat_entries($copy->stat_cat_entry_copy_maps);
555 if( $copy->status->id == OILS_COPY_STATUS_CHECKED_OUT ) {
557 $e->search_action_circulation(
559 { target_copy => $copy->id },
561 order_by => { circ => 'xact_start desc' },
573 __PACKAGE__->register_method(
574 method => 'flesh_copy_custom',
575 api_name => 'open-ils.search.asset.copy.fleshed.custom',
579 sub flesh_copy_custom {
580 my( $self, $conn, $copyid, $fields ) = @_;
581 my $e = new_editor();
582 my $copy = $e->retrieve_asset_copy(
592 ) or return $e->event;
597 __PACKAGE__->register_method(
598 method => "biblio_barcode_to_title",
599 api_name => "open-ils.search.biblio.find_by_barcode",
602 sub biblio_barcode_to_title {
603 my( $self, $client, $barcode ) = @_;
605 my $title = $apputils->simple_scalar_request(
607 "open-ils.storage.biblio.record_entry.retrieve_by_barcode", $barcode );
609 return { ids => [ $title->id ], count => 1 } if $title;
610 return { count => 0 };
613 __PACKAGE__->register_method(
614 method => 'title_id_by_item_barcode',
615 api_name => 'open-ils.search.bib_id.by_barcode',
618 desc => 'Retrieve bib record id associated with the copy identified by the given barcode',
620 { desc => 'Item barcode', type => 'string' }
623 desc => 'Bib record id.'
628 __PACKAGE__->register_method(
629 method => 'title_id_by_item_barcode',
630 api_name => 'open-ils.search.multi_home.bib_ids.by_barcode',
633 desc => 'Retrieve bib record ids associated with the copy identified by the given barcode. This includes peer bibs for Multi-Home items.',
635 { desc => 'Item barcode', type => 'string' }
638 desc => 'Array of bib record ids. First element is the native bib for the item.'
644 sub title_id_by_item_barcode {
645 my( $self, $conn, $barcode ) = @_;
646 my $e = new_editor();
647 my $copies = $e->search_asset_copy(
649 { deleted => 'f', barcode => $barcode },
653 acp => [ 'call_number' ],
660 return $e->event unless @$copies;
662 if( $self->api_name =~ /multi_home/ ) {
663 my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
665 { target_copy => $$copies[0]->id }
668 my @temp = map { $_->peer_record } @{ $multi_home_list };
669 unshift @temp, $$copies[0]->call_number->record->id;
672 return $$copies[0]->call_number->record->id;
676 __PACKAGE__->register_method(
677 method => 'find_peer_bibs',
678 api_name => 'open-ils.search.peer_bibs.test',
681 desc => 'Tests to see if the specified record is a peer record.',
683 { desc => 'Biblio record entry Id', type => 'number' }
686 desc => 'True if specified id can be found in biblio.peer_bib_copy_map.peer_record.',
692 __PACKAGE__->register_method(
693 method => 'find_peer_bibs',
694 api_name => 'open-ils.search.peer_bibs',
697 desc => 'Return acps and mvrs for multi-home items linked to specified peer record.',
699 { desc => 'Biblio record entry Id', type => 'number' }
702 desc => '{ records => Array of mvrs, items => array of acps }',
709 my( $self, $client, $doc_id ) = @_;
710 my $e = new_editor();
712 my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
714 { peer_record => $doc_id },
718 bpbcm => [ 'target_copy', 'peer_type' ],
719 acp => [ 'call_number', 'location', 'status', 'peer_record_maps' ]
725 if ($self->api_name =~ /test/) {
726 return scalar( @{$multi_home_list} ) > 0 ? 1 : 0;
729 if (scalar(@{$multi_home_list})==0) {
733 # create a unique hash of the primary record MVRs for foreign copies
734 # XXX PLEASE let's change to unAPI2 (supports foreign copies) in the TT opac?!?
736 ($_->target_copy->call_number->record, _records_to_mods( $_->target_copy->call_number->record )->[0])
739 # set the foreign_copy_maps field to an empty array
740 map { $rec_hash{$_}->foreign_copy_maps([]) } keys( %rec_hash );
742 # push the maps onto the correct MVRs
743 for (@$multi_home_list) {
745 @{$rec_hash{ $_->target_copy->call_number->record }->foreign_copy_maps()},
750 return [sort {$a->title cmp $b->title} values(%rec_hash)];
753 __PACKAGE__->register_method(
754 method => "biblio_copy_to_mods",
755 api_name => "open-ils.search.biblio.copy.mods.retrieve",
758 # takes a copy object and returns it fleshed mods object
759 sub biblio_copy_to_mods {
760 my( $self, $client, $copy ) = @_;
762 my $volume = $U->cstorereq(
763 "open-ils.cstore.direct.asset.call_number.retrieve",
764 $copy->call_number() );
766 my $mods = _records_to_mods($volume->record());
767 $mods = shift @$mods;
768 $volume->copies([$copy]);
769 push @{$mods->call_numbers()}, $volume;
777 OpenILS::Application::Search::Biblio
783 =head3 open-ils.search.biblio.multiclass.query (arghash, query, docache)
785 For arghash and docache, see B<open-ils.search.biblio.multiclass>.
787 The query argument is a string, but built like a hash with key: value pairs.
788 Recognized search keys include:
790 keyword (kw) - search keyword(s) *
791 author (au) - search author(s) *
792 name (au) - same as author *
793 title (ti) - search title *
794 subject (su) - search subject *
795 series (se) - search series *
796 lang - limit by language (specifiy multiple langs with lang:l1 lang:l2 ...)
797 site - search at specified org unit, corresponds to actor.org_unit.shortname
798 pref_ou - extend search to specified org unit, corresponds to actor.org_unit.shortname
799 sort - sort type (title, author, pubdate)
800 dir - sort direction (asc, desc)
801 available - if set to anything other than "false" or "0", limits to available items
803 * Searching keyword, author, title, subject, and series supports additional search
804 subclasses, specified with a "|". For example, C<title|proper:gone with the wind>.
806 For more, see B<config.metabib_field>.
810 foreach (qw/open-ils.search.biblio.multiclass.query
811 open-ils.search.biblio.multiclass.query.staff
812 open-ils.search.metabib.multiclass.query
813 open-ils.search.metabib.multiclass.query.staff/)
815 __PACKAGE__->register_method(
817 method => 'multiclass_query',
819 desc => 'Perform a search query. The .staff version of the call includes otherwise hidden hits.',
821 {name => 'arghash', desc => 'Arg hash (see open-ils.search.biblio.multiclass)', type => 'object'},
822 {name => 'query', desc => 'Raw human-readable query (see perldoc '. __PACKAGE__ .')', type => 'string'},
823 {name => 'docache', desc => 'Flag for caching (see open-ils.search.biblio.multiclass)', type => 'object'},
826 desc => 'Search results from query, like: { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
827 type => 'object', # TODO: update as miker's new elements are included
833 sub multiclass_query {
834 my($self, $conn, $arghash, $query, $docache) = @_;
836 $logger->debug("initial search query => $query");
837 my $orig_query = $query;
840 $query =~ s/^\s+//go;
842 # convert convenience classes (e.g. kw for keyword) to the full class name
843 # ensure that the convenience class isn't part of a word (e.g. 'playhouse')
844 $query =~ s/(^|\s)kw(:|\|)/$1keyword$2/go;
845 $query =~ s/(^|\s)ti(:|\|)/$1title$2/go;
846 $query =~ s/(^|\s)au(:|\|)/$1author$2/go;
847 $query =~ s/(^|\s)su(:|\|)/$1subject$2/go;
848 $query =~ s/(^|\s)se(:|\|)/$1series$2/go;
849 $query =~ s/(^|\s)name(:|\|)/$1author$2/og;
851 $logger->debug("cleansed query string => $query");
854 my $simple_class_re = qr/((?:\w+(?:\|\w+)?):[^:]+?)$/;
855 my $class_list_re = qr/(?:keyword|title|author|subject|series)/;
856 my $modifier_list_re = qr/(?:site|dir|sort|lang|available|preflib)/;
859 while ($query =~ s/$simple_class_re//so) {
862 my $where = index($qpart,':');
863 my $type = substr($qpart, 0, $where++);
864 my $value = substr($qpart, $where);
866 if ($type !~ /^(?:$class_list_re|$modifier_list_re)/o) {
867 $tmp_value = "$qpart $tmp_value";
871 if ($type =~ /$class_list_re/o ) {
872 $value .= $tmp_value;
876 next unless $type and $value;
878 $value =~ s/^\s*//og;
879 $value =~ s/\s*$//og;
880 $type = 'sort_dir' if $type eq 'dir';
882 if($type eq 'site') {
883 # 'site' is the org shortname. when using this, we also want
884 # to search at the requested org's depth
885 my $e = new_editor();
886 if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
887 $arghash->{org_unit} = $org->id if $org;
888 $arghash->{depth} = $e->retrieve_actor_org_unit_type($org->ou_type)->depth;
890 $logger->warn("'site:' query used on invalid org shortname: $value ... ignoring");
892 } elsif($type eq 'pref_ou') {
893 # 'pref_ou' is the preferred org shortname.
894 my $e = new_editor();
895 if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
896 $arghash->{pref_ou} = $org->id if $org;
898 $logger->warn("'pref_ou:' query used on invalid org shortname: $value ... ignoring");
901 } elsif($type eq 'available') {
903 $arghash->{available} = 1 unless $value eq 'false' or $value eq '0';
905 } elsif($type eq 'lang') {
906 # collect languages into an array of languages
907 $arghash->{language} = [] unless $arghash->{language};
908 push(@{$arghash->{language}}, $value);
910 } elsif($type =~ /^sort/o) {
911 # sort and sort_dir modifiers
912 $arghash->{$type} = $value;
915 # append the search term to the term under construction
916 $search->{$type} = {} unless $search->{$type};
917 $search->{$type}->{term} =
918 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $value" : $value;
922 $query .= " $tmp_value";
923 $query =~ s/\s+/ /go;
924 $query =~ s/^\s+//go;
925 $query =~ s/\s+$//go;
927 my $type = $arghash->{default_class} || 'keyword';
928 $type = ($type eq '-') ? 'keyword' : $type;
929 $type = ($type !~ /^(title|author|keyword|subject|series)(?:\|\w+)?$/o) ? 'keyword' : $type;
932 # This is the front part of the string before any special tokens were
933 # parsed OR colon-separated strings that do not denote a class.
934 # Add this data to the default search class
935 $search->{$type} = {} unless $search->{$type};
936 $search->{$type}->{term} =
937 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $query" : $query;
939 my $real_search = $arghash->{searches} = { $type => { term => $orig_query } };
941 # capture the original limit because the search method alters the limit internally
942 my $ol = $arghash->{limit};
944 my $sclient = OpenSRF::Utils::SettingsClient->new;
946 (my $method = $self->api_name) =~ s/\.query//o;
948 $method =~ s/multiclass/multiclass.staged/
949 if $sclient->config_value(apps => 'open-ils.search',
950 app_settings => 'use_staged_search') =~ /true/i;
952 # XXX This stops the session locale from doing the right thing.
953 # XXX Revisit this and have it translate to a lang instead of a locale.
954 #$arghash->{preferred_language} = $U->get_org_locale($arghash->{org_unit})
955 # unless $arghash->{preferred_language};
957 $method = $self->method_lookup($method);
958 my ($data) = $method->run($arghash, $docache);
960 $arghash->{searches} = $search if (!$data->{complex_query});
962 $arghash->{limit} = $ol if $ol;
963 $data->{compiled_search} = $arghash;
964 $data->{query} = $orig_query;
966 $logger->info("compiled search is " . OpenSRF::Utils::JSON->perl2JSON($arghash));
971 __PACKAGE__->register_method(
972 method => 'cat_search_z_style_wrapper',
973 api_name => 'open-ils.search.biblio.zstyle',
975 signature => q/@see open-ils.search.biblio.multiclass/
978 __PACKAGE__->register_method(
979 method => 'cat_search_z_style_wrapper',
980 api_name => 'open-ils.search.biblio.zstyle.staff',
982 signature => q/@see open-ils.search.biblio.multiclass/
985 sub cat_search_z_style_wrapper {
988 my $authtoken = shift;
991 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
993 my $ou = $cstore->request(
994 'open-ils.cstore.direct.actor.org_unit.search',
995 { parent_ou => undef }
998 my $result = { service => 'native-evergreen-catalog', records => [] };
999 my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
1001 $$searchhash{searches}{title}{term} = $$args{search}{title} if $$args{search}{title};
1002 $$searchhash{searches}{author}{term} = $$args{search}{author} if $$args{search}{author};
1003 $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
1004 $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
1005 $$searchhash{searches}{'identifier|isbn'}{term} = $$args{search}{isbn} if $$args{search}{isbn};
1006 $$searchhash{searches}{'identifier|issn'}{term} = $$args{search}{issn} if $$args{search}{issn};
1008 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
1009 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
1010 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
1011 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
1013 my $list = the_quest_for_knowledge( $self, $client, $searchhash );
1015 if ($list->{count} > 0 and @{$list->{ids}}) {
1016 $result->{count} = $list->{count};
1018 my $records = $cstore->request(
1019 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
1020 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
1023 for my $rec ( @$records ) {
1025 my $u = OpenILS::Utils::ModsParser->new();
1026 $u->start_mods_batch( $rec->marc );
1027 my $mods = $u->finish_mods_batch();
1029 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
1035 $cstore->disconnect();
1039 # ----------------------------------------------------------------------------
1040 # These are the main OPAC search methods
1041 # ----------------------------------------------------------------------------
1043 __PACKAGE__->register_method(
1044 method => 'the_quest_for_knowledge',
1045 api_name => 'open-ils.search.biblio.multiclass',
1047 desc => "Performs a multi class biblio or metabib search",
1050 desc => "A search hash with keys: "
1051 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
1052 . "See perldoc " . __PACKAGE__ . " for more detail",
1056 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
1061 desc => 'An object of the form: '
1062 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
1067 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
1069 The search-hash argument can have the following elements:
1071 searches: { "$class" : "$value", ...} [REQUIRED]
1072 org_unit: The org id to focus the search at
1073 depth : The org depth
1074 limit : The search limit default: 10
1075 offset : The search offset default: 0
1076 format : The MARC format
1077 sort : What field to sort the results on? [ author | title | pubdate ]
1078 sort_dir: What direction do we sort? [ asc | desc ]
1079 tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
1080 will be tagged with an additional value ("1") as the last value in the record ID array for
1081 each record. Requires the 'authtoken'
1082 authtoken : Authentication token string; When actions are performed that require a user login
1083 (e.g. tagging circulated records), the authentication token is required
1085 The searches element is required, must have a hashref value, and the hashref must contain at least one
1086 of the following classes as a key:
1094 The value paired with a key is the associated search string.
1096 The docache argument enables/disables searching and saving results in cache (default OFF).
1098 The return object, if successful, will look like:
1100 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
1104 __PACKAGE__->register_method(
1105 method => 'the_quest_for_knowledge',
1106 api_name => 'open-ils.search.biblio.multiclass.staff',
1107 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1109 __PACKAGE__->register_method(
1110 method => 'the_quest_for_knowledge',
1111 api_name => 'open-ils.search.metabib.multiclass',
1112 signature => q/@see open-ils.search.biblio.multiclass/
1114 __PACKAGE__->register_method(
1115 method => 'the_quest_for_knowledge',
1116 api_name => 'open-ils.search.metabib.multiclass.staff',
1117 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1120 sub the_quest_for_knowledge {
1121 my( $self, $conn, $searchhash, $docache ) = @_;
1123 return { count => 0 } unless $searchhash and
1124 ref $searchhash->{searches} eq 'HASH';
1126 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
1130 if($self->api_name =~ /metabib/) {
1132 $method =~ s/biblio/metabib/o;
1135 # do some simple sanity checking
1136 if(!$searchhash->{searches} or
1137 ( !grep { /^(?:title|author|subject|series|keyword|identifier\|is[bs]n)/ } keys %{$searchhash->{searches}} ) ) {
1138 return { count => 0 };
1141 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
1142 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
1143 my $end = $offset + $limit - 1;
1145 my $maxlimit = 5000;
1146 $searchhash->{offset} = 0; # possible user value overwritten in hash
1147 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
1149 return { count => 0 } if $offset > $maxlimit;
1152 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
1153 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
1154 my $ckey = $pfx . md5_hex($method . $s);
1156 $logger->info("bib search for: $s");
1158 $searchhash->{limit} -= $offset;
1162 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1166 $method .= ".staff" if($self->api_name =~ /staff/);
1167 $method .= ".atomic";
1169 for (keys %$searchhash) {
1170 delete $$searchhash{$_}
1171 unless defined $$searchhash{$_};
1174 $result = $U->storagereq( $method, %$searchhash );
1178 $docache = 0; # results came FROM cache, so we don't write back
1181 return {count => 0} unless ($result && $$result[0]);
1185 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1188 # If we didn't get this data from the cache, put it into the cache
1189 # then return the correct offset of records
1190 $logger->debug("putting search cache $ckey\n");
1191 put_cache($ckey, $count, \@recs);
1195 # if we have the full set of data, trim out
1196 # the requested chunk based on limit and offset
1198 for ($offset..$end) {
1199 last unless $recs[$_];
1200 push(@t, $recs[$_]);
1205 return { ids => \@recs, count => $count };
1209 __PACKAGE__->register_method(
1210 method => 'staged_search',
1211 api_name => 'open-ils.search.biblio.multiclass.staged',
1213 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1214 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1217 desc => "A search hash with keys: "
1218 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1219 . "See perldoc " . __PACKAGE__ . " for more detail",
1223 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1228 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1229 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1234 __PACKAGE__->register_method(
1235 method => 'staged_search',
1236 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1237 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1239 __PACKAGE__->register_method(
1240 method => 'staged_search',
1241 api_name => 'open-ils.search.metabib.multiclass.staged',
1242 signature => q/@see open-ils.search.biblio.multiclass.staged/
1244 __PACKAGE__->register_method(
1245 method => 'staged_search',
1246 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1247 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1251 my($self, $conn, $search_hash, $docache) = @_;
1253 my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1255 my $method = $IAmMetabib?
1256 'open-ils.storage.metabib.multiclass.staged.search_fts':
1257 'open-ils.storage.biblio.multiclass.staged.search_fts';
1259 $method .= '.staff' if $self->api_name =~ /staff$/;
1260 $method .= '.atomic';
1262 return {count => 0} unless (
1264 $search_hash->{searches} and
1265 scalar( keys %{$search_hash->{searches}} ));
1267 my $search_duration;
1268 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1269 my $user_limit = $search_hash->{limit} || 10;
1270 my $ignore_facet_classes = $search_hash->{ignore_facet_classes};
1271 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1272 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1274 # restrict DB query to our max results
1275 $search_hash->{core_limit} = $max_search_results;
1277 # pull any existing results from the cache
1278 my $key = search_cache_key($method, $search_hash);
1279 my $facet_key = $key.'_facets';
1280 my $cache_data = $cache->get_cache($key) || {};
1282 # keep retrieving results until we find enough to
1283 # fulfill the user-specified limit and offset
1284 my $all_results = [];
1290 if($cache_data->{summary}) {
1291 # this window of results is already cached
1292 $logger->debug("staged search: found cached results");
1293 $summary = $cache_data->{summary};
1294 $results = $cache_data->{results};
1297 # retrieve the window of results from the database
1298 $logger->debug("staged search: fetching results from the database");
1300 $results = $U->storagereq($method, %$search_hash);
1301 $search_duration = time - $start;
1302 $summary = shift(@$results) if $results;
1305 $logger->info("search timed out: duration=$search_duration: params=".
1306 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1307 return {count => 0};
1310 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1312 my $hc = $summary->{visible};
1314 $logger->info("search returned 0 results: duration=$search_duration: params=".
1315 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1318 # Create backwards-compatible result structures
1320 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1322 $results = [map {[$_->{id}]} @$results];
1325 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1326 $results = [grep {defined $_->[0]} @$results];
1327 cache_staged_search($key, $summary, $results) if $docache;
1330 tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib)
1331 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1333 # add the new set of results to the set under construction
1334 push(@$all_results, @$results);
1336 my $current_count = scalar(@$all_results);
1338 $logger->debug("staged search: located $current_count, visible=".$summary->{visible});
1340 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1342 $conn->respond_complete(
1344 count => $summary->{visible},
1345 core_limit => $search_hash->{core_limit},
1346 facet_key => $facet_key,
1351 cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1356 sub tag_circulated_records {
1357 my ($auth, $results, $metabib) = @_;
1358 my $e = new_editor(authtoken => $auth);
1359 return $results unless $e->checkauth;
1362 select => { acn => [{ column => 'record', alias => 'tagme' }] },
1363 from => { acp => 'acn' },
1364 where => { id => { in => { from => ['action.usr_visible_circ_copies', $e->requestor->id] } } },
1370 select => { mmsm => [{ column => 'metarecord', alias => 'tagme' }] },
1372 where => { source => { in => $query } },
1377 # Give me the distinct set of bib records that exist in the user's visible circulation history
1378 my $circ_recs = $e->json_query( $query );
1380 # if the record appears in the circ history, push a 1 onto
1381 # the rec array structure to indicate truthiness
1382 for my $rec (@$results) {
1383 push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1389 # creates a unique token to represent the query in the cache
1390 sub search_cache_key {
1392 my $search_hash = shift;
1394 for my $key (sort keys %$search_hash) {
1395 push(@sorted, ($key => $$search_hash{$key}))
1396 unless $key eq 'limit' or
1398 $key eq 'skip_check';
1400 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1401 return $pfx . md5_hex($method . $s);
1404 sub retrieve_cached_facets {
1410 return undef unless ($key and $key =~ /_facets$/);
1412 my $blob = $cache->get_cache($key) || {};
1416 for my $f ( keys %$blob ) {
1417 my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1418 @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1419 for my $s ( @sorted ) {
1420 my ($k) = keys(%$s);
1421 my ($v) = values(%$s);
1422 $$facets{$f}{$k} = $v;
1432 __PACKAGE__->register_method(
1433 method => "retrieve_cached_facets",
1434 api_name => "open-ils.search.facet_cache.retrieve",
1436 desc => 'Returns facet data derived from a specific search based on a key '.
1437 'generated by open-ils.search.biblio.multiclass.staged and friends.',
1440 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1445 desc => 'Two level hash of facet values. Top level key is the facet id defined on the config.metabib_field table. '.
1446 'Second level key is a string facet value. Datum attached to each facet value is the number of distinct records, '.
1447 'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1448 'facet retrieval. These counts are calculated for all superpages that have been checked for visibility.',
1456 # add facets for this search to the facet cache
1457 my($key, $results, $metabib, $ignore) = @_;
1458 my $data = $cache->get_cache($key);
1461 return undef unless (@$results);
1463 # The query we're constructing
1465 # select mfae.field as id,
1467 # count(distinct mmrsm.appropriate-id-field )
1468 # from metabib.facet_entry mfae
1469 # join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1470 # where mmrsm.appropriate-id-field in IDLIST
1473 my $count_field = $metabib ? 'metarecord' : 'source';
1476 mfae => [ { column => 'field', alias => 'id'}, 'value' ],
1478 transform => 'count',
1480 column => $count_field,
1487 mmrsm => { field => 'source', fkey => 'source' },
1488 cmf => { field => 'id', fkey => 'field' }
1492 '+mmrsm' => { $count_field => $results },
1493 '+cmf' => { facet_field => 't' }
1497 $query->{where}->{'+cmf'}->{field_class} = {'not in' => $ignore}
1498 if ref($ignore) and @$ignore > 0;
1500 my $facets = $U->cstorereq("open-ils.cstore.json_query.atomic", $query);
1502 for my $facet (@$facets) {
1503 next unless ($facet->{value});
1504 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1507 $logger->info("facet compilation: cached with key=$key");
1509 $cache->put_cache($key, $data, $cache_timeout);
1512 sub cache_staged_search {
1513 # puts this set of results into the cache
1514 my($key, $summary, $results) = @_;
1516 summary => $summary,
1520 $logger->info("staged search: cached with key=$key, visible=".$summary->{visible});
1522 $cache->put_cache($key, $data, $cache_timeout);
1530 my $start = $offset;
1531 my $end = $offset + $limit - 1;
1533 $logger->debug("searching cache for $key : $start..$end\n");
1535 return undef unless $cache;
1536 my $data = $cache->get_cache($key);
1538 return undef unless $data;
1540 my $count = $data->[0];
1543 return undef unless $offset < $count;
1546 for( my $i = $offset; $i <= $end; $i++ ) {
1547 last unless my $d = $$data[$i];
1548 push( @result, $d );
1551 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1558 my( $key, $count, $data ) = @_;
1559 return undef unless $cache;
1560 $logger->debug("search_cache putting ".
1561 scalar(@$data)." items at key $key with timeout $cache_timeout");
1562 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1566 __PACKAGE__->register_method(
1567 method => "biblio_mrid_to_modsbatch_batch",
1568 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1571 sub biblio_mrid_to_modsbatch_batch {
1572 my( $self, $client, $mrids) = @_;
1573 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1575 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1576 for my $id (@$mrids) {
1577 next unless defined $id;
1578 my ($m) = $method->run($id);
1585 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1586 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1588 __PACKAGE__->register_method(
1589 method => "biblio_mrid_to_modsbatch",
1592 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1593 . "As usual, the .staff version of this method will include otherwise hidden records.",
1595 { desc => 'Metarecord ID', type => 'number' },
1596 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1599 desc => 'MVR Object, event on error',
1605 sub biblio_mrid_to_modsbatch {
1606 my( $self, $client, $mrid, $args) = @_;
1608 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1610 my ($mr, $evt) = _grab_metarecord($mrid);
1611 return $evt unless $mr;
1613 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1614 biblio_mrid_make_modsbatch($self, $client, $mr);
1616 return $mvr unless ref($args);
1618 # Here we find the lead record appropriate for the given filters
1619 # and use that for the title and author of the metarecord
1620 my $format = $$args{format};
1621 my $org = $$args{org};
1622 my $depth = $$args{depth};
1624 return $mvr unless $format or $org or $depth;
1626 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1627 $method = "$method.staff" if $self->api_name =~ /staff/o;
1629 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1631 if( my $mods = $U->record_to_mvr($rec) ) {
1633 $mvr->title( $mods->title );
1634 $mvr->author($mods->author);
1635 $logger->debug("mods_slim updating title and ".
1636 "author in mvr with ".$mods->title." : ".$mods->author);
1642 # converts a metarecord to an mvr
1645 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1646 return Fieldmapper::metabib::virtual_record->new($perl);
1649 # checks to see if a metarecord has mods, if so returns true;
1651 __PACKAGE__->register_method(
1652 method => "biblio_mrid_check_mvr",
1653 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1654 notes => "Takes a metarecord ID or a metarecord object and returns true "
1655 . "if the metarecord already has an mvr associated with it."
1658 sub biblio_mrid_check_mvr {
1659 my( $self, $client, $mrid ) = @_;
1663 if(ref($mrid)) { $mr = $mrid; }
1664 else { ($mr, $evt) = _grab_metarecord($mrid); }
1665 return $evt if $evt;
1667 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1669 return _mr_to_mvr($mr) if $mr->mods();
1673 sub _grab_metarecord {
1675 #my $e = OpenILS::Utils::Editor->new;
1676 my $e = new_editor();
1677 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1682 __PACKAGE__->register_method(
1683 method => "biblio_mrid_make_modsbatch",
1684 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1685 notes => "Takes either a metarecord ID or a metarecord object. "
1686 . "Forces the creations of an mvr for the given metarecord. "
1687 . "The created mvr is returned."
1690 sub biblio_mrid_make_modsbatch {
1691 my( $self, $client, $mrid ) = @_;
1693 #my $e = OpenILS::Utils::Editor->new;
1694 my $e = new_editor();
1701 $mr = $e->retrieve_metabib_metarecord($mrid)
1702 or return $e->event;
1705 my $masterid = $mr->master_record;
1706 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1708 my $ids = $U->storagereq(
1709 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1710 return undef unless @$ids;
1712 my $master = $e->retrieve_biblio_record_entry($masterid)
1713 or return $e->event;
1715 # start the mods batch
1716 my $u = OpenILS::Utils::ModsParser->new();
1717 $u->start_mods_batch( $master->marc );
1719 # grab all of the sub-records and shove them into the batch
1720 my @ids = grep { $_ ne $masterid } @$ids;
1721 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1726 my $r = $e->retrieve_biblio_record_entry($i);
1727 push( @$subrecs, $r ) if $r;
1732 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1733 $u->push_mods_batch( $_->marc ) if $_->marc;
1737 # finish up and send to the client
1738 my $mods = $u->finish_mods_batch();
1739 $mods->doc_id($mrid);
1740 $client->respond_complete($mods);
1743 # now update the mods string in the db
1744 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1747 #$e = OpenILS::Utils::Editor->new(xact => 1);
1748 $e = new_editor(xact => 1);
1749 $e->update_metabib_metarecord($mr)
1750 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1757 # converts a mr id into a list of record ids
1759 foreach (qw/open-ils.search.biblio.metarecord_to_records
1760 open-ils.search.biblio.metarecord_to_records.staff/)
1762 __PACKAGE__->register_method(
1763 method => "biblio_mrid_to_record_ids",
1766 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1767 . "As usual, the .staff version of this method will include otherwise hidden records.",
1769 { desc => 'Metarecord ID', type => 'number' },
1770 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1773 desc => 'Results object like {count => $i, ids =>[...]}',
1781 sub biblio_mrid_to_record_ids {
1782 my( $self, $client, $mrid, $args ) = @_;
1784 my $format = $$args{format};
1785 my $org = $$args{org};
1786 my $depth = $$args{depth};
1788 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1789 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1790 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1792 return { count => scalar(@$recs), ids => $recs };
1796 __PACKAGE__->register_method(
1797 method => "biblio_record_to_marc_html",
1798 api_name => "open-ils.search.biblio.record.html"
1801 __PACKAGE__->register_method(
1802 method => "biblio_record_to_marc_html",
1803 api_name => "open-ils.search.authority.to_html"
1806 # Persistent parsers and setting objects
1807 my $parser = XML::LibXML->new();
1808 my $xslt = XML::LibXSLT->new();
1810 my $slim_marc_sheet;
1811 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1813 sub biblio_record_to_marc_html {
1814 my($self, $client, $recordid, $slim, $marcxml) = @_;
1817 my $dir = $settings_client->config_value("dirs", "xsl");
1820 unless($slim_marc_sheet) {
1821 my $xsl = $settings_client->config_value(
1822 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1824 $xsl = $parser->parse_file("$dir/$xsl");
1825 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1828 $sheet = $slim_marc_sheet;
1832 unless($marc_sheet) {
1833 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1834 my $xsl = $settings_client->config_value(
1835 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1836 $xsl = $parser->parse_file("$dir/$xsl");
1837 $marc_sheet = $xslt->parse_stylesheet($xsl);
1839 $sheet = $marc_sheet;
1844 my $e = new_editor();
1845 if($self->api_name =~ /authority/) {
1846 $record = $e->retrieve_authority_record_entry($recordid)
1847 or return $e->event;
1849 $record = $e->retrieve_biblio_record_entry($recordid)
1850 or return $e->event;
1852 $marcxml = $record->marc;
1855 my $xmldoc = $parser->parse_string($marcxml);
1856 my $html = $sheet->transform($xmldoc);
1857 return $html->documentElement->toString();
1860 __PACKAGE__->register_method(
1861 method => "format_biblio_record_entry",
1862 api_name => "open-ils.search.biblio.record.print",
1864 desc => 'Returns a printable version of the specified bib record',
1866 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1869 desc => q/An action_trigger.event object or error event./,
1874 __PACKAGE__->register_method(
1875 method => "format_biblio_record_entry",
1876 api_name => "open-ils.search.biblio.record.email",
1878 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1880 { desc => 'Authentication token', type => 'string'},
1881 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1884 desc => q/Undefined on success, otherwise an error event./,
1890 sub format_biblio_record_entry {
1891 my($self, $conn, $arg1, $arg2) = @_;
1893 my $for_print = ($self->api_name =~ /print/);
1894 my $for_email = ($self->api_name =~ /email/);
1896 my $e; my $auth; my $bib_id; my $context_org;
1900 $context_org = $arg2 || $U->get_org_tree->id;
1901 $e = new_editor(xact => 1);
1902 } elsif ($for_email) {
1905 $e = new_editor(authtoken => $auth, xact => 1);
1906 return $e->die_event unless $e->checkauth;
1907 $context_org = $e->requestor->home_ou;
1911 if (ref $bib_id ne 'ARRAY') {
1912 $bib_ids = [ $bib_id ];
1917 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1918 $bucket->btype('temp');
1919 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1921 $bucket->owner($e->requestor)
1925 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1927 for my $id (@$bib_ids) {
1929 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1931 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1932 $bucket_entry->target_biblio_record_entry($bib);
1933 $bucket_entry->bucket($bucket_obj->id);
1934 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1941 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1943 } elsif ($for_email) {
1945 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1952 __PACKAGE__->register_method(
1953 method => "retrieve_all_copy_statuses",
1954 api_name => "open-ils.search.config.copy_status.retrieve.all"
1957 sub retrieve_all_copy_statuses {
1958 my( $self, $client ) = @_;
1959 return new_editor()->retrieve_all_config_copy_status();
1963 __PACKAGE__->register_method(
1964 method => "copy_counts_per_org",
1965 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1968 __PACKAGE__->register_method(
1969 method => "copy_counts_per_org",
1970 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1973 sub copy_counts_per_org {
1974 my( $self, $client, $record_id ) = @_;
1976 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1978 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1979 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1981 my $counts = $apputils->simple_scalar_request(
1982 "open-ils.storage", $method, $record_id );
1984 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1989 __PACKAGE__->register_method(
1990 method => "copy_count_summary",
1991 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1992 notes => "returns an array of these: "
1993 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
1994 . "where statusx is a copy status name. The statuses are sorted by ID.",
1998 sub copy_count_summary {
1999 my( $self, $client, $rid, $org, $depth ) = @_;
2002 my $data = $U->storagereq(
2003 'open-ils.storage.biblio.record_entry.status_copy_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] : ''))
2012 __PACKAGE__->register_method(
2013 method => "copy_location_count_summary",
2014 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
2015 notes => "returns an array of these: "
2016 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
2017 . "where statusx is a copy status name. The statuses are sorted by ID.",
2020 sub copy_location_count_summary {
2021 my( $self, $client, $rid, $org, $depth ) = @_;
2024 my $data = $U->storagereq(
2025 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2028 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2030 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2032 || $a->[4] cmp $b->[4]
2036 __PACKAGE__->register_method(
2037 method => "copy_count_location_summary",
2038 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2039 notes => "returns an array of these: "
2040 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2041 . "where statusx is a copy status name. The statuses are sorted by ID."
2044 sub copy_count_location_summary {
2045 my( $self, $client, $rid, $org, $depth ) = @_;
2048 my $data = $U->storagereq(
2049 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2051 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2053 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2058 foreach (qw/open-ils.search.biblio.marc
2059 open-ils.search.biblio.marc.staff/)
2061 __PACKAGE__->register_method(
2062 method => "marc_search",
2065 desc => 'Fetch biblio IDs based on MARC record criteria. '
2066 . 'As usual, the .staff version of the search includes otherwise hidden records',
2069 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2070 'See perldoc ' . __PACKAGE__ . ' for more detail.',
2073 {desc => 'limit (optional)', type => 'number'},
2074 {desc => 'offset (optional)', type => 'number'}
2077 desc => 'Results object like: { "count": $i, "ids": [...] }',
2084 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
2086 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
2088 searches: complex query object (required)
2089 org_unit: The org ID to focus the search at
2090 depth : The org depth
2091 limit : integer search limit default: 10
2092 offset : integer search offset default: 0
2093 sort : What field to sort the results on? [ author | title | pubdate ]
2094 sort_dir: In what direction do we sort? [ asc | desc ]
2096 Additional keys to refine search criteria:
2099 language : Language (code)
2100 lit_form : Literary form
2101 item_form: Item form
2102 item_type: Item type
2103 format : The MARC format
2105 Please note that the specific strings to be used in the "addtional keys" will be entirely
2106 dependent on your loaded data.
2108 All keys except "searches" are optional.
2109 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
2111 For example, an arg hash might look like:
2133 The arghash is eventually passed to the SRF call:
2134 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2136 Presently, search uses the cache unconditionally.
2140 # FIXME: that example above isn't actually tested.
2141 # TODO: docache option?
2143 my( $self, $conn, $args, $limit, $offset, $timeout ) = @_;
2145 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2146 $method .= ".staff" if $self->api_name =~ /staff/;
2147 $method .= ".atomic";
2149 $limit ||= 10; # FIXME: what about $args->{limit} ?
2150 $offset ||= 0; # FIXME: what about $args->{offset} ?
2152 # allow caller to pass in a call timeout since MARC searches
2153 # can take longer than the default 60-second timeout.
2154 # Default to 2 mins. Arbitrarily cap at 5 mins.
2155 $timeout = 120 if !$timeout or $timeout > 300;
2158 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2159 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2161 my $recs = search_cache($ckey, $offset, $limit);
2165 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2166 my $req = $ses->request($method, %$args);
2167 my $resp = $req->recv($timeout);
2169 if($resp and $recs = $resp->content) {
2170 put_cache($ckey, scalar(@$recs), $recs);
2171 $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2180 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2181 my @recs = map { $_->[0] } @$recs;
2183 return { ids => \@recs, count => $count };
2187 foreach my $isbn_method (qw/
2188 open-ils.search.biblio.isbn
2189 open-ils.search.biblio.isbn.staff
2191 __PACKAGE__->register_method(
2192 method => "biblio_search_isbn",
2193 api_name => $isbn_method,
2195 desc => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2197 {desc => 'ISBN', type => 'string'}
2200 desc => 'Results object like: { "count": $i, "ids": [...] }',
2207 sub biblio_search_isbn {
2208 my( $self, $client, $isbn ) = @_;
2209 $logger->debug("Searching ISBN $isbn");
2210 # the previous implementation of this method was essentially unlimited,
2211 # so we will set our limit very high and let multiclass.query provide any
2213 # XXX: if making this unlimited is deemed important, we might consider
2214 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2215 # which is functionally deprecated at this point, or a custom call to
2216 # 'open-ils.storage.biblio.multiclass.search_fts'
2218 my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2219 if ($self->api_name =~ m/.staff$/) {
2220 $isbn_method .= '.staff';
2223 my $method = $self->method_lookup($isbn_method);
2224 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2225 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2226 return { ids => \@recs, count => $search_result->{'count'} };
2229 __PACKAGE__->register_method(
2230 method => "biblio_search_isbn_batch",
2231 api_name => "open-ils.search.biblio.isbn_list",
2234 # XXX: see biblio_search_isbn() for note concerning 'limit'
2235 sub biblio_search_isbn_batch {
2236 my( $self, $client, $isbn_list ) = @_;
2237 $logger->debug("Searching ISBNs @$isbn_list");
2238 my @recs = (); my %rec_set = ();
2239 my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2240 foreach my $isbn ( @$isbn_list ) {
2241 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2242 my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2243 foreach my $rec (@recs_subset) {
2244 if (! $rec_set{ $rec }) {
2245 $rec_set{ $rec } = 1;
2250 return { ids => \@recs, count => scalar(@recs) };
2253 foreach my $issn_method (qw/
2254 open-ils.search.biblio.issn
2255 open-ils.search.biblio.issn.staff
2257 __PACKAGE__->register_method(
2258 method => "biblio_search_issn",
2259 api_name => $issn_method,
2261 desc => 'Retrieve biblio IDs for a given ISSN',
2263 {desc => 'ISBN', type => 'string'}
2266 desc => 'Results object like: { "count": $i, "ids": [...] }',
2273 sub biblio_search_issn {
2274 my( $self, $client, $issn ) = @_;
2275 $logger->debug("Searching ISSN $issn");
2276 # the previous implementation of this method was essentially unlimited,
2277 # so we will set our limit very high and let multiclass.query provide any
2279 # XXX: if making this unlimited is deemed important, we might consider
2280 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2281 # which is functionally deprecated at this point, or a custom call to
2282 # 'open-ils.storage.biblio.multiclass.search_fts'
2284 my $issn_method = 'open-ils.search.biblio.multiclass.query';
2285 if ($self->api_name =~ m/.staff$/) {
2286 $issn_method .= '.staff';
2289 my $method = $self->method_lookup($issn_method);
2290 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2291 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2292 return { ids => \@recs, count => $search_result->{'count'} };
2296 __PACKAGE__->register_method(
2297 method => "fetch_mods_by_copy",
2298 api_name => "open-ils.search.biblio.mods_from_copy",
2301 desc => 'Retrieve MODS record given an attached copy ID',
2303 { desc => 'Copy ID', type => 'number' }
2306 desc => 'MODS record, event on error or uncataloged item'
2311 sub fetch_mods_by_copy {
2312 my( $self, $client, $copyid ) = @_;
2313 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2314 return $evt if $evt;
2315 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2316 return $apputils->record_to_mvr($record);
2320 # -------------------------------------------------------------------------------------
2322 __PACKAGE__->register_method(
2323 method => "cn_browse",
2324 api_name => "open-ils.search.callnumber.browse.target",
2325 notes => "Starts a callnumber browse"
2328 __PACKAGE__->register_method(
2329 method => "cn_browse",
2330 api_name => "open-ils.search.callnumber.browse.page_up",
2331 notes => "Returns the previous page of callnumbers",
2334 __PACKAGE__->register_method(
2335 method => "cn_browse",
2336 api_name => "open-ils.search.callnumber.browse.page_down",
2337 notes => "Returns the next page of callnumbers",
2341 # RETURNS array of arrays like so: label, owning_lib, record, id
2343 my( $self, $client, @params ) = @_;
2346 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2347 if( $self->api_name =~ /target/ );
2348 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2349 if( $self->api_name =~ /page_up/ );
2350 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2351 if( $self->api_name =~ /page_down/ );
2353 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2355 # -------------------------------------------------------------------------------------
2357 __PACKAGE__->register_method(
2358 method => "fetch_cn",
2359 api_name => "open-ils.search.callnumber.retrieve",
2361 notes => "retrieves a callnumber based on ID",
2365 my( $self, $client, $id ) = @_;
2367 my $e = new_editor();
2368 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2369 return $evt if $evt;
2373 __PACKAGE__->register_method(
2374 method => "fetch_fleshed_cn",
2375 api_name => "open-ils.search.callnumber.fleshed.retrieve",
2377 notes => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2380 sub fetch_fleshed_cn {
2381 my( $self, $client, $id ) = @_;
2383 my $e = new_editor();
2384 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2385 return $evt if $evt;
2390 __PACKAGE__->register_method(
2391 method => "fetch_copy_by_cn",
2392 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2394 Returns an array of copy ID's by callnumber ID
2395 @param cnid The callnumber ID
2396 @return An array of copy IDs
2400 sub fetch_copy_by_cn {
2401 my( $self, $conn, $cnid ) = @_;
2402 return $U->cstorereq(
2403 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2404 { call_number => $cnid, deleted => 'f' } );
2407 __PACKAGE__->register_method(
2408 method => 'fetch_cn_by_info',
2409 api_name => 'open-ils.search.call_number.retrieve_by_info',
2411 @param label The callnumber label
2412 @param record The record the cn is attached to
2413 @param org The owning library of the cn
2414 @return The callnumber object
2419 sub fetch_cn_by_info {
2420 my( $self, $conn, $label, $record, $org ) = @_;
2421 return $U->cstorereq(
2422 'open-ils.cstore.direct.asset.call_number.search',
2423 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2428 __PACKAGE__->register_method(
2429 method => 'bib_extras',
2430 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2433 __PACKAGE__->register_method(
2434 method => 'bib_extras',
2435 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2436 ctype => 'item_form'
2438 __PACKAGE__->register_method(
2439 method => 'bib_extras',
2440 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2441 ctype => 'item_type',
2443 __PACKAGE__->register_method(
2444 method => 'bib_extras',
2445 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2446 ctype => 'bib_level'
2448 __PACKAGE__->register_method(
2449 method => 'bib_extras',
2450 api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2456 $logger->warn("deprecation warning: " .$self->api_name);
2458 my $e = new_editor();
2460 my $ctype = $self->{ctype};
2461 my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2464 for my $ccvm (@$ccvms) {
2465 my $obj = "Fieldmapper::config::${ctype}_map"->new;
2466 $obj->value($ccvm->value);
2467 $obj->code($ccvm->code);
2468 $obj->description($ccvm->description) if $obj->can('description');
2477 __PACKAGE__->register_method(
2478 method => 'fetch_slim_record',
2479 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2481 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2483 { desc => 'Array of Record IDs', type => 'array' }
2486 desc => 'Array of biblio records, event on error'
2491 sub fetch_slim_record {
2492 my( $self, $conn, $ids ) = @_;
2494 #my $editor = OpenILS::Utils::Editor->new;
2495 my $editor = new_editor();
2498 return $editor->event unless
2499 my $r = $editor->retrieve_biblio_record_entry($_);
2506 __PACKAGE__->register_method(
2507 method => 'rec_hold_parts',
2508 api_name => 'open-ils.search.biblio.record_hold_parts',
2510 Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2514 sub rec_hold_parts {
2515 my( $self, $conn, $args ) = @_;
2517 my $rec = $$args{record};
2518 my $mrec = $$args{metarecord};
2519 my $pickup_lib = $$args{pickup_lib};
2520 my $e = new_editor();
2523 select => {bmp => ['id', 'label']},
2528 select => {'acpm' => ['part']},
2529 from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2531 '+acp' => {'deleted' => 'f'},
2532 '+bre' => {id => $rec}
2538 order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2541 if(defined $pickup_lib) {
2542 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2543 if($hard_boundary) {
2544 my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2545 $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2549 return $e->json_query($query);
2555 __PACKAGE__->register_method(
2556 method => 'rec_to_mr_rec_descriptors',
2557 api_name => 'open-ils.search.metabib.record_to_descriptors',
2559 specialized method...
2560 Given a biblio record id or a metarecord id,
2561 this returns a list of metabib.record_descriptor
2562 objects that live within the same metarecord
2563 @param args Object of args including:
2567 sub rec_to_mr_rec_descriptors {
2568 my( $self, $conn, $args ) = @_;
2570 my $rec = $$args{record};
2571 my $mrec = $$args{metarecord};
2572 my $item_forms = $$args{item_forms};
2573 my $item_types = $$args{item_types};
2574 my $item_lang = $$args{item_lang};
2575 my $pickup_lib = $$args{pickup_lib};
2577 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2579 my $e = new_editor();
2583 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2584 return $e->event unless @$map;
2585 $mrec = $$map[0]->metarecord;
2588 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2589 return $e->event unless @$recs;
2591 my @recs = map { $_->source } @$recs;
2592 my $search = { record => \@recs };
2593 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2594 $search->{item_type} = $item_types if $item_types and @$item_types;
2595 $search->{item_lang} = $item_lang if $item_lang;
2597 my $desc = $e->search_metabib_record_descriptor($search);
2601 select => { 'bre' => ['id'] },
2606 'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2612 '+bre' => { id => \@recs },
2617 "+ccs" => { holdable => 't' },
2618 "+acpl" => { holdable => 't' }
2622 if ($hard_boundary) { # 0 (or "top") is the same as no setting
2623 my $orgs = $e->json_query(
2624 { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2625 ) or return $e->die_event;
2627 $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2630 my $good_records = $e->json_query($query) or return $e->die_event;
2633 for my $d (@$desc) {
2634 if ( grep { $d->record == $_->{id} } @$good_records ) {
2641 return { metarecord => $mrec, descriptors => $desc };
2645 __PACKAGE__->register_method(
2646 method => 'fetch_age_protect',
2647 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2650 sub fetch_age_protect {
2651 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2655 __PACKAGE__->register_method(
2656 method => 'copies_by_cn_label',
2657 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2660 __PACKAGE__->register_method(
2661 method => 'copies_by_cn_label',
2662 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2665 sub copies_by_cn_label {
2666 my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2667 my $e = new_editor();
2668 my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2669 my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2670 my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2671 return [] unless @$cns;
2673 # show all non-deleted copies in the staff client ...
2674 if ($self->api_name =~ /staff$/o) {
2675 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2678 # ... otherwise, grab the copies ...
2679 my $copies = $e->search_asset_copy(
2680 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2681 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2685 # ... and test for location and status visibility
2686 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];