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/;
16 use Time::HiRes qw(time sleep);
17 use OpenSRF::EX qw(:try);
18 use Digest::MD5 qw(md5_hex);
24 $Data::Dumper::Indent = 0;
26 use OpenILS::Const qw/:const/;
28 use OpenILS::Application::AppUtils;
29 my $apputils = "OpenILS::Application::AppUtils";
32 my $pfx = "open-ils.search_";
40 $cache = OpenSRF::Utils::Cache->new('global');
41 my $sclient = OpenSRF::Utils::SettingsClient->new();
42 $cache_timeout = $sclient->config_value(
43 "apps", "open-ils.search", "app_settings", "cache_timeout" ) || 300;
45 $superpage_size = $sclient->config_value(
46 "apps", "open-ils.search", "app_settings", "superpage_size" ) || 500;
48 $max_superpages = $sclient->config_value(
49 "apps", "open-ils.search", "app_settings", "max_superpages" ) || 20;
51 $logger->info("Search cache timeout is $cache_timeout, ".
52 " superpage_size is $superpage_size, max_superpages is $max_superpages");
57 # ---------------------------------------------------------------------------
58 # takes a list of record id's and turns the docs into friendly
59 # mods structures. Creates one MODS structure for each doc id.
60 # ---------------------------------------------------------------------------
61 sub _records_to_mods {
67 my $session = OpenSRF::AppSession->create("open-ils.cstore");
68 my $request = $session->request(
69 "open-ils.cstore.direct.biblio.record_entry.search", { id => \@ids } );
71 while( my $resp = $request->recv ) {
72 my $content = $resp->content;
73 next if $content->id == OILS_PRECAT_RECORD;
74 my $u = OpenILS::Utils::ModsParser->new(); # FIXME: we really need a new parser for each object?
75 $u->start_mods_batch( $content->marc );
76 my $mods = $u->finish_mods_batch();
77 $mods->doc_id($content->id());
78 $mods->tcn($content->tcn_value);
82 $session->disconnect();
86 __PACKAGE__->register_method(
87 method => "record_id_to_mods",
88 api_name => "open-ils.search.biblio.record.mods.retrieve",
91 desc => "Provide ID, we provide the MODS object with copy count. "
92 . "Note: this method does NOT take an array of IDs like mods_slim.retrieve", # FIXME: do it here too
94 { desc => 'Record ID', type => 'number' }
97 desc => 'MODS object', type => 'object'
102 # converts a record into a mods object with copy counts attached
103 sub record_id_to_mods {
105 my( $self, $client, $org_id, $id ) = @_;
107 my $mods_list = _records_to_mods( $id );
108 my $mods_obj = $mods_list->[0];
109 my $cmethod = $self->method_lookup("open-ils.search.biblio.record.copy_count");
110 my ($count) = $cmethod->run($org_id, $id);
111 $mods_obj->copy_count($count);
118 __PACKAGE__->register_method(
119 method => "record_id_to_mods_slim",
120 api_name => "open-ils.search.biblio.record.mods_slim.retrieve",
124 desc => "Provide ID(s), we provide the MODS",
126 { desc => 'Record ID or array of IDs' }
129 desc => 'MODS object(s), event on error'
134 # converts a record into a mods object with NO copy counts attached
135 sub record_id_to_mods_slim {
136 my( $self, $client, $id ) = @_;
137 return undef unless defined $id;
139 if(ref($id) and ref($id) == 'ARRAY') {
140 return _records_to_mods( @$id );
142 my $mods_list = _records_to_mods( $id );
143 my $mods_obj = $mods_list->[0];
144 return OpenILS::Event->new('BIBLIO_RECORD_ENTRY_NOT_FOUND') unless $mods_obj;
150 __PACKAGE__->register_method(
151 method => "record_id_to_mods_slim_batch",
152 api_name => "open-ils.search.biblio.record.mods_slim.batch.retrieve",
155 sub record_id_to_mods_slim_batch {
156 my($self, $conn, $id_list) = @_;
157 $conn->respond(_records_to_mods($_)->[0]) for @$id_list;
162 # Returns the number of copies attached to a record based on org location
163 __PACKAGE__->register_method(
164 method => "record_id_to_copy_count",
165 api_name => "open-ils.search.biblio.record.copy_count",
167 desc => q/Returns a copy summary for the given record for the context org
168 unit and all ancestor org units/,
170 {desc => 'Context org unit id', type => 'number'},
171 {desc => 'Record ID', type => 'number'}
174 desc => q/summary object per org unit in the set, where the set
175 includes the context org unit and all parent org units.
176 Object includes the keys "transcendant", "count", "org_unit", "depth",
177 "unshadow", "available". Each is a count, except "org_unit" which is
178 the context org unit and "depth" which is the depth of the context org unit
185 __PACKAGE__->register_method(
186 method => "record_id_to_copy_count",
187 api_name => "open-ils.search.biblio.record.copy_count.staff",
190 desc => q/Returns a copy summary for the given record for the context org
191 unit and all ancestor org units/,
193 {desc => 'Context org unit id', type => 'number'},
194 {desc => 'Record ID', type => 'number'}
197 desc => q/summary object per org unit in the set, where the set
198 includes the context org unit and all parent org units.
199 Object includes the keys "transcendant", "count", "org_unit", "depth",
200 "unshadow", "available". Each is a count, except "org_unit" which is
201 the context org unit and "depth" which is the depth of the context org unit
208 __PACKAGE__->register_method(
209 method => "record_id_to_copy_count",
210 api_name => "open-ils.search.biblio.metarecord.copy_count",
212 desc => q/Returns a copy summary for the given record for the context org
213 unit and all ancestor org units/,
215 {desc => 'Context org unit id', type => 'number'},
216 {desc => 'Record ID', type => 'number'}
219 desc => q/summary object per org unit in the set, where the set
220 includes the context org unit and all parent org units.
221 Object includes the keys "transcendant", "count", "org_unit", "depth",
222 "unshadow", "available". Each is a count, except "org_unit" which is
223 the context org unit and "depth" which is the depth of the context org unit
230 __PACKAGE__->register_method(
231 method => "record_id_to_copy_count",
232 api_name => "open-ils.search.biblio.metarecord.copy_count.staff",
234 desc => q/Returns a copy summary for the given record for the context org
235 unit and all ancestor org units/,
237 {desc => 'Context org unit id', type => 'number'},
238 {desc => 'Record ID', type => 'number'}
241 desc => q/summary object per org unit in the set, where the set
242 includes the context org unit and all parent org units.
243 Object includes the keys "transcendant", "count", "org_unit", "depth",
244 "unshadow", "available". Each is a count, except "org_unit" which is
245 the context org unit and "depth" which is the depth of the context org
246 unit. "depth" is always -1 when the count from a lasso search is
247 performed, since depth doesn't mean anything in a lasso context.
254 sub record_id_to_copy_count {
255 my( $self, $client, $org_id, $record_id ) = @_;
257 return [] unless $record_id;
259 my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
260 my $staff = $self->api_name =~ /staff/ ? 't' : 'f';
262 my $data = $U->cstorereq(
263 "open-ils.cstore.json_query.atomic",
264 { from => ['asset.' . $key . '_copy_count' => $org_id => $record_id => $staff] }
268 for my $d ( @$data ) { # fix up the key name change required by stored-proc version
269 $$d{count} = delete $$d{visible};
273 return [ sort { $a->{depth} <=> $b->{depth} } @count ];
276 __PACKAGE__->register_method(
277 method => "record_has_holdable_copy",
278 api_name => "open-ils.search.biblio.record.has_holdable_copy",
280 desc => q/Returns a boolean indicating if a record has any holdable copies./,
282 {desc => 'Record ID', type => 'number'}
285 desc => q/bool indicating if the record has any holdable copies/,
291 __PACKAGE__->register_method(
292 method => "record_has_holdable_copy",
293 api_name => "open-ils.search.biblio.metarecord.has_holdable_copy",
295 desc => q/Returns a boolean indicating if a record has any holdable copies./,
297 {desc => 'Record ID', type => 'number'}
300 desc => q/bool indicating if the record has any holdable copies/,
306 sub record_has_holdable_copy {
307 my($self, $client, $record_id ) = @_;
309 return 0 unless $record_id;
311 my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
313 my $data = $U->cstorereq(
314 "open-ils.cstore.json_query.atomic",
315 { from => ['asset.' . $key . '_has_holdable_copy' => $record_id ] }
318 return ${@$data[0]}{'asset.' . $key . '_has_holdable_copy'} eq 't';
322 __PACKAGE__->register_method(
323 method => "biblio_search_tcn",
324 api_name => "open-ils.search.biblio.tcn",
327 desc => "Retrieve related record ID(s) given a TCN",
329 { desc => 'TCN', type => 'string' },
330 { desc => 'Flag indicating to include deleted records', type => 'string' }
333 desc => 'Results object like: { "count": $i, "ids": [...] }',
340 sub biblio_search_tcn {
342 my( $self, $client, $tcn, $include_deleted ) = @_;
344 $tcn =~ s/^\s+|\s+$//og;
346 my $e = new_editor();
347 my $search = {tcn_value => $tcn};
348 $search->{deleted} = 'f' unless $include_deleted;
349 my $recs = $e->search_biblio_record_entry( $search, {idlist =>1} );
351 return { count => scalar(@$recs), ids => $recs };
355 # --------------------------------------------------------------------------------
357 __PACKAGE__->register_method(
358 method => "biblio_barcode_to_copy",
359 api_name => "open-ils.search.asset.copy.find_by_barcode",
361 sub biblio_barcode_to_copy {
362 my( $self, $client, $barcode ) = @_;
363 my( $copy, $evt ) = $U->fetch_copy_by_barcode($barcode);
368 __PACKAGE__->register_method(
369 method => "biblio_id_to_copy",
370 api_name => "open-ils.search.asset.copy.batch.retrieve",
372 sub biblio_id_to_copy {
373 my( $self, $client, $ids ) = @_;
374 $logger->info("Fetching copies @$ids");
375 return $U->cstorereq(
376 "open-ils.cstore.direct.asset.copy.search.atomic", { id => $ids } );
380 __PACKAGE__->register_method(
381 method => "biblio_id_to_uris",
382 api_name=> "open-ils.search.asset.uri.retrieve_by_bib",
386 @param BibID Which bib record contains the URIs
387 @param OrgID Where to look for URIs
388 @param OrgDepth Range adjustment for OrgID
389 @return A stream or list of 'auri' objects
393 sub biblio_id_to_uris {
394 my( $self, $client, $bib, $org, $depth ) = @_;
395 die "Org ID required" unless defined($org);
396 die "Bib ID required" unless defined($bib);
399 push @params, $depth if (defined $depth);
401 my $ids = $U->cstorereq( "open-ils.cstore.json_query.atomic",
402 { select => { auri => [ 'id' ] },
406 field => 'call_number',
412 filter => { active => 't' }
423 select => { aou => [ { column => 'id', transform => 'actor.org_unit_descendants', params => \@params, result_field => 'id' } ] },
425 where => { id => $org },
435 my $uris = $U->cstorereq(
436 "open-ils.cstore.direct.asset.uri.search.atomic",
437 { id => [ map { (values %$_) } @$ids ] }
440 $client->respond($_) for (@$uris);
446 __PACKAGE__->register_method(
447 method => "copy_retrieve",
448 api_name => "open-ils.search.asset.copy.retrieve",
451 desc => 'Retrieve a copy object based on the Copy ID',
453 { desc => 'Copy ID', type => 'number'}
456 desc => 'Copy object, event on error'
462 my( $self, $client, $cid ) = @_;
463 my( $copy, $evt ) = $U->fetch_copy($cid);
464 return $evt || $copy;
467 __PACKAGE__->register_method(
468 method => "volume_retrieve",
469 api_name => "open-ils.search.asset.call_number.retrieve"
471 sub volume_retrieve {
472 my( $self, $client, $vid ) = @_;
473 my $e = new_editor();
474 my $vol = $e->retrieve_asset_call_number($vid) or return $e->event;
478 __PACKAGE__->register_method(
479 method => "fleshed_copy_retrieve_batch",
480 api_name => "open-ils.search.asset.copy.fleshed.batch.retrieve",
484 sub fleshed_copy_retrieve_batch {
485 my( $self, $client, $ids ) = @_;
486 $logger->info("Fetching fleshed copies @$ids");
487 return $U->cstorereq(
488 "open-ils.cstore.direct.asset.copy.search.atomic",
491 flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries parts / ] }
496 __PACKAGE__->register_method(
497 method => "fleshed_copy_retrieve",
498 api_name => "open-ils.search.asset.copy.fleshed.retrieve",
501 sub fleshed_copy_retrieve {
502 my( $self, $client, $id ) = @_;
503 my( $c, $e) = $U->fetch_fleshed_copy($id);
508 __PACKAGE__->register_method(
509 method => 'fleshed_by_barcode',
510 api_name => "open-ils.search.asset.copy.fleshed2.find_by_barcode",
513 sub fleshed_by_barcode {
514 my( $self, $conn, $barcode ) = @_;
515 my $e = new_editor();
516 my $copyid = $e->search_asset_copy(
517 {barcode => $barcode, deleted => 'f'}, {idlist=>1})->[0]
519 return fleshed_copy_retrieve2( $self, $conn, $copyid);
523 __PACKAGE__->register_method(
524 method => "fleshed_copy_retrieve2",
525 api_name => "open-ils.search.asset.copy.fleshed2.retrieve",
529 sub fleshed_copy_retrieve2 {
530 my( $self, $client, $id ) = @_;
531 my $e = new_editor();
532 my $copy = $e->retrieve_asset_copy(
539 qw/ location status stat_cat_entry_copy_maps notes age_protect parts peer_record_maps /
541 ascecm => [qw/ stat_cat stat_cat_entry /],
545 ) or return $e->event;
547 # For backwards compatibility
548 #$copy->stat_cat_entries($copy->stat_cat_entry_copy_maps);
550 if( $copy->status->id == OILS_COPY_STATUS_CHECKED_OUT ) {
552 $e->search_action_circulation(
554 { target_copy => $copy->id },
556 order_by => { circ => 'xact_start desc' },
568 __PACKAGE__->register_method(
569 method => 'flesh_copy_custom',
570 api_name => 'open-ils.search.asset.copy.fleshed.custom',
574 sub flesh_copy_custom {
575 my( $self, $conn, $copyid, $fields ) = @_;
576 my $e = new_editor();
577 my $copy = $e->retrieve_asset_copy(
587 ) or return $e->event;
592 __PACKAGE__->register_method(
593 method => "biblio_barcode_to_title",
594 api_name => "open-ils.search.biblio.find_by_barcode",
597 sub biblio_barcode_to_title {
598 my( $self, $client, $barcode ) = @_;
600 my $title = $apputils->simple_scalar_request(
602 "open-ils.storage.biblio.record_entry.retrieve_by_barcode", $barcode );
604 return { ids => [ $title->id ], count => 1 } if $title;
605 return { count => 0 };
608 __PACKAGE__->register_method(
609 method => 'title_id_by_item_barcode',
610 api_name => 'open-ils.search.bib_id.by_barcode',
613 desc => 'Retrieve bib record id associated with the copy identified by the given barcode',
615 { desc => 'Item barcode', type => 'string' }
618 desc => 'Bib record id.'
623 __PACKAGE__->register_method(
624 method => 'title_id_by_item_barcode',
625 api_name => 'open-ils.search.multi_home.bib_ids.by_barcode',
628 desc => 'Retrieve bib record ids associated with the copy identified by the given barcode. This includes peer bibs for Multi-Home items.',
630 { desc => 'Item barcode', type => 'string' }
633 desc => 'Array of bib record ids. First element is the native bib for the item.'
639 sub title_id_by_item_barcode {
640 my( $self, $conn, $barcode ) = @_;
641 my $e = new_editor();
642 my $copies = $e->search_asset_copy(
644 { deleted => 'f', barcode => $barcode },
648 acp => [ 'call_number' ],
655 return $e->event unless @$copies;
657 if( $self->api_name =~ /multi_home/ ) {
658 my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
660 { target_copy => $$copies[0]->id }
663 my @temp = map { $_->peer_record } @{ $multi_home_list };
664 unshift @temp, $$copies[0]->call_number->record->id;
667 return $$copies[0]->call_number->record->id;
671 __PACKAGE__->register_method(
672 method => 'find_peer_bibs',
673 api_name => 'open-ils.search.peer_bibs.test',
676 desc => 'Tests to see if the specified record is a peer record.',
678 { desc => 'Biblio record entry Id', type => 'number' }
681 desc => 'True if specified id can be found in biblio.peer_bib_copy_map.peer_record.',
687 __PACKAGE__->register_method(
688 method => 'find_peer_bibs',
689 api_name => 'open-ils.search.peer_bibs',
692 desc => 'Return acps and mvrs for multi-home items linked to specified peer record.',
694 { desc => 'Biblio record entry Id', type => 'number' }
697 desc => '{ records => Array of mvrs, items => array of acps }',
704 my( $self, $client, $doc_id ) = @_;
705 my $e = new_editor();
707 my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
709 { peer_record => $doc_id },
713 bpbcm => [ 'target_copy', 'peer_type' ],
714 acp => [ 'call_number', 'location', 'status', 'peer_record_maps' ]
720 if ($self->api_name =~ /test/) {
721 return scalar( @{$multi_home_list} ) > 0 ? 1 : 0;
724 if (scalar(@{$multi_home_list})==0) {
728 # create a unique hash of the primary record MVRs for foreign copies
729 # XXX PLEASE let's change to unAPI2 (supports foreign copies) in the TT opac?!?
731 ($_->target_copy->call_number->record, _records_to_mods( $_->target_copy->call_number->record )->[0])
734 # set the foreign_copy_maps field to an empty array
735 map { $rec_hash{$_}->foreign_copy_maps([]) } keys( %rec_hash );
737 # push the maps onto the correct MVRs
738 for (@$multi_home_list) {
740 @{$rec_hash{ $_->target_copy->call_number->record }->foreign_copy_maps()},
745 return [sort {$a->title cmp $b->title} values(%rec_hash)];
748 __PACKAGE__->register_method(
749 method => "biblio_copy_to_mods",
750 api_name => "open-ils.search.biblio.copy.mods.retrieve",
753 # takes a copy object and returns it fleshed mods object
754 sub biblio_copy_to_mods {
755 my( $self, $client, $copy ) = @_;
757 my $volume = $U->cstorereq(
758 "open-ils.cstore.direct.asset.call_number.retrieve",
759 $copy->call_number() );
761 my $mods = _records_to_mods($volume->record());
762 $mods = shift @$mods;
763 $volume->copies([$copy]);
764 push @{$mods->call_numbers()}, $volume;
772 OpenILS::Application::Search::Biblio
778 =head3 open-ils.search.biblio.multiclass.query (arghash, query, docache)
780 For arghash and docache, see B<open-ils.search.biblio.multiclass>.
782 The query argument is a string, but built like a hash with key: value pairs.
783 Recognized search keys include:
785 keyword (kw) - search keyword(s) *
786 author (au) - search author(s) *
787 name (au) - same as author *
788 title (ti) - search title *
789 subject (su) - search subject *
790 series (se) - search series *
791 lang - limit by language (specify multiple langs with lang:l1 lang:l2 ...)
792 site - search at specified org unit, corresponds to actor.org_unit.shortname
793 pref_ou - extend search to specified org unit, corresponds to actor.org_unit.shortname
794 sort - sort type (title, author, pubdate)
795 dir - sort direction (asc, desc)
796 available - if set to anything other than "false" or "0", limits to available items
798 * Searching keyword, author, title, subject, and series supports additional search
799 subclasses, specified with a "|". For example, C<title|proper:gone with the wind>.
801 For more, see B<config.metabib_field>.
805 foreach (qw/open-ils.search.biblio.multiclass.query
806 open-ils.search.biblio.multiclass.query.staff
807 open-ils.search.metabib.multiclass.query
808 open-ils.search.metabib.multiclass.query.staff/)
810 __PACKAGE__->register_method(
812 method => 'multiclass_query',
814 desc => 'Perform a search query. The .staff version of the call includes otherwise hidden hits.',
816 {name => 'arghash', desc => 'Arg hash (see open-ils.search.biblio.multiclass)', type => 'object'},
817 {name => 'query', desc => 'Raw human-readable query (see perldoc '. __PACKAGE__ .')', type => 'string'},
818 {name => 'docache', desc => 'Flag for caching (see open-ils.search.biblio.multiclass)', type => 'object'},
821 desc => 'Search results from query, like: { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
822 type => 'object', # TODO: update as miker's new elements are included
828 sub multiclass_query {
829 # arghash only really supports limit/offset anymore
830 my($self, $conn, $arghash, $query, $docache) = @_;
834 $query =~ s/^\s+//go;
835 $query =~ s/\s+/ /go;
836 $arghash->{query} = $query
839 $logger->debug("initial search query => $query") if $query;
841 (my $method = $self->api_name) =~ s/\.query/.staged/o;
842 return $self->method_lookup($method)->dispatch($arghash, $docache);
846 __PACKAGE__->register_method(
847 method => 'cat_search_z_style_wrapper',
848 api_name => 'open-ils.search.biblio.zstyle',
850 signature => q/@see open-ils.search.biblio.multiclass/
853 __PACKAGE__->register_method(
854 method => 'cat_search_z_style_wrapper',
855 api_name => 'open-ils.search.biblio.zstyle.staff',
857 signature => q/@see open-ils.search.biblio.multiclass/
860 sub cat_search_z_style_wrapper {
863 my $authtoken = shift;
866 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
868 my $ou = $cstore->request(
869 'open-ils.cstore.direct.actor.org_unit.search',
870 { parent_ou => undef }
873 my $result = { service => 'native-evergreen-catalog', records => [] };
874 my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
876 $$searchhash{searches}{title}{term} = $$args{search}{title} if $$args{search}{title};
877 $$searchhash{searches}{author}{term} = $$args{search}{author} if $$args{search}{author};
878 $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
879 $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
880 $$searchhash{searches}{'identifier|isbn'}{term} = $$args{search}{isbn} if $$args{search}{isbn};
881 $$searchhash{searches}{'identifier|issn'}{term} = $$args{search}{issn} if $$args{search}{issn};
882 $$searchhash{searches}{'identifier|upc'}{term} = $$args{search}{upc} if $$args{search}{upc};
884 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
885 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
886 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
887 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
889 my $method = 'open-ils.search.biblio.multiclass.staged';
890 $method .= '.staff' if $self->api_name =~ /staff$/;
892 my ($list) = $self->method_lookup($method)->run( $searchhash );
894 if ($list->{count} > 0 and @{$list->{ids}}) {
895 $result->{count} = $list->{count};
897 my $records = $cstore->request(
898 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
899 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
902 for my $rec ( @$records ) {
904 my $u = OpenILS::Utils::ModsParser->new();
905 $u->start_mods_batch( $rec->marc );
906 my $mods = $u->finish_mods_batch();
908 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
914 $cstore->disconnect();
918 # ----------------------------------------------------------------------------
919 # These are the main OPAC search methods
920 # ----------------------------------------------------------------------------
922 __PACKAGE__->register_method(
923 method => 'the_quest_for_knowledge',
924 api_name => 'open-ils.search.biblio.multiclass',
926 desc => "Performs a multi class biblio or metabib search",
929 desc => "A search hash with keys: "
930 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
931 . "See perldoc " . __PACKAGE__ . " for more detail",
935 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
940 desc => 'An object of the form: '
941 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
946 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
948 The search-hash argument can have the following elements:
950 searches: { "$class" : "$value", ...} [REQUIRED]
951 org_unit: The org id to focus the search at
952 depth : The org depth
953 limit : The search limit default: 10
954 offset : The search offset default: 0
955 format : The MARC format
956 sort : What field to sort the results on? [ author | title | pubdate ]
957 sort_dir: What direction do we sort? [ asc | desc ]
958 tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
959 will be tagged with an additional value ("1") as the last value in the record ID array for
960 each record. Requires the 'authtoken'
961 authtoken : Authentication token string; When actions are performed that require a user login
962 (e.g. tagging circulated records), the authentication token is required
964 The searches element is required, must have a hashref value, and the hashref must contain at least one
965 of the following classes as a key:
973 The value paired with a key is the associated search string.
975 The docache argument enables/disables searching and saving results in cache (default OFF).
977 The return object, if successful, will look like:
979 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
983 __PACKAGE__->register_method(
984 method => 'the_quest_for_knowledge',
985 api_name => 'open-ils.search.biblio.multiclass.staff',
986 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
988 __PACKAGE__->register_method(
989 method => 'the_quest_for_knowledge',
990 api_name => 'open-ils.search.metabib.multiclass',
991 signature => q/@see open-ils.search.biblio.multiclass/
993 __PACKAGE__->register_method(
994 method => 'the_quest_for_knowledge',
995 api_name => 'open-ils.search.metabib.multiclass.staff',
996 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
999 sub the_quest_for_knowledge {
1000 my( $self, $conn, $searchhash, $docache ) = @_;
1002 return { count => 0 } unless $searchhash and
1003 ref $searchhash->{searches} eq 'HASH';
1005 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
1009 if($self->api_name =~ /metabib/) {
1011 $method =~ s/biblio/metabib/o;
1014 # do some simple sanity checking
1015 if(!$searchhash->{searches} or
1016 ( !grep { /^(?:title|author|subject|series|keyword|identifier\|is[bs]n)/ } keys %{$searchhash->{searches}} ) ) {
1017 return { count => 0 };
1020 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
1021 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
1022 my $end = $offset + $limit - 1;
1024 my $maxlimit = 5000;
1025 $searchhash->{offset} = 0; # possible user value overwritten in hash
1026 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
1028 return { count => 0 } if $offset > $maxlimit;
1031 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
1032 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
1033 my $ckey = $pfx . md5_hex($method . $s);
1035 $logger->info("bib search for: $s");
1037 $searchhash->{limit} -= $offset;
1041 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1045 $method .= ".staff" if($self->api_name =~ /staff/);
1046 $method .= ".atomic";
1048 for (keys %$searchhash) {
1049 delete $$searchhash{$_}
1050 unless defined $$searchhash{$_};
1053 $result = $U->storagereq( $method, %$searchhash );
1057 $docache = 0; # results came FROM cache, so we don't write back
1060 return {count => 0} unless ($result && $$result[0]);
1064 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1067 # If we didn't get this data from the cache, put it into the cache
1068 # then return the correct offset of records
1069 $logger->debug("putting search cache $ckey\n");
1070 put_cache($ckey, $count, \@recs);
1074 # if we have the full set of data, trim out
1075 # the requested chunk based on limit and offset
1077 for ($offset..$end) {
1078 last unless $recs[$_];
1079 push(@t, $recs[$_]);
1084 return { ids => \@recs, count => $count };
1088 __PACKAGE__->register_method(
1089 method => 'staged_search',
1090 api_name => 'open-ils.search.biblio.multiclass.staged',
1092 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1093 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1096 desc => "A search hash with keys: "
1097 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1098 . "See perldoc " . __PACKAGE__ . " for more detail",
1102 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1107 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1108 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1113 __PACKAGE__->register_method(
1114 method => 'staged_search',
1115 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1116 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1118 __PACKAGE__->register_method(
1119 method => 'staged_search',
1120 api_name => 'open-ils.search.metabib.multiclass.staged',
1121 signature => q/@see open-ils.search.biblio.multiclass.staged/
1123 __PACKAGE__->register_method(
1124 method => 'staged_search',
1125 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1126 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1129 my $estimation_strategy;
1131 my($self, $conn, $search_hash, $docache) = @_;
1133 my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1135 my $method = $IAmMetabib?
1136 'open-ils.storage.metabib.multiclass.staged.search_fts':
1137 'open-ils.storage.biblio.multiclass.staged.search_fts';
1139 $method .= '.staff' if $self->api_name =~ /staff$/;
1140 $method .= '.atomic';
1142 if (!$search_hash->{query}) {
1143 return {count => 0} unless (
1145 $search_hash->{searches} and
1146 scalar( keys %{$search_hash->{searches}} ));
1149 my $search_duration;
1150 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1151 my $user_limit = $search_hash->{limit} || 10;
1152 my $ignore_facet_classes = $search_hash->{ignore_facet_classes};
1153 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1154 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1157 # we're grabbing results on a per-superpage basis, which means the
1158 # limit and offset should coincide with superpage boundaries
1159 $search_hash->{offset} = 0;
1160 $search_hash->{limit} = $superpage_size;
1162 # force a well-known check_limit
1163 $search_hash->{check_limit} = $superpage_size;
1164 # restrict total tested to superpage size * number of superpages
1165 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1167 # Set the configured estimation strategy, defaults to 'inclusion'.
1168 unless ($estimation_strategy) {
1169 $estimation_strategy = OpenSRF::Utils::SettingsClient
1172 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1175 $search_hash->{estimation_strategy} = $estimation_strategy;
1177 # pull any existing results from the cache
1178 my $key = search_cache_key($method, $search_hash);
1179 my $facet_key = $key.'_facets';
1180 my $cache_data = $cache->get_cache($key) || {};
1182 # First, we want to make sure that someone else isn't currently trying to perform exactly
1183 # this same search. The point is to allow just one instance of a search to fill the needs
1184 # of all concurrent, identical searches. This will avoid spammy searches killing the
1185 # database without requiring admins to start locking some IP addresses out entirely.
1187 # There's still a tiny race condition where 2 might run, but without sigificantly more code
1188 # and complexity, this is close to the best we can do.
1190 if ($cache_data->{running}) { # someone is already doing the search...
1191 my $stop_looping = time() + $cache_timeout;
1192 while ( sleep(1) and time() < $stop_looping ) { # sleep for a second ... maybe they'll finish
1193 $cache_data = $cache->get_cache($key) || {};
1194 last if (!$cache_data->{running});
1196 } elsif (!$cache_data->{0}) { # we're the first ... let's give it a try
1197 $cache->put_cache($key, { running => $$ }, $cache_timeout / 3);
1200 # keep retrieving results until we find enough to
1201 # fulfill the user-specified limit and offset
1202 my $all_results = [];
1203 my $page; # current superpage
1204 my $current_page_summary = {};
1205 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1208 for($page = 0; $page < $max_superpages; $page++) {
1210 my $data = $cache_data->{$page};
1214 $logger->debug("staged search: analyzing superpage $page");
1217 # this window of results is already cached
1218 $logger->debug("staged search: found cached results");
1219 $summary = $data->{summary};
1220 $results = $data->{results};
1223 # retrieve the window of results from the database
1224 $logger->debug("staged search: fetching results from the database");
1225 $search_hash->{skip_check} = $page * $superpage_size;
1226 $search_hash->{return_query} = $page == 0 ? 1 : 0;
1229 $results = $U->storagereq($method, %$search_hash);
1230 $search_duration = time - $start;
1231 $summary = shift(@$results) if $results;
1234 $logger->info("search timed out: duration=$search_duration: params=".
1235 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1236 return {count => 0};
1239 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1241 # Create backwards-compatible result structures
1243 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}, $_->{rel}, $_->{record}]} @$results];
1245 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}]} @$results];
1248 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1249 $results = [grep {defined $_->[0]} @$results];
1250 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1253 tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib)
1254 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1256 $current_page_summary = $summary;
1258 # add the new set of results to the set under construction
1259 push(@$all_results, @$results);
1261 my $current_count = scalar(@$all_results);
1263 if ($page == 0) { # all summaries are the same, just get the first
1264 for (keys %$summary) {
1265 $global_summary->{$_} = $summary->{$_};
1269 # we've found all the possible hits
1270 last if $current_count == $summary->{visible};
1272 # we've found enough results to satisfy the requested limit/offset
1273 last if $current_count >= ($user_limit + $user_offset);
1275 # we've scanned all possible hits
1276 last if($summary->{checked} < $superpage_size);
1279 # Let other backends grab our data now that we're done.
1280 $cache_data = $cache->get_cache($key);
1281 if ($$cache_data{running} and $$cache_data{running} == $$) {
1282 delete $$cache_data{running};
1283 $cache->put_cache($key, $cache_data, $cache_timeout);
1286 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1288 $conn->respond_complete(
1290 global_summary => $global_summary,
1291 count => $global_summary->{visible},
1292 core_limit => $search_hash->{core_limit},
1294 superpage_size => $search_hash->{check_limit},
1295 superpage_summary => $current_page_summary,
1296 facet_key => $facet_key,
1301 $logger->info("Completed canonicalized search is: $$global_summary{canonicalized_query}");
1303 return cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1306 sub fetch_display_fields {
1309 my $highlight_map = shift;
1313 $conn->respond_complete;
1317 my $hl_map_string = "";
1318 if (ref($highlight_map) =~ /HASH/) {
1319 for my $tsq (keys %$highlight_map) {
1320 my $field_list = join(',', @{$$highlight_map{$tsq}});
1321 $hl_map_string .= ' || ' if $hl_map_string;
1322 $hl_map_string .= "hstore(($tsq)\:\:TEXT,'$field_list')";
1326 my $e = new_editor();
1328 for my $record ( @records ) {
1329 next unless ($record && $hl_map_string);
1332 {from => ['search.highlight_display_fields', $record, $hl_map_string]}
1339 __PACKAGE__->register_method(
1340 method => 'fetch_display_fields',
1341 api_name => 'open-ils.search.fetch.metabib.display_field.highlight',
1346 sub tag_circulated_records {
1347 my ($auth, $results, $metabib) = @_;
1348 my $e = new_editor(authtoken => $auth);
1349 return $results unless $e->checkauth;
1352 select => { acn => [{ column => 'record', alias => 'tagme' }] },
1353 from => { auch => { acp => { join => 'acn' }} },
1354 where => { usr => $e->requestor->id },
1360 select => { mmrsm => [{ column => 'metarecord', alias => 'tagme' }] },
1362 where => { source => { in => $query } },
1367 # Give me the distinct set of bib records that exist in the user's visible circulation history
1368 my $circ_recs = $e->json_query( $query );
1370 # if the record appears in the circ history, push a 1 onto
1371 # the rec array structure to indicate truthiness
1372 for my $rec (@$results) {
1373 push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1379 # creates a unique token to represent the query in the cache
1380 sub search_cache_key {
1382 my $search_hash = shift;
1384 for my $key (sort keys %$search_hash) {
1385 push(@sorted, ($key => $$search_hash{$key}))
1386 unless $key eq 'limit' or
1388 $key eq 'skip_check';
1390 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1391 return $pfx . md5_hex($method . $s);
1394 sub retrieve_cached_facets {
1400 return undef unless ($key and $key =~ /_facets$/);
1403 local $SIG{ALRM} = sub {die};
1404 alarm(10); # we'll sleep for as much as 10s
1406 die if $cache->get_cache($key . '_COMPLETE');
1407 } while (sleep(0.05));
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 my $facets_function = $metabib ? 'search.facets_for_metarecord_set'
1464 : 'search.facets_for_record_set';
1465 my $results_str = '{' . join(',', @$results) . '}';
1466 my $ignore_str = ref($ignore) ? '{' . join(',', @$ignore) . '}'
1469 from => [ $facets_function, $ignore_str, $results_str ]
1472 my $facets = OpenILS::Utils::CStoreEditor->new->json_query($query, {substream => 1});
1474 for my $facet (@$facets) {
1475 next unless ($facet->{value});
1476 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1479 $logger->info("facet compilation: cached with key=$key");
1481 $cache->put_cache($key, $data, $cache_timeout);
1482 $cache->put_cache($key.'_COMPLETE', 1, $cache_timeout);
1485 sub cache_staged_search_page {
1486 # puts this set of results into the cache
1487 my($key, $page, $summary, $results) = @_;
1488 my $data = $cache->get_cache($key);
1491 summary => $summary,
1495 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1496 ($summary->{estimated_hit_count} || "none") .
1497 ", visible=" . ($summary->{visible} || "none")
1500 $cache->put_cache($key, $data, $cache_timeout);
1508 my $start = $offset;
1509 my $end = $offset + $limit - 1;
1511 $logger->debug("searching cache for $key : $start..$end\n");
1513 return undef unless $cache;
1514 my $data = $cache->get_cache($key);
1516 return undef unless $data;
1518 my $count = $data->[0];
1521 return undef unless $offset < $count;
1524 for( my $i = $offset; $i <= $end; $i++ ) {
1525 last unless my $d = $$data[$i];
1526 push( @result, $d );
1529 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1536 my( $key, $count, $data ) = @_;
1537 return undef unless $cache;
1538 $logger->debug("search_cache putting ".
1539 scalar(@$data)." items at key $key with timeout $cache_timeout");
1540 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1544 __PACKAGE__->register_method(
1545 method => "biblio_mrid_to_modsbatch_batch",
1546 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1549 sub biblio_mrid_to_modsbatch_batch {
1550 my( $self, $client, $mrids) = @_;
1551 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1553 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1554 for my $id (@$mrids) {
1555 next unless defined $id;
1556 my ($m) = $method->run($id);
1563 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1564 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1566 __PACKAGE__->register_method(
1567 method => "biblio_mrid_to_modsbatch",
1570 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1571 . "As usual, the .staff version of this method will include otherwise hidden records.",
1573 { desc => 'Metarecord ID', type => 'number' },
1574 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1577 desc => 'MVR Object, event on error',
1583 sub biblio_mrid_to_modsbatch {
1584 my( $self, $client, $mrid, $args) = @_;
1586 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1588 my ($mr, $evt) = _grab_metarecord($mrid);
1589 return $evt unless $mr;
1591 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1592 biblio_mrid_make_modsbatch($self, $client, $mr);
1594 return $mvr unless ref($args);
1596 # Here we find the lead record appropriate for the given filters
1597 # and use that for the title and author of the metarecord
1598 my $format = $$args{format};
1599 my $org = $$args{org};
1600 my $depth = $$args{depth};
1602 return $mvr unless $format or $org or $depth;
1604 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1605 $method = "$method.staff" if $self->api_name =~ /staff/o;
1607 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1609 if( my $mods = $U->record_to_mvr($rec) ) {
1611 $mvr->title( $mods->title );
1612 $mvr->author($mods->author);
1613 $logger->debug("mods_slim updating title and ".
1614 "author in mvr with ".$mods->title." : ".$mods->author);
1620 # converts a metarecord to an mvr
1623 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1624 return Fieldmapper::metabib::virtual_record->new($perl);
1627 # checks to see if a metarecord has mods, if so returns true;
1629 __PACKAGE__->register_method(
1630 method => "biblio_mrid_check_mvr",
1631 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1632 notes => "Takes a metarecord ID or a metarecord object and returns true "
1633 . "if the metarecord already has an mvr associated with it."
1636 sub biblio_mrid_check_mvr {
1637 my( $self, $client, $mrid ) = @_;
1641 if(ref($mrid)) { $mr = $mrid; }
1642 else { ($mr, $evt) = _grab_metarecord($mrid); }
1643 return $evt if $evt;
1645 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1647 return _mr_to_mvr($mr) if $mr->mods();
1651 sub _grab_metarecord {
1653 my $e = new_editor();
1654 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1659 __PACKAGE__->register_method(
1660 method => "biblio_mrid_make_modsbatch",
1661 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1662 notes => "Takes either a metarecord ID or a metarecord object. "
1663 . "Forces the creations of an mvr for the given metarecord. "
1664 . "The created mvr is returned."
1667 sub biblio_mrid_make_modsbatch {
1668 my( $self, $client, $mrid ) = @_;
1670 my $e = new_editor();
1677 $mr = $e->retrieve_metabib_metarecord($mrid)
1678 or return $e->event;
1681 my $masterid = $mr->master_record;
1682 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1684 my $ids = $U->storagereq(
1685 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1686 return undef unless @$ids;
1688 my $master = $e->retrieve_biblio_record_entry($masterid)
1689 or return $e->event;
1691 # start the mods batch
1692 my $u = OpenILS::Utils::ModsParser->new();
1693 $u->start_mods_batch( $master->marc );
1695 # grab all of the sub-records and shove them into the batch
1696 my @ids = grep { $_ ne $masterid } @$ids;
1697 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1702 my $r = $e->retrieve_biblio_record_entry($i);
1703 push( @$subrecs, $r ) if $r;
1708 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1709 $u->push_mods_batch( $_->marc ) if $_->marc;
1713 # finish up and send to the client
1714 my $mods = $u->finish_mods_batch();
1715 $mods->doc_id($mrid);
1716 $client->respond_complete($mods);
1719 # now update the mods string in the db
1720 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1723 $e = new_editor(xact => 1);
1724 $e->update_metabib_metarecord($mr)
1725 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1732 # converts a mr id into a list of record ids
1734 foreach (qw/open-ils.search.biblio.metarecord_to_records
1735 open-ils.search.biblio.metarecord_to_records.staff/)
1737 __PACKAGE__->register_method(
1738 method => "biblio_mrid_to_record_ids",
1741 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1742 . "As usual, the .staff version of this method will include otherwise hidden records.",
1744 { desc => 'Metarecord ID', type => 'number' },
1745 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1748 desc => 'Results object like {count => $i, ids =>[...]}',
1756 sub biblio_mrid_to_record_ids {
1757 my( $self, $client, $mrid, $args ) = @_;
1759 my $format = $$args{format};
1760 my $org = $$args{org};
1761 my $depth = $$args{depth};
1763 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1764 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1765 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1767 return { count => scalar(@$recs), ids => $recs };
1771 __PACKAGE__->register_method(
1772 method => "biblio_record_to_marc_html",
1773 api_name => "open-ils.search.biblio.record.html"
1776 __PACKAGE__->register_method(
1777 method => "biblio_record_to_marc_html",
1778 api_name => "open-ils.search.authority.to_html"
1781 # Persistent parsers and setting objects
1782 my $parser = XML::LibXML->new();
1783 my $xslt = XML::LibXSLT->new();
1785 my $slim_marc_sheet;
1786 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1788 sub biblio_record_to_marc_html {
1789 my($self, $client, $recordid, $slim, $marcxml) = @_;
1792 my $dir = $settings_client->config_value("dirs", "xsl");
1795 unless($slim_marc_sheet) {
1796 my $xsl = $settings_client->config_value(
1797 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1799 $xsl = $parser->parse_file("$dir/$xsl");
1800 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1803 $sheet = $slim_marc_sheet;
1807 unless($marc_sheet) {
1808 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1809 my $xsl = $settings_client->config_value(
1810 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1811 $xsl = $parser->parse_file("$dir/$xsl");
1812 $marc_sheet = $xslt->parse_stylesheet($xsl);
1814 $sheet = $marc_sheet;
1819 my $e = new_editor();
1820 if($self->api_name =~ /authority/) {
1821 $record = $e->retrieve_authority_record_entry($recordid)
1822 or return $e->event;
1824 $record = $e->retrieve_biblio_record_entry($recordid)
1825 or return $e->event;
1827 $marcxml = $record->marc;
1830 my $xmldoc = $parser->parse_string($marcxml);
1831 my $html = $sheet->transform($xmldoc);
1832 return $html->documentElement->toString();
1835 __PACKAGE__->register_method(
1836 method => "format_biblio_record_entry",
1837 api_name => "open-ils.search.biblio.record.print",
1839 desc => 'Returns a printable version of the specified bib record',
1841 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1844 desc => q/An action_trigger.event object or error event./,
1849 __PACKAGE__->register_method(
1850 method => "format_biblio_record_entry",
1851 api_name => "open-ils.search.biblio.record.email",
1853 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
1855 { desc => 'Authentication token', type => 'string'},
1856 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1859 desc => q/Undefined on success, otherwise an error event./,
1865 sub format_biblio_record_entry {
1866 my($self, $conn, $arg1, $arg2) = @_;
1868 my $for_print = ($self->api_name =~ /print/);
1869 my $for_email = ($self->api_name =~ /email/);
1871 my $e; my $auth; my $bib_id; my $context_org;
1875 $context_org = $arg2 || $U->get_org_tree->id;
1876 $e = new_editor(xact => 1);
1877 } elsif ($for_email) {
1880 $e = new_editor(authtoken => $auth, xact => 1);
1881 return $e->die_event unless $e->checkauth;
1882 $context_org = $e->requestor->home_ou;
1886 if (ref $bib_id ne 'ARRAY') {
1887 $bib_ids = [ $bib_id ];
1892 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1893 $bucket->btype('temp');
1894 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1896 $bucket->owner($e->requestor)
1900 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1902 for my $id (@$bib_ids) {
1904 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1906 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1907 $bucket_entry->target_biblio_record_entry($bib);
1908 $bucket_entry->bucket($bucket_obj->id);
1909 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1916 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1918 } elsif ($for_email) {
1920 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1927 __PACKAGE__->register_method(
1928 method => "retrieve_all_copy_statuses",
1929 api_name => "open-ils.search.config.copy_status.retrieve.all"
1932 sub retrieve_all_copy_statuses {
1933 my( $self, $client ) = @_;
1934 return new_editor()->retrieve_all_config_copy_status();
1938 __PACKAGE__->register_method(
1939 method => "copy_counts_per_org",
1940 api_name => "open-ils.search.biblio.copy_counts.retrieve"
1943 __PACKAGE__->register_method(
1944 method => "copy_counts_per_org",
1945 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1948 sub copy_counts_per_org {
1949 my( $self, $client, $record_id ) = @_;
1951 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1953 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1954 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1956 my $counts = $apputils->simple_scalar_request(
1957 "open-ils.storage", $method, $record_id );
1959 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1964 __PACKAGE__->register_method(
1965 method => "copy_count_summary",
1966 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1967 notes => "returns an array of these: "
1968 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
1969 . "where statusx is a copy status name. The statuses are sorted by ID.",
1973 sub copy_count_summary {
1974 my( $self, $client, $rid, $org, $depth ) = @_;
1977 my $data = $U->storagereq(
1978 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1981 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
1983 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
1987 __PACKAGE__->register_method(
1988 method => "copy_location_count_summary",
1989 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1990 notes => "returns an array of these: "
1991 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
1992 . "where statusx is a copy status name. The statuses are sorted by ID.",
1995 sub copy_location_count_summary {
1996 my( $self, $client, $rid, $org, $depth ) = @_;
1999 my $data = $U->storagereq(
2000 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2003 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2005 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2007 || $a->[4] cmp $b->[4]
2011 __PACKAGE__->register_method(
2012 method => "copy_count_location_summary",
2013 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2014 notes => "returns an array of these: "
2015 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2016 . "where statusx is a copy status name. The statuses are sorted by ID."
2019 sub copy_count_location_summary {
2020 my( $self, $client, $rid, $org, $depth ) = @_;
2023 my $data = $U->storagereq(
2024 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2026 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2028 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2033 foreach (qw/open-ils.search.biblio.marc
2034 open-ils.search.biblio.marc.staff/)
2036 __PACKAGE__->register_method(
2037 method => "marc_search",
2040 desc => 'Fetch biblio IDs based on MARC record criteria. '
2041 . 'As usual, the .staff version of the search includes otherwise hidden records',
2044 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2045 'See perldoc ' . __PACKAGE__ . ' for more detail.',
2048 {desc => 'timeout (optional)', type => 'number'}
2051 desc => 'Results object like: { "count": $i, "ids": [...] }',
2058 =head3 open-ils.search.biblio.marc (arghash, timeout)
2060 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
2062 searches: complex query object (required)
2063 org_unit: The org ID to focus the search at
2064 depth : The org depth
2065 limit : integer search limit default: 10
2066 offset : integer search offset default: 0
2067 sort : What field to sort the results on? [ author | title | pubdate ]
2068 sort_dir: In what direction do we sort? [ asc | desc ]
2070 Additional keys to refine search criteria:
2073 language : Language (code)
2074 lit_form : Literary form
2075 item_form: Item form
2076 item_type: Item type
2077 format : The MARC format
2079 Please note that the specific strings to be used in the "addtional keys" will be entirely
2080 dependent on your loaded data.
2082 All keys except "searches" are optional.
2083 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
2085 For example, an arg hash might look like:
2107 The arghash is eventually passed to the SRF call:
2108 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2110 Presently, search uses the cache unconditionally.
2114 # FIXME: that example above isn't actually tested.
2115 # FIXME: sort and limit added. item_type not tested yet.
2116 # TODO: docache option?
2118 my( $self, $conn, $args, $timeout ) = @_;
2120 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2121 $method .= ".staff" if $self->api_name =~ /staff/;
2122 $method .= ".atomic";
2124 my $limit = $args->{limit} || 10;
2125 my $offset = $args->{offset} || 0;
2127 # allow caller to pass in a call timeout since MARC searches
2128 # can take longer than the default 60-second timeout.
2129 # Default to 2 mins. Arbitrarily cap at 5 mins.
2130 $timeout = 120 if !$timeout or $timeout > 300;
2133 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2134 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2136 my $recs = search_cache($ckey, $offset, $limit);
2140 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2141 my $req = $ses->request($method, %$args);
2142 my $resp = $req->recv($timeout);
2144 if($resp and $recs = $resp->content) {
2145 put_cache($ckey, scalar(@$recs), $recs);
2154 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2155 my @recs = map { $_->[0] } @$recs;
2157 return { ids => \@recs, count => $count };
2161 foreach my $isbn_method (qw/
2162 open-ils.search.biblio.isbn
2163 open-ils.search.biblio.isbn.staff
2165 __PACKAGE__->register_method(
2166 method => "biblio_search_isbn",
2167 api_name => $isbn_method,
2169 desc => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2171 {desc => 'ISBN', type => 'string'}
2174 desc => 'Results object like: { "count": $i, "ids": [...] }',
2181 sub biblio_search_isbn {
2182 my( $self, $client, $isbn ) = @_;
2183 $logger->debug("Searching ISBN $isbn");
2184 # the previous implementation of this method was essentially unlimited,
2185 # so we will set our limit very high and let multiclass.query provide any
2187 # XXX: if making this unlimited is deemed important, we might consider
2188 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2189 # which is functionally deprecated at this point, or a custom call to
2190 # 'open-ils.storage.biblio.multiclass.search_fts'
2192 my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2193 if ($self->api_name =~ m/.staff$/) {
2194 $isbn_method .= '.staff';
2197 my $method = $self->method_lookup($isbn_method);
2198 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2199 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2200 return { ids => \@recs, count => $search_result->{'count'} };
2203 __PACKAGE__->register_method(
2204 method => "biblio_search_isbn_batch",
2205 api_name => "open-ils.search.biblio.isbn_list",
2208 # XXX: see biblio_search_isbn() for note concerning 'limit'
2209 sub biblio_search_isbn_batch {
2210 my( $self, $client, $isbn_list ) = @_;
2211 $logger->debug("Searching ISBNs @$isbn_list");
2212 my @recs = (); my %rec_set = ();
2213 my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2214 foreach my $isbn ( @$isbn_list ) {
2215 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2216 my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2217 foreach my $rec (@recs_subset) {
2218 if (! $rec_set{ $rec }) {
2219 $rec_set{ $rec } = 1;
2224 return { ids => \@recs, count => scalar(@recs) };
2227 foreach my $issn_method (qw/
2228 open-ils.search.biblio.issn
2229 open-ils.search.biblio.issn.staff
2231 __PACKAGE__->register_method(
2232 method => "biblio_search_issn",
2233 api_name => $issn_method,
2235 desc => 'Retrieve biblio IDs for a given ISSN',
2237 {desc => 'ISBN', type => 'string'}
2240 desc => 'Results object like: { "count": $i, "ids": [...] }',
2247 sub biblio_search_issn {
2248 my( $self, $client, $issn ) = @_;
2249 $logger->debug("Searching ISSN $issn");
2250 # the previous implementation of this method was essentially unlimited,
2251 # so we will set our limit very high and let multiclass.query provide any
2253 # XXX: if making this unlimited is deemed important, we might consider
2254 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2255 # which is functionally deprecated at this point, or a custom call to
2256 # 'open-ils.storage.biblio.multiclass.search_fts'
2258 my $issn_method = 'open-ils.search.biblio.multiclass.query';
2259 if ($self->api_name =~ m/.staff$/) {
2260 $issn_method .= '.staff';
2263 my $method = $self->method_lookup($issn_method);
2264 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2265 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2266 return { ids => \@recs, count => $search_result->{'count'} };
2270 __PACKAGE__->register_method(
2271 method => "fetch_mods_by_copy",
2272 api_name => "open-ils.search.biblio.mods_from_copy",
2275 desc => 'Retrieve MODS record given an attached copy ID',
2277 { desc => 'Copy ID', type => 'number' }
2280 desc => 'MODS record, event on error or uncataloged item'
2285 sub fetch_mods_by_copy {
2286 my( $self, $client, $copyid ) = @_;
2287 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2288 return $evt if $evt;
2289 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2290 return $apputils->record_to_mvr($record);
2294 # -------------------------------------------------------------------------------------
2296 __PACKAGE__->register_method(
2297 method => "cn_browse",
2298 api_name => "open-ils.search.callnumber.browse.target",
2299 notes => "Starts a callnumber browse"
2302 __PACKAGE__->register_method(
2303 method => "cn_browse",
2304 api_name => "open-ils.search.callnumber.browse.page_up",
2305 notes => "Returns the previous page of callnumbers",
2308 __PACKAGE__->register_method(
2309 method => "cn_browse",
2310 api_name => "open-ils.search.callnumber.browse.page_down",
2311 notes => "Returns the next page of callnumbers",
2315 # RETURNS array of arrays like so: label, owning_lib, record, id
2317 my( $self, $client, @params ) = @_;
2320 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2321 if( $self->api_name =~ /target/ );
2322 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2323 if( $self->api_name =~ /page_up/ );
2324 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2325 if( $self->api_name =~ /page_down/ );
2327 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2329 # -------------------------------------------------------------------------------------
2331 __PACKAGE__->register_method(
2332 method => "fetch_cn",
2333 api_name => "open-ils.search.callnumber.retrieve",
2335 notes => "retrieves a callnumber based on ID",
2339 my( $self, $client, $id ) = @_;
2341 my $e = new_editor();
2342 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2343 return $evt if $evt;
2347 __PACKAGE__->register_method(
2348 method => "fetch_fleshed_cn",
2349 api_name => "open-ils.search.callnumber.fleshed.retrieve",
2351 notes => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2354 sub fetch_fleshed_cn {
2355 my( $self, $client, $id ) = @_;
2357 my $e = new_editor();
2358 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2359 return $evt if $evt;
2364 __PACKAGE__->register_method(
2365 method => "fetch_copy_by_cn",
2366 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2368 Returns an array of copy ID's by callnumber ID
2369 @param cnid The callnumber ID
2370 @return An array of copy IDs
2374 sub fetch_copy_by_cn {
2375 my( $self, $conn, $cnid ) = @_;
2376 return $U->cstorereq(
2377 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2378 { call_number => $cnid, deleted => 'f' } );
2381 __PACKAGE__->register_method(
2382 method => 'fetch_cn_by_info',
2383 api_name => 'open-ils.search.call_number.retrieve_by_info',
2385 @param label The callnumber label
2386 @param record The record the cn is attached to
2387 @param org The owning library of the cn
2388 @return The callnumber object
2393 sub fetch_cn_by_info {
2394 my( $self, $conn, $label, $record, $org ) = @_;
2395 return $U->cstorereq(
2396 'open-ils.cstore.direct.asset.call_number.search',
2397 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2402 __PACKAGE__->register_method(
2403 method => 'bib_extras',
2404 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2407 __PACKAGE__->register_method(
2408 method => 'bib_extras',
2409 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2410 ctype => 'item_form'
2412 __PACKAGE__->register_method(
2413 method => 'bib_extras',
2414 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2415 ctype => 'item_type',
2417 __PACKAGE__->register_method(
2418 method => 'bib_extras',
2419 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2420 ctype => 'bib_level'
2422 __PACKAGE__->register_method(
2423 method => 'bib_extras',
2424 api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2430 $logger->warn("deprecation warning: " .$self->api_name);
2432 my $e = new_editor();
2434 my $ctype = $self->{ctype};
2435 my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2438 for my $ccvm (@$ccvms) {
2439 my $obj = "Fieldmapper::config::${ctype}_map"->new;
2440 $obj->value($ccvm->value);
2441 $obj->code($ccvm->code);
2442 $obj->description($ccvm->description) if $obj->can('description');
2451 __PACKAGE__->register_method(
2452 method => 'fetch_slim_record',
2453 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2455 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2457 { desc => 'Array of Record IDs', type => 'array' }
2460 desc => 'Array of biblio records, event on error'
2465 sub fetch_slim_record {
2466 my( $self, $conn, $ids ) = @_;
2468 my $editor = new_editor();
2471 return $editor->event unless
2472 my $r = $editor->retrieve_biblio_record_entry($_);
2479 __PACKAGE__->register_method(
2480 method => 'rec_hold_parts',
2481 api_name => 'open-ils.search.biblio.record_hold_parts',
2483 Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2487 sub rec_hold_parts {
2488 my( $self, $conn, $args ) = @_;
2490 my $rec = $$args{record};
2491 my $mrec = $$args{metarecord};
2492 my $pickup_lib = $$args{pickup_lib};
2493 my $e = new_editor();
2496 select => {bmp => ['id', 'label']},
2501 select => {'acpm' => ['part']},
2502 from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2504 '+acp' => {'deleted' => 'f'},
2505 '+bre' => {id => $rec}
2512 order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2515 if(defined $pickup_lib) {
2516 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2517 if($hard_boundary) {
2518 my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2519 $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2523 return $e->json_query($query);
2529 __PACKAGE__->register_method(
2530 method => 'rec_to_mr_rec_descriptors',
2531 api_name => 'open-ils.search.metabib.record_to_descriptors',
2533 specialized method...
2534 Given a biblio record id or a metarecord id,
2535 this returns a list of metabib.record_descriptor
2536 objects that live within the same metarecord
2537 @param args Object of args including:
2541 sub rec_to_mr_rec_descriptors {
2542 my( $self, $conn, $args ) = @_;
2544 my $rec = $$args{record};
2545 my $mrec = $$args{metarecord};
2546 my $item_forms = $$args{item_forms};
2547 my $item_types = $$args{item_types};
2548 my $item_lang = $$args{item_lang};
2549 my $pickup_lib = $$args{pickup_lib};
2551 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2553 my $e = new_editor();
2557 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2558 return $e->event unless @$map;
2559 $mrec = $$map[0]->metarecord;
2562 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2563 return $e->event unless @$recs;
2565 my @recs = map { $_->source } @$recs;
2566 my $search = { record => \@recs };
2567 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2568 $search->{item_type} = $item_types if $item_types and @$item_types;
2569 $search->{item_lang} = $item_lang if $item_lang;
2571 my $desc = $e->search_metabib_record_descriptor($search);
2575 select => { 'bre' => ['id'] },
2580 'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2586 '+bre' => { id => \@recs },
2591 "+ccs" => { holdable => 't' },
2592 "+acpl" => { holdable => 't', deleted => 'f' }
2596 if ($hard_boundary) { # 0 (or "top") is the same as no setting
2597 my $orgs = $e->json_query(
2598 { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2599 ) or return $e->die_event;
2601 $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2604 my $good_records = $e->json_query($query) or return $e->die_event;
2607 for my $d (@$desc) {
2608 if ( grep { $d->record == $_->{id} } @$good_records ) {
2615 return { metarecord => $mrec, descriptors => $desc };
2619 __PACKAGE__->register_method(
2620 method => 'fetch_age_protect',
2621 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2624 sub fetch_age_protect {
2625 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2629 __PACKAGE__->register_method(
2630 method => 'copies_by_cn_label',
2631 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2634 __PACKAGE__->register_method(
2635 method => 'copies_by_cn_label',
2636 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2639 sub copies_by_cn_label {
2640 my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2641 my $e = new_editor();
2642 my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2643 my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2644 my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2645 return [] unless @$cns;
2647 # show all non-deleted copies in the staff client ...
2648 if ($self->api_name =~ /staff$/o) {
2649 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2652 # ... otherwise, grab the copies ...
2653 my $copies = $e->search_asset_copy(
2654 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2655 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2659 # ... and test for location and status visibility
2660 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2663 __PACKAGE__->register_method(
2664 method => 'bib_copies',
2665 api_name => 'open-ils.search.bib.copies',
2668 __PACKAGE__->register_method(
2669 method => 'bib_copies',
2670 api_name => 'open-ils.search.bib.copies.staff',
2675 my ($self, $client, $rec_id, $org, $depth, $limit, $offset, $pref_ou) = @_;
2676 my $is_staff = ($self->api_name =~ /staff/);
2678 my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
2679 my $req = $cstore->request(
2680 'open-ils.cstore.json_query', mk_copy_query(
2681 $rec_id, $org, $depth, $limit, $offset, $pref_ou, $is_staff));
2684 while ($resp = $req->recv) {
2685 $client->respond($resp->content);
2691 # TODO: this comes almost directly from WWW/EGCatLoader/Record.pm
2697 my $copy_limit = shift;
2698 my $copy_offset = shift;
2699 my $pref_ou = shift;
2700 my $is_staff = shift;
2702 my $query = $U->basic_opac_copy_query(
2703 $rec_id, undef, undef, $copy_limit, $copy_offset, $is_staff
2706 if ($org) { # TODO: root org test
2707 # no need to add the org join filter if we're not actually filtering
2708 $query->{from}->{acp}->[1] = { aou => {
2714 select => {aou => [{
2716 transform => 'actor.org_unit_descendants',
2717 result_field => 'id',
2721 where => {id => $org}
2728 # Unsure if we want these in the shared function, leaving here for now
2729 unshift(@{$query->{order_by}},
2730 { class => "aou", field => 'id',
2731 transform => 'evergreen.rank_ou', params => [$org, $pref_ou]
2734 push(@{$query->{order_by}},
2735 { class => "acp", field => 'id',
2736 transform => 'evergreen.rank_cp'
2744 __PACKAGE__->register_method(
2745 method => 'catalog_record_summary',
2746 api_name => 'open-ils.search.biblio.record.catalog_summary',
2748 max_bundle_count => 1,
2750 desc => 'Stream of record data suitable for catalog display',
2752 {desc => 'Context org unit ID', type => 'number'},
2753 {desc => 'Array of Record IDs', type => 'array'}
2757 Stream of record summary objects including id, record,
2758 hold_count, copy_counts, display (metabib display
2759 fields), attributes (metabib record attrs), plus
2760 metabib_id and metabib_records for the metabib variant.
2765 __PACKAGE__->register_method(
2766 method => 'catalog_record_summary',
2767 api_name => 'open-ils.search.biblio.record.catalog_summary.staff',
2769 max_bundle_count => 1,
2770 signature => q/see open-ils.search.biblio.record.catalog_summary/
2772 __PACKAGE__->register_method(
2773 method => 'catalog_record_summary',
2774 api_name => 'open-ils.search.biblio.metabib.catalog_summary',
2776 max_bundle_count => 1,
2777 signature => q/see open-ils.search.biblio.record.catalog_summary/
2780 __PACKAGE__->register_method(
2781 method => 'catalog_record_summary',
2782 api_name => 'open-ils.search.biblio.metabib.catalog_summary.staff',
2784 max_bundle_count => 1,
2785 signature => q/see open-ils.search.biblio.record.catalog_summary/
2789 sub catalog_record_summary {
2790 my ($self, $client, $org_id, $record_ids) = @_;
2791 my $e = new_editor();
2793 my $is_meta = ($self->api_name =~ /metabib/);
2794 my $is_staff = ($self->api_name =~ /staff/);
2796 my $holds_method = $is_meta ?
2797 'open-ils.circ.mmr.holds.count' :
2798 'open-ils.circ.bre.holds.count';
2800 my $copy_method = $is_meta ?
2801 'open-ils.search.biblio.metarecord.copy_count':
2802 'open-ils.search.biblio.record.copy_count';
2804 $copy_method .= '.staff' if $is_staff;
2806 $copy_method = $self->method_lookup($copy_method); # local method
2808 for my $rec_id (@$record_ids) {
2810 my $response = $is_meta ?
2811 get_one_metarecord_summary($e, $rec_id) :
2812 get_one_record_summary($e, $rec_id);
2814 ($response->{copy_counts}) = $copy_method->run($org_id, $rec_id);
2816 $response->{hold_count} =
2817 $U->simplereq('open-ils.circ', $holds_method, $rec_id);
2819 $client->respond($response);
2825 # Start with a bib summary and augment the data with additional
2826 # metarecord content.
2827 sub get_one_metarecord_summary {
2828 my ($e, $rec_id) = @_;
2830 my $meta = $e->retrieve_metabib_metarecord($rec_id) or return {};
2831 my $maps = $e->search_metabib_metarecord_source_map({metarecord => $rec_id});
2833 my $bre_id = $meta->master_record;
2835 my $response = get_one_record_summary($e, $bre_id);
2837 $response->{metabib_id} = $rec_id;
2838 $response->{metabib_records} = [map {$_->source} @$maps];
2840 my @other_bibs = map {$_->source} grep {$_->source != $bre_id} @$maps;
2842 # Augment the record attributes with those of all of the records
2843 # linked to this metarecord.
2845 my $attrs = $e->search_metabib_record_attr_flat({id => \@other_bibs});
2847 my $attributes = $response->{attributes};
2849 for my $attr (@$attrs) {
2850 $attributes->{$attr->attr} = [] unless $attributes->{$attr->attr};
2851 push(@{$attributes->{$attr->attr}}, $attr->value) # avoid dupes
2852 unless grep {$_ eq $attr->value} @{$attributes->{$attr->attr}};
2859 sub get_one_record_summary {
2860 my ($e, $rec_id) = @_;
2862 my $bre = $e->retrieve_biblio_record_entry([$rec_id, {
2865 bre => [qw/compressed_display_entries mattrs creator editor/]
2869 # Compressed display fields are pachaged as JSON
2871 $display->{$_->name} = OpenSRF::Utils::JSON->JSON2perl($_->value)
2872 foreach @{$bre->compressed_display_entries};
2874 # Create an object of 'mraf' attributes.
2875 # Any attribute can be multi so dedupe and array-ify all of them.
2876 my $attributes = {};
2877 for my $attr (@{$bre->mattrs}) {
2878 $attributes->{$attr->attr} = {} unless $attributes->{$attr->attr};
2879 $attributes->{$attr->attr}->{$attr->value} = 1; # avoid dupes
2881 $attributes->{$_} = [keys %{$attributes->{$_}}] for keys %$attributes;
2886 $bre->clear_compressed_display_entries;
2891 display => $display,
2892 attributes => $attributes