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;
16 use OpenSRF::Utils::Logger qw/:logger/;
18 use Time::HiRes qw(time sleep);
19 use OpenSRF::EX qw(:try);
20 use Digest::MD5 qw(md5_hex);
26 $Data::Dumper::Indent = 0;
28 use OpenILS::Const qw/:const/;
30 use OpenILS::Application::AppUtils;
31 my $apputils = "OpenILS::Application::AppUtils";
34 my $pfx = "open-ils.search_";
40 my $max_concurrent_search;
43 $cache = OpenSRF::Utils::Cache->new('global');
44 my $sclient = OpenSRF::Utils::SettingsClient->new();
45 $cache_timeout = $sclient->config_value(
46 "apps", "open-ils.search", "app_settings", "cache_timeout" ) || 300;
48 $superpage_size = $sclient->config_value(
49 "apps", "open-ils.search", "app_settings", "superpage_size" ) || 500;
51 $max_superpages = $sclient->config_value(
52 "apps", "open-ils.search", "app_settings", "max_superpages" ) || 20;
54 $logger->info("Search cache timeout is $cache_timeout, ".
55 " superpage_size is $superpage_size, max_superpages is $max_superpages");
60 # ---------------------------------------------------------------------------
61 # takes a list of record id's and turns the docs into friendly
62 # mods structures. Creates one MODS structure for each doc id.
63 # ---------------------------------------------------------------------------
64 sub _records_to_mods {
70 my $session = OpenSRF::AppSession->create("open-ils.cstore");
71 my $request = $session->request(
72 "open-ils.cstore.direct.biblio.record_entry.search", { id => \@ids } );
74 while( my $resp = $request->recv ) {
75 my $content = $resp->content;
76 next if $content->id == OILS_PRECAT_RECORD;
77 my $u = OpenILS::Utils::ModsParser->new(); # FIXME: we really need a new parser for each object?
78 $u->start_mods_batch( $content->marc );
79 my $mods = $u->finish_mods_batch();
80 $mods->doc_id($content->id());
81 $mods->tcn($content->tcn_value);
85 $session->disconnect();
89 __PACKAGE__->register_method(
90 method => "record_id_to_mods",
91 api_name => "open-ils.search.biblio.record.mods.retrieve",
94 desc => "Provide ID, we provide the MODS object with copy count. "
95 . "Note: this method does NOT take an array of IDs like mods_slim.retrieve", # FIXME: do it here too
97 { desc => 'Record ID', type => 'number' }
100 desc => 'MODS object', type => 'object'
105 # converts a record into a mods object with copy counts attached
106 sub record_id_to_mods {
108 my( $self, $client, $org_id, $id ) = @_;
110 my $mods_list = _records_to_mods( $id );
111 my $mods_obj = $mods_list->[0];
112 my $cmethod = $self->method_lookup("open-ils.search.biblio.record.copy_count");
113 my ($count) = $cmethod->run($org_id, $id);
114 $mods_obj->copy_count($count);
121 __PACKAGE__->register_method(
122 method => "record_id_to_mods_slim",
123 api_name => "open-ils.search.biblio.record.mods_slim.retrieve",
127 desc => "Provide ID(s), we provide the MODS",
129 { desc => 'Record ID or array of IDs' }
132 desc => 'MODS object(s), event on error'
137 # converts a record into a mods object with NO copy counts attached
138 sub record_id_to_mods_slim {
139 my( $self, $client, $id ) = @_;
140 return undef unless defined $id;
142 if(ref($id) and ref($id) eq 'ARRAY') {
143 return _records_to_mods( @$id );
145 my $mods_list = _records_to_mods( $id );
146 my $mods_obj = $mods_list->[0];
147 return OpenILS::Event->new('BIBLIO_RECORD_ENTRY_NOT_FOUND') unless $mods_obj;
153 __PACKAGE__->register_method(
154 method => "record_id_to_mods_slim_batch",
155 api_name => "open-ils.search.biblio.record.mods_slim.batch.retrieve",
158 sub record_id_to_mods_slim_batch {
159 my($self, $conn, $id_list) = @_;
160 $conn->respond(_records_to_mods($_)->[0]) for @$id_list;
165 # Returns the number of copies attached to a record based on org location
166 __PACKAGE__->register_method(
167 method => "record_id_to_copy_count",
168 api_name => "open-ils.search.biblio.record.copy_count",
170 desc => q/Returns a copy summary for the given record for the context org
171 unit and all ancestor org units/,
173 {desc => 'Context org unit id', type => 'number'},
174 {desc => 'Record ID', type => 'number'}
177 desc => q/summary object per org unit in the set, where the set
178 includes the context org unit and all parent org units.
179 Object includes the keys "transcendant", "count", "org_unit", "depth",
180 "unshadow", "available". Each is a count, except "org_unit" which is
181 the context org unit and "depth" which is the depth of the context org unit
188 __PACKAGE__->register_method(
189 method => "record_id_to_copy_count",
190 api_name => "open-ils.search.biblio.record.copy_count.staff",
193 desc => q/Returns a copy summary for the given record for the context org
194 unit and all ancestor org units/,
196 {desc => 'Context org unit id', type => 'number'},
197 {desc => 'Record ID', type => 'number'}
200 desc => q/summary object per org unit in the set, where the set
201 includes the context org unit and all parent org units.
202 Object includes the keys "transcendant", "count", "org_unit", "depth",
203 "unshadow", "available". Each is a count, except "org_unit" which is
204 the context org unit and "depth" which is the depth of the context org unit
211 __PACKAGE__->register_method(
212 method => "record_id_to_copy_count",
213 api_name => "open-ils.search.biblio.metarecord.copy_count",
215 desc => q/Returns a copy summary for the given record for the context org
216 unit and all ancestor org units/,
218 {desc => 'Context org unit id', type => 'number'},
219 {desc => 'Record ID', type => 'number'}
222 desc => q/summary object per org unit in the set, where the set
223 includes the context org unit and all parent org units.
224 Object includes the keys "transcendant", "count", "org_unit", "depth",
225 "unshadow", "available". Each is a count, except "org_unit" which is
226 the context org unit and "depth" which is the depth of the context org unit
233 __PACKAGE__->register_method(
234 method => "record_id_to_copy_count",
235 api_name => "open-ils.search.biblio.metarecord.copy_count.staff",
237 desc => q/Returns a copy summary for the given record for the context org
238 unit and all ancestor org units/,
240 {desc => 'Context org unit id', type => 'number'},
241 {desc => 'Record ID', type => 'number'}
244 desc => q/summary object per org unit in the set, where the set
245 includes the context org unit and all parent org units.
246 Object includes the keys "transcendant", "count", "org_unit", "depth",
247 "unshadow", "available". Each is a count, except "org_unit" which is
248 the context org unit and "depth" which is the depth of the context org
249 unit. "depth" is always -1 when the count from a lasso search is
250 performed, since depth doesn't mean anything in a lasso context.
257 sub record_id_to_copy_count {
258 my( $self, $client, $org_id, $record_id ) = @_;
260 return [] unless $record_id;
262 my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
263 my $staff = $self->api_name =~ /staff/ ? 't' : 'f';
265 my $data = $U->cstorereq(
266 "open-ils.cstore.json_query.atomic",
267 { from => ['asset.' . $key . '_copy_count' => $org_id => $record_id => $staff] }
271 for my $d ( @$data ) { # fix up the key name change required by stored-proc version
272 $$d{count} = delete $$d{visible};
276 return [ sort { $a->{depth} <=> $b->{depth} } @count ];
279 __PACKAGE__->register_method(
280 method => "record_has_holdable_copy",
281 api_name => "open-ils.search.biblio.record.has_holdable_copy",
283 desc => q/Returns a boolean indicating if a record has any holdable copies./,
285 {desc => 'Record ID', type => 'number'}
288 desc => q/bool indicating if the record has any holdable copies/,
294 __PACKAGE__->register_method(
295 method => "record_has_holdable_copy",
296 api_name => "open-ils.search.biblio.metarecord.has_holdable_copy",
298 desc => q/Returns a boolean indicating if a record has any holdable copies./,
300 {desc => 'Record ID', type => 'number'}
303 desc => q/bool indicating if the record has any holdable copies/,
309 sub record_has_holdable_copy {
310 my($self, $client, $record_id ) = @_;
312 return 0 unless $record_id;
314 my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
316 my $data = $U->cstorereq(
317 "open-ils.cstore.json_query.atomic",
318 { from => ['asset.' . $key . '_has_holdable_copy' => $record_id ] }
321 return ${@$data[0]}{'asset.' . $key . '_has_holdable_copy'} eq 't';
325 __PACKAGE__->register_method(
326 method => "biblio_search_tcn",
327 api_name => "open-ils.search.biblio.tcn",
330 desc => "Retrieve related record ID(s) given a TCN",
332 { desc => 'TCN', type => 'string' },
333 { desc => 'Flag indicating to include deleted records', type => 'string' }
336 desc => 'Results object like: { "count": $i, "ids": [...] }',
343 sub biblio_search_tcn {
345 my( $self, $client, $tcn, $include_deleted ) = @_;
347 $tcn =~ s/^\s+|\s+$//og;
349 my $e = new_editor();
350 my $search = {tcn_value => $tcn};
351 $search->{deleted} = 'f' unless $include_deleted;
352 my $recs = $e->search_biblio_record_entry( $search, {idlist =>1} );
354 return { count => scalar(@$recs), ids => $recs };
358 # --------------------------------------------------------------------------------
360 __PACKAGE__->register_method(
361 method => "biblio_barcode_to_copy",
362 api_name => "open-ils.search.asset.copy.find_by_barcode",
364 sub biblio_barcode_to_copy {
365 my( $self, $client, $barcode ) = @_;
366 my( $copy, $evt ) = $U->fetch_copy_by_barcode($barcode);
371 __PACKAGE__->register_method(
372 method => "biblio_id_to_copy",
373 api_name => "open-ils.search.asset.copy.batch.retrieve",
375 sub biblio_id_to_copy {
376 my( $self, $client, $ids ) = @_;
377 $logger->info("Fetching copies @$ids");
378 return $U->cstorereq(
379 "open-ils.cstore.direct.asset.copy.search.atomic", { id => $ids } );
383 __PACKAGE__->register_method(
384 method => "biblio_id_to_uris",
385 api_name=> "open-ils.search.asset.uri.retrieve_by_bib",
389 @param BibID Which bib record contains the URIs
390 @param OrgID Where to look for URIs
391 @param OrgDepth Range adjustment for OrgID
392 @return A stream or list of 'auri' objects
396 sub biblio_id_to_uris {
397 my( $self, $client, $bib, $org, $depth ) = @_;
398 die "Org ID required" unless defined($org);
399 die "Bib ID required" unless defined($bib);
402 push @params, $depth if (defined $depth);
404 my $ids = $U->cstorereq( "open-ils.cstore.json_query.atomic",
405 { select => { auri => [ 'id' ] },
409 field => 'call_number',
415 filter => { active => 't' }
426 select => { aou => [ { column => 'id', transform => 'actor.org_unit_descendants', params => \@params, result_field => 'id' } ] },
428 where => { id => $org },
438 my $uris = $U->cstorereq(
439 "open-ils.cstore.direct.asset.uri.search.atomic",
440 { id => [ map { (values %$_) } @$ids ] }
443 $client->respond($_) for (@$uris);
449 __PACKAGE__->register_method(
450 method => "copy_retrieve",
451 api_name => "open-ils.search.asset.copy.retrieve",
454 desc => 'Retrieve a copy object based on the Copy ID',
456 { desc => 'Copy ID', type => 'number'}
459 desc => 'Copy object, event on error'
465 my( $self, $client, $cid ) = @_;
466 my( $copy, $evt ) = $U->fetch_copy($cid);
467 return $evt || $copy;
470 __PACKAGE__->register_method(
471 method => "volume_retrieve",
472 api_name => "open-ils.search.asset.call_number.retrieve"
474 sub volume_retrieve {
475 my( $self, $client, $vid ) = @_;
476 my $e = new_editor();
477 my $vol = $e->retrieve_asset_call_number($vid) or return $e->event;
481 __PACKAGE__->register_method(
482 method => "fleshed_copy_retrieve_batch",
483 api_name => "open-ils.search.asset.copy.fleshed.batch.retrieve",
487 sub fleshed_copy_retrieve_batch {
488 my( $self, $client, $ids ) = @_;
489 $logger->info("Fetching fleshed copies @$ids");
490 return $U->cstorereq(
491 "open-ils.cstore.direct.asset.copy.search.atomic",
494 flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries parts / ] }
499 __PACKAGE__->register_method(
500 method => "fleshed_copy_retrieve",
501 api_name => "open-ils.search.asset.copy.fleshed.retrieve",
504 sub fleshed_copy_retrieve {
505 my( $self, $client, $id ) = @_;
506 my( $c, $e) = $U->fetch_fleshed_copy($id);
511 __PACKAGE__->register_method(
512 method => 'fleshed_by_barcode',
513 api_name => "open-ils.search.asset.copy.fleshed2.find_by_barcode",
516 sub fleshed_by_barcode {
517 my( $self, $conn, $barcode ) = @_;
518 my $e = new_editor();
519 my $copyid = $e->search_asset_copy(
520 {barcode => $barcode, deleted => 'f'}, {idlist=>1})->[0]
522 return fleshed_copy_retrieve2( $self, $conn, $copyid);
526 __PACKAGE__->register_method(
527 method => "fleshed_copy_retrieve2",
528 api_name => "open-ils.search.asset.copy.fleshed2.retrieve",
532 sub fleshed_copy_retrieve2 {
533 my( $self, $client, $id ) = @_;
534 my $e = new_editor();
535 my $copy = $e->retrieve_asset_copy(
542 qw/ location status stat_cat_entry_copy_maps notes age_protect parts peer_record_maps /
544 ascecm => [qw/ stat_cat stat_cat_entry /],
548 ) or return $e->event;
550 # For backwards compatibility
551 #$copy->stat_cat_entries($copy->stat_cat_entry_copy_maps);
553 if( $copy->status->id == OILS_COPY_STATUS_CHECKED_OUT ) {
555 $e->search_action_circulation(
557 { target_copy => $copy->id },
559 order_by => { circ => 'xact_start desc' },
571 __PACKAGE__->register_method(
572 method => 'flesh_copy_custom',
573 api_name => 'open-ils.search.asset.copy.fleshed.custom',
577 sub flesh_copy_custom {
578 my( $self, $conn, $copyid, $fields ) = @_;
579 my $e = new_editor();
580 my $copy = $e->retrieve_asset_copy(
590 ) or return $e->event;
595 __PACKAGE__->register_method(
596 method => "biblio_barcode_to_title",
597 api_name => "open-ils.search.biblio.find_by_barcode",
600 sub biblio_barcode_to_title {
601 my( $self, $client, $barcode ) = @_;
603 my $title = $apputils->simple_scalar_request(
605 "open-ils.storage.biblio.record_entry.retrieve_by_barcode", $barcode );
607 return { ids => [ $title->id ], count => 1 } if $title;
608 return { count => 0 };
611 __PACKAGE__->register_method(
612 method => 'title_id_by_item_barcode',
613 api_name => 'open-ils.search.bib_id.by_barcode',
616 desc => 'Retrieve bib record id associated with the copy identified by the given barcode',
618 { desc => 'Item barcode', type => 'string' }
621 desc => 'Bib record id.'
626 __PACKAGE__->register_method(
627 method => 'title_id_by_item_barcode',
628 api_name => 'open-ils.search.multi_home.bib_ids.by_barcode',
631 desc => 'Retrieve bib record ids associated with the copy identified by the given barcode. This includes peer bibs for Multi-Home items.',
633 { desc => 'Item barcode', type => 'string' }
636 desc => 'Array of bib record ids. First element is the native bib for the item.'
642 sub title_id_by_item_barcode {
643 my( $self, $conn, $barcode ) = @_;
644 my $e = new_editor();
645 my $copies = $e->search_asset_copy(
647 { deleted => 'f', barcode => $barcode },
651 acp => [ 'call_number' ],
658 return $e->event unless @$copies;
660 if( $self->api_name =~ /multi_home/ ) {
661 my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
663 { target_copy => $$copies[0]->id }
666 my @temp = map { $_->peer_record } @{ $multi_home_list };
667 unshift @temp, $$copies[0]->call_number->record->id;
670 return $$copies[0]->call_number->record->id;
674 __PACKAGE__->register_method(
675 method => 'find_peer_bibs',
676 api_name => 'open-ils.search.peer_bibs.test',
679 desc => 'Tests to see if the specified record is a peer record.',
681 { desc => 'Biblio record entry Id', type => 'number' }
684 desc => 'True if specified id can be found in biblio.peer_bib_copy_map.peer_record.',
690 __PACKAGE__->register_method(
691 method => 'find_peer_bibs',
692 api_name => 'open-ils.search.peer_bibs',
695 desc => 'Return acps and mvrs for multi-home items linked to specified peer record.',
697 { desc => 'Biblio record entry Id', type => 'number' }
700 desc => '{ records => Array of mvrs, items => array of acps }',
707 my( $self, $client, $doc_id ) = @_;
708 my $e = new_editor();
710 my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
712 { peer_record => $doc_id },
716 bpbcm => [ 'target_copy', 'peer_type' ],
717 acp => [ 'call_number', 'location', 'status', 'peer_record_maps' ]
723 if ($self->api_name =~ /test/) {
724 return scalar( @{$multi_home_list} ) > 0 ? 1 : 0;
727 if (scalar(@{$multi_home_list})==0) {
731 # create a unique hash of the primary record MVRs for foreign copies
732 # XXX PLEASE let's change to unAPI2 (supports foreign copies) in the TT opac?!?
734 ($_->target_copy->call_number->record, _records_to_mods( $_->target_copy->call_number->record )->[0])
737 # set the foreign_copy_maps field to an empty array
738 map { $rec_hash{$_}->foreign_copy_maps([]) } keys( %rec_hash );
740 # push the maps onto the correct MVRs
741 for (@$multi_home_list) {
743 @{$rec_hash{ $_->target_copy->call_number->record }->foreign_copy_maps()},
748 return [sort {$a->title cmp $b->title} values(%rec_hash)];
751 __PACKAGE__->register_method(
752 method => "biblio_copy_to_mods",
753 api_name => "open-ils.search.biblio.copy.mods.retrieve",
756 # takes a copy object and returns it fleshed mods object
757 sub biblio_copy_to_mods {
758 my( $self, $client, $copy ) = @_;
760 my $volume = $U->cstorereq(
761 "open-ils.cstore.direct.asset.call_number.retrieve",
762 $copy->call_number() );
764 my $mods = _records_to_mods($volume->record());
765 $mods = shift @$mods;
766 $volume->copies([$copy]);
767 push @{$mods->call_numbers()}, $volume;
775 OpenILS::Application::Search::Biblio
781 =head3 open-ils.search.biblio.multiclass.query (arghash, query, docache)
783 For arghash and docache, see B<open-ils.search.biblio.multiclass>.
785 The query argument is a string, but built like a hash with key: value pairs.
786 Recognized search keys include:
788 keyword (kw) - search keyword(s) *
789 author (au) - search author(s) *
790 name (au) - same as author *
791 title (ti) - search title *
792 subject (su) - search subject *
793 series (se) - search series *
794 lang - limit by language (specify multiple langs with lang:l1 lang:l2 ...)
795 site - search at specified org unit, corresponds to actor.org_unit.shortname
796 pref_ou - extend search to specified org unit, corresponds to actor.org_unit.shortname
797 sort - sort type (title, author, pubdate)
798 dir - sort direction (asc, desc)
799 available - if set to anything other than "false" or "0", limits to available items
801 * Searching keyword, author, title, subject, and series supports additional search
802 subclasses, specified with a "|". For example, C<title|proper:gone with the wind>.
804 For more, see B<config.metabib_field>.
808 foreach (qw/open-ils.search.biblio.multiclass.query
809 open-ils.search.biblio.multiclass.query.staff
810 open-ils.search.metabib.multiclass.query
811 open-ils.search.metabib.multiclass.query.staff/)
813 __PACKAGE__->register_method(
815 method => 'multiclass_query',
817 desc => 'Perform a search query. The .staff version of the call includes otherwise hidden hits.',
819 {name => 'arghash', desc => 'Arg hash (see open-ils.search.biblio.multiclass)', type => 'object'},
820 {name => 'query', desc => 'Raw human-readable query (see perldoc '. __PACKAGE__ .')', type => 'string'},
821 {name => 'docache', desc => 'Flag for caching (see open-ils.search.biblio.multiclass)', type => 'object'},
824 desc => 'Search results from query, like: { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
825 type => 'object', # TODO: update as miker's new elements are included
831 sub multiclass_query {
832 # arghash only really supports limit/offset anymore
833 my($self, $conn, $arghash, $query, $docache, $phys_loc) = @_;
837 $query =~ s/^\s+//go;
838 $query =~ s/\s+/ /go;
839 $arghash->{query} = $query
842 $logger->debug("initial search query => $query") if $query;
844 (my $method = $self->api_name) =~ s/\.query/.staged/o;
845 return $self->method_lookup($method)->dispatch($arghash, $docache, $phys_loc);
849 __PACKAGE__->register_method(
850 method => 'cat_search_z_style_wrapper',
851 api_name => 'open-ils.search.biblio.zstyle',
853 signature => q/@see open-ils.search.biblio.multiclass/
856 __PACKAGE__->register_method(
857 method => 'cat_search_z_style_wrapper',
858 api_name => 'open-ils.search.biblio.zstyle.staff',
860 signature => q/@see open-ils.search.biblio.multiclass/
863 sub cat_search_z_style_wrapper {
866 my $authtoken = shift;
869 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
871 my $ou = $cstore->request(
872 'open-ils.cstore.direct.actor.org_unit.search',
873 { parent_ou => undef }
876 my $result = { service => 'native-evergreen-catalog', records => [] };
877 my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
879 $$searchhash{searches}{title}{term} = $$args{search}{title} if $$args{search}{title};
880 $$searchhash{searches}{author}{term} = $$args{search}{author} if $$args{search}{author};
881 $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
882 $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
883 $$searchhash{searches}{'identifier|isbn'}{term} = $$args{search}{isbn} if $$args{search}{isbn};
884 $$searchhash{searches}{'identifier|issn'}{term} = $$args{search}{issn} if $$args{search}{issn};
885 $$searchhash{searches}{'identifier|upc'}{term} = $$args{search}{upc} if $$args{search}{upc};
887 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
888 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
889 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
890 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
892 my $method = 'open-ils.search.biblio.multiclass.staged';
893 $method .= '.staff' if $self->api_name =~ /staff$/;
895 my ($list) = $self->method_lookup($method)->run( $searchhash );
897 if ($list->{count} > 0 and @{$list->{ids}}) {
898 $result->{count} = $list->{count};
900 my $records = $cstore->request(
901 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
902 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
905 for my $rec ( @$records ) {
907 my $u = OpenILS::Utils::ModsParser->new();
908 $u->start_mods_batch( $rec->marc );
909 my $mods = $u->finish_mods_batch();
911 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
917 $cstore->disconnect();
921 # ----------------------------------------------------------------------------
922 # These are the main OPAC search methods
923 # ----------------------------------------------------------------------------
925 __PACKAGE__->register_method(
926 method => 'the_quest_for_knowledge',
927 api_name => 'open-ils.search.biblio.multiclass',
929 desc => "Performs a multi class biblio or metabib search",
932 desc => "A search hash with keys: "
933 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
934 . "See perldoc " . __PACKAGE__ . " for more detail",
938 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
943 desc => 'An object of the form: '
944 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
949 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
951 The search-hash argument can have the following elements:
953 searches: { "$class" : "$value", ...} [REQUIRED]
954 org_unit: The org id to focus the search at
955 depth : The org depth
956 limit : The search limit default: 10
957 offset : The search offset default: 0
958 format : The MARC format
959 sort : What field to sort the results on? [ author | title | pubdate ]
960 sort_dir: What direction do we sort? [ asc | desc ]
961 tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
962 will be tagged with an additional value ("1") as the last value in the record ID array for
963 each record. Requires the 'authtoken'
964 authtoken : Authentication token string; When actions are performed that require a user login
965 (e.g. tagging circulated records), the authentication token is required
967 The searches element is required, must have a hashref value, and the hashref must contain at least one
968 of the following classes as a key:
976 The value paired with a key is the associated search string.
978 The docache argument enables/disables searching and saving results in cache (default OFF).
980 The return object, if successful, will look like:
982 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
986 __PACKAGE__->register_method(
987 method => 'the_quest_for_knowledge',
988 api_name => 'open-ils.search.biblio.multiclass.staff',
989 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
991 __PACKAGE__->register_method(
992 method => 'the_quest_for_knowledge',
993 api_name => 'open-ils.search.metabib.multiclass',
994 signature => q/@see open-ils.search.biblio.multiclass/
996 __PACKAGE__->register_method(
997 method => 'the_quest_for_knowledge',
998 api_name => 'open-ils.search.metabib.multiclass.staff',
999 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1002 sub the_quest_for_knowledge {
1003 my( $self, $conn, $searchhash, $docache ) = @_;
1005 return { count => 0 } unless $searchhash and
1006 ref $searchhash->{searches} eq 'HASH';
1008 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
1012 if($self->api_name =~ /metabib/) {
1014 $method =~ s/biblio/metabib/o;
1017 # do some simple sanity checking
1018 if(!$searchhash->{searches} or
1019 ( !grep { /^(?:title|author|subject|series|keyword|identifier\|is[bs]n)/ } keys %{$searchhash->{searches}} ) ) {
1020 return { count => 0 };
1023 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
1024 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
1025 my $end = $offset + $limit - 1;
1027 my $maxlimit = 5000;
1028 $searchhash->{offset} = 0; # possible user value overwritten in hash
1029 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
1031 return { count => 0 } if $offset > $maxlimit;
1034 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
1035 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
1036 my $ckey = $pfx . md5_hex($method . $s);
1038 $logger->info("bib search for: $s");
1040 $searchhash->{limit} -= $offset;
1044 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1048 $method .= ".staff" if($self->api_name =~ /staff/);
1049 $method .= ".atomic";
1051 for (keys %$searchhash) {
1052 delete $$searchhash{$_}
1053 unless defined $$searchhash{$_};
1056 $result = $U->storagereq( $method, %$searchhash );
1060 $docache = 0; # results came FROM cache, so we don't write back
1063 return {count => 0} unless ($result && $$result[0]);
1067 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1070 # If we didn't get this data from the cache, put it into the cache
1071 # then return the correct offset of records
1072 $logger->debug("putting search cache $ckey\n");
1073 put_cache($ckey, $count, \@recs);
1077 # if we have the full set of data, trim out
1078 # the requested chunk based on limit and offset
1080 for ($offset..$end) {
1081 last unless $recs[$_];
1082 push(@t, $recs[$_]);
1087 return { ids => \@recs, count => $count };
1091 __PACKAGE__->register_method(
1092 method => 'staged_search',
1093 api_name => 'open-ils.search.biblio.multiclass.staged',
1095 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1096 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1099 desc => "A search hash with keys: "
1100 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1101 . "See perldoc " . __PACKAGE__ . " for more detail",
1105 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1110 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1111 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1116 __PACKAGE__->register_method(
1117 method => 'staged_search',
1118 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1119 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1121 __PACKAGE__->register_method(
1122 method => 'staged_search',
1123 api_name => 'open-ils.search.metabib.multiclass.staged',
1124 signature => q/@see open-ils.search.biblio.multiclass.staged/
1126 __PACKAGE__->register_method(
1127 method => 'staged_search',
1128 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1129 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1132 my $estimation_strategy;
1134 my($self, $conn, $search_hash, $docache, $phys_loc) = @_;
1136 my $e = new_editor();
1137 if (!$max_concurrent_search) {
1138 my $mcs = $e->retrieve_config_global_flag('opac.max_concurrent_search.query');
1139 $max_concurrent_search = ($mcs and $mcs->enabled eq 't') ? $mcs->value : 20;
1142 $phys_loc ||= $U->get_org_tree->id;
1144 my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1146 my $method = $IAmMetabib?
1147 'open-ils.storage.metabib.multiclass.staged.search_fts':
1148 'open-ils.storage.biblio.multiclass.staged.search_fts';
1150 $method .= '.staff' if $self->api_name =~ /staff$/;
1151 $method .= '.atomic';
1153 if (!$search_hash->{query}) {
1154 return {count => 0} unless (
1156 $search_hash->{searches} and
1157 int(scalar( keys %{$search_hash->{searches}} )));
1160 my $search_duration;
1161 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1162 my $user_limit = $search_hash->{limit} || 10;
1163 my $ignore_facet_classes = $search_hash->{ignore_facet_classes};
1164 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1165 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1168 # we're grabbing results on a per-superpage basis, which means the
1169 # limit and offset should coincide with superpage boundaries
1170 $search_hash->{offset} = 0;
1171 $search_hash->{limit} = $superpage_size;
1173 # force a well-known check_limit
1174 $search_hash->{check_limit} = $superpage_size;
1175 # restrict total tested to superpage size * number of superpages
1176 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1178 # Set the configured estimation strategy, defaults to 'inclusion'.
1179 unless ($estimation_strategy) {
1180 $estimation_strategy = OpenSRF::Utils::SettingsClient
1183 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1186 $search_hash->{estimation_strategy} = $estimation_strategy;
1188 # pull any existing results from the cache
1189 my $key = search_cache_key($method, $search_hash);
1190 my $facet_key = $key.'_facets';
1192 # Let the world know that there is at least one backend that will be searching
1193 my $counter_key = $key.'_counter';
1194 $cache->get_cache($counter_key) || $cache->{memcache}->add($counter_key, 0, $cache_timeout);
1195 my $search_peers = $cache->{memcache}->incr($counter_key);
1197 # If the world tells us that there are more than we want to allow, we stop.
1198 if ($search_peers > $max_concurrent_search) {
1199 $logger->warn("Too many concurrent searches per $counter_key: $search_peers");
1200 $cache->{memcache}->decr($counter_key);
1201 return OpenILS::Event->new('BAD_PARAMS')
1204 my $cache_data = $cache->get_cache($key) || {};
1206 # First, we want to make sure that someone else isn't currently trying to perform exactly
1207 # this same search. The point is to allow just one instance of a search to fill the needs
1208 # of all concurrent, identical searches. This will avoid spammy searches killing the
1209 # database without requiring admins to start locking some IP addresses out entirely.
1211 # There's still a tiny race condition where 2 might run, but without sigificantly more code
1212 # and complexity, this is close to the best we can do.
1214 if ($cache_data->{running}) { # someone is already doing the search...
1215 my $stop_looping = time() + $cache_timeout;
1216 while ( sleep(1) and time() < $stop_looping ) { # sleep for a second ... maybe they'll finish
1217 $cache_data = $cache->get_cache($key) || {};
1218 last if (!$cache_data->{running});
1220 } elsif (!$cache_data->{0}) { # we're the first ... let's give it a try
1221 $cache->put_cache($key, { running => $$ }, $cache_timeout / 3);
1224 # keep retrieving results until we find enough to
1225 # fulfill the user-specified limit and offset
1226 my $all_results = [];
1227 my $page; # current superpage
1228 my $current_page_summary = {};
1229 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1232 for($page = 0; $page < $max_superpages; $page++) {
1234 my $data = $cache_data->{$page};
1238 $logger->debug("staged search: analyzing superpage $page");
1241 # this window of results is already cached
1242 $logger->debug("staged search: found cached results");
1243 $summary = $data->{summary};
1244 $results = $data->{results};
1247 # retrieve the window of results from the database
1248 $logger->debug("staged search: fetching results from the database");
1249 $search_hash->{skip_check} = $page * $superpage_size;
1250 $search_hash->{return_query} = $page == 0 ? 1 : 0;
1253 $results = $U->storagereq($method, %$search_hash);
1254 $search_duration = time - $start;
1255 $summary = shift(@$results) if $results;
1258 $logger->info("search timed out: duration=$search_duration: params=".
1259 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1260 $cache->{memcache}->decr($counter_key);
1261 return {count => 0};
1264 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1266 # Create backwards-compatible result structures
1268 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}, $_->{rel}, $_->{record}]} @$results];
1270 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}]} @$results];
1273 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1274 $results = [grep {defined $_->[0]} @$results];
1275 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1278 tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib)
1279 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1281 $current_page_summary = $summary;
1283 # add the new set of results to the set under construction
1284 push(@$all_results, @$results);
1286 my $current_count = scalar(@$all_results);
1288 if ($page == 0) { # all summaries are the same, just get the first
1289 for (keys %$summary) {
1290 $global_summary->{$_} = $summary->{$_};
1294 # we've found all the possible hits
1295 last if $current_count == $summary->{visible};
1297 # we've found enough results to satisfy the requested limit/offset
1298 last if $current_count >= ($user_limit + $user_offset);
1300 # we've scanned all possible hits
1301 last if($summary->{checked} < $superpage_size);
1304 # Let other backends grab our data now that we're done, and flush the key if we're the last one.
1305 $cache_data = $cache->get_cache($key);
1306 if ($$cache_data{running} and $$cache_data{running} == $$) {
1307 delete $$cache_data{running};
1308 $cache->put_cache($key, $cache_data, $cache_timeout);
1311 my ($class, $term, $field_list) = one_class_multi_term($global_summary->{query_struct});
1312 if ($class and $term) { # we meet the current "can suggest" criteria, check for suggestions!
1313 my $editor = new_editor();
1314 my $class_settings = $editor->retrieve_config_metabib_class($class);
1317 if ( # search did not provide enough hits and settings
1318 # for this class want more than 0 suggestions
1319 $global_summary->{visible} <= $class_settings->low_result_threshold
1320 and $class_settings->max_suggestions != 0
1322 my $suggestion_verbosity = $class_settings->symspell_suggestion_verbosity;
1323 if ($class_settings->max_suggestions == -1) { # special value that means "only best suggestion, and not always"
1324 $class_settings->max_suggestions(1);
1325 $suggestion_verbosity = 0;
1328 my $suggs = $editor->json_query({
1330 'search.symspell_suggest',
1331 $term, $class, '{'.join($field_list).'}',
1332 undef, # max edit distance per word, just get the database setting
1333 $suggestion_verbosity
1338 $$a{lev_distance} <=> $$b{lev_distance}
1340 $$b{pg_trgm_sim} * $class_settings->pg_trgm_weight
1341 + $$b{soundex_sim} * $class_settings->soundex_weight
1342 + $$b{qwerty_kb_match} * $class_settings->keyboard_distance_weight
1344 $$a{pg_trgm_sim} * $class_settings->pg_trgm_weight
1345 + $$a{soundex_sim} * $class_settings->soundex_weight
1346 + $$a{qwerty_kb_match} * $class_settings->keyboard_distance_weight
1348 || abs($$b{suggestion_count}) <=> abs($$a{suggestion_count})
1349 } grep { $$_{lev_distance} != 0 || $$_{suggestion_count} < 0 } @$suggs;
1352 $global_summary->{suggestions}{'one_class_multi_term'} = {
1355 suggestions => [ splice @$suggs, 0, $class_settings->max_suggestions ]
1361 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1363 $conn->respond_complete(
1365 global_summary => $global_summary,
1366 count => $global_summary->{visible},
1367 core_limit => $search_hash->{core_limit},
1369 superpage_size => $search_hash->{check_limit},
1370 superpage_summary => $current_page_summary,
1371 facet_key => $facet_key,
1375 $cache->{memcache}->decr($counter_key);
1377 $logger->info("Completed canonicalized search is: $$global_summary{canonicalized_query}");
1379 return cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1382 sub one_class_multi_term {
1383 my $qstruct = shift;
1385 my $node = $$qstruct{children};
1390 if ($$node{fields} and @{$$node{fields}} > 0) {
1391 return (undef,undef,undef) if (join(',', @{$$node{fields}}) ne join(',', @$fields));
1393 } elsif ($$node{fields}) {
1394 $fields = [ @{$$node{fields}} ];
1399 return (undef,undef,undef) if ($$node{'|'}
1400 # or ($$node{modifiers} and @{$$node{modifiers}} > 0)
1401 # or ($$node{filters} and @{$$node{filters}} > 0)
1404 for my $kid (@{$$node{'&'}}) {
1405 my ($subclass, $subterm);
1406 if ($$kid{type} eq 'query_plan') {
1407 ($subclass, $subterm) = one_class_multi_term($kid, $fields);
1408 return (undef,undef,undef) if ($class and $subclass and $class ne $subclass);
1410 $term .= ' ' if $term;
1411 $term .= $subterm if $subterm;
1412 } elsif ($$kid{type} eq 'node') {
1413 $subclass = $$kid{class};
1414 return (undef,undef,undef) if ($class and $subclass and $class ne $subclass);
1416 ($subclass, $subterm) = one_class_multi_term($kid, $fields);
1417 return (undef,undef,undef) if ($subclass and $class ne $subclass);
1418 $term .= ' ' if $term;
1419 $term .= $subterm if $subterm;
1420 } elsif ($$kid{type} eq 'atom') {
1421 $term .= ' ' if $term;
1422 if ($$kid{content} !~ /\s+/ and $$kid{prefix} =~ /^-/) {
1423 # only quote negated multi-word phrases, not negated single words
1424 $$kid{prefix} = '-';
1427 $term .= $$kid{prefix}.$$kid{content}.$$kid{suffix};
1431 return ($class, $term, $fields);
1434 sub fetch_display_fields {
1437 my $highlight_map = shift;
1441 $conn->respond_complete;
1445 my $e = new_editor();
1447 for my $record ( @records ) {
1448 next unless ($record && $highlight_map);
1451 {from => ['search.highlight_display_fields', $record, $highlight_map]}
1458 __PACKAGE__->register_method(
1459 method => 'fetch_display_fields',
1460 api_name => 'open-ils.search.fetch.metabib.display_field.highlight',
1465 sub tag_circulated_records {
1466 my ($auth, $results, $metabib) = @_;
1467 my $e = new_editor(authtoken => $auth);
1468 return $results unless $e->checkauth;
1471 select => { acn => [{ column => 'record', alias => 'tagme' }] },
1472 from => { auch => { acp => { join => 'acn' }} },
1473 where => { usr => $e->requestor->id },
1479 select => { mmrsm => [{ column => 'metarecord', alias => 'tagme' }] },
1481 where => { source => { in => $query } },
1486 # Give me the distinct set of bib records that exist in the user's visible circulation history
1487 my $circ_recs = $e->json_query( $query );
1489 # if the record appears in the circ history, push a 1 onto
1490 # the rec array structure to indicate truthiness
1491 for my $rec (@$results) {
1492 push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1498 # creates a unique token to represent the query in the cache
1499 sub search_cache_key {
1501 my $search_hash = shift;
1503 for my $key (sort keys %$search_hash) {
1504 push(@sorted, ($key => $$search_hash{$key}))
1505 unless $key eq 'limit' or
1507 $key eq 'skip_check';
1509 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1510 return $pfx . md5_hex($method . $s);
1513 sub retrieve_cached_facets {
1519 return undef unless ($key and $key =~ /_facets$/);
1522 local $SIG{ALRM} = sub {die};
1523 alarm(10); # we'll sleep for as much as 10s
1525 die if $cache->get_cache($key . '_COMPLETE');
1526 } while (sleep(0.05));
1531 my $blob = $cache->get_cache($key) || {};
1535 for my $f ( keys %$blob ) {
1536 my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1537 @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1538 for my $s ( @sorted ) {
1539 my ($k) = keys(%$s);
1540 my ($v) = values(%$s);
1541 $$facets{$f}{$k} = $v;
1551 __PACKAGE__->register_method(
1552 method => "retrieve_cached_facets",
1553 api_name => "open-ils.search.facet_cache.retrieve",
1555 desc => 'Returns facet data derived from a specific search based on a key '.
1556 'generated by open-ils.search.biblio.multiclass.staged and friends.',
1559 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1564 desc => 'Two level hash of facet values. Top level key is the facet id defined on the config.metabib_field table. '.
1565 'Second level key is a string facet value. Datum attached to each facet value is the number of distinct records, '.
1566 'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1567 'facet retrieval. These counts are calculated for all superpages that have been checked for visibility.',
1575 # add facets for this search to the facet cache
1576 my($key, $results, $metabib, $ignore) = @_;
1577 my $data = $cache->get_cache($key);
1580 return undef unless (@$results);
1582 my $facets_function = $metabib ? 'search.facets_for_metarecord_set'
1583 : 'search.facets_for_record_set';
1584 my $results_str = '{' . join(',', @$results) . '}';
1585 my $ignore_str = ref($ignore) ? '{' . join(',', @$ignore) . '}'
1588 from => [ $facets_function, $ignore_str, $results_str ]
1591 my $facets = OpenILS::Utils::CStoreEditor->new->json_query($query, {substream => 1});
1593 for my $facet (@$facets) {
1594 next unless ($facet->{value});
1595 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1598 $logger->info("facet compilation: cached with key=$key");
1600 $cache->put_cache($key, $data, $cache_timeout);
1601 $cache->put_cache($key.'_COMPLETE', 1, $cache_timeout);
1604 sub cache_staged_search_page {
1605 # puts this set of results into the cache
1606 my($key, $page, $summary, $results) = @_;
1607 my $data = $cache->get_cache($key);
1610 summary => $summary,
1614 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1615 ($summary->{estimated_hit_count} || "none") .
1616 ", visible=" . ($summary->{visible} || "none")
1619 $cache->put_cache($key, $data, $cache_timeout);
1627 my $start = $offset;
1628 my $end = $offset + $limit - 1;
1630 $logger->debug("searching cache for $key : $start..$end\n");
1632 return undef unless $cache;
1633 my $data = $cache->get_cache($key);
1635 return undef unless $data;
1637 my $count = $data->[0];
1640 return undef unless $offset < $count;
1643 for( my $i = $offset; $i <= $end; $i++ ) {
1644 last unless my $d = $$data[$i];
1645 push( @result, $d );
1648 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1655 my( $key, $count, $data ) = @_;
1656 return undef unless $cache;
1657 $logger->debug("search_cache putting ".
1658 scalar(@$data)." items at key $key with timeout $cache_timeout");
1659 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1663 __PACKAGE__->register_method(
1664 method => "biblio_mrid_to_modsbatch_batch",
1665 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1668 sub biblio_mrid_to_modsbatch_batch {
1669 my( $self, $client, $mrids) = @_;
1670 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1672 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1673 for my $id (@$mrids) {
1674 next unless defined $id;
1675 my ($m) = $method->run($id);
1682 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1683 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1685 __PACKAGE__->register_method(
1686 method => "biblio_mrid_to_modsbatch",
1689 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1690 . "As usual, the .staff version of this method will include otherwise hidden records.",
1692 { desc => 'Metarecord ID', type => 'number' },
1693 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1696 desc => 'MVR Object, event on error',
1702 sub biblio_mrid_to_modsbatch {
1703 my( $self, $client, $mrid, $args) = @_;
1705 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1707 my ($mr, $evt) = _grab_metarecord($mrid);
1708 return $evt unless $mr;
1710 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1711 biblio_mrid_make_modsbatch($self, $client, $mr);
1713 return $mvr unless ref($args);
1715 # Here we find the lead record appropriate for the given filters
1716 # and use that for the title and author of the metarecord
1717 my $format = $$args{format};
1718 my $org = $$args{org};
1719 my $depth = $$args{depth};
1721 return $mvr unless $format or $org or $depth;
1723 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1724 $method = "$method.staff" if $self->api_name =~ /staff/o;
1726 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1728 if( my $mods = $U->record_to_mvr($rec) ) {
1730 $mvr->title( $mods->title );
1731 $mvr->author($mods->author);
1732 $logger->debug("mods_slim updating title and ".
1733 "author in mvr with ".$mods->title." : ".$mods->author);
1739 # converts a metarecord to an mvr
1742 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1743 return Fieldmapper::metabib::virtual_record->new($perl);
1746 # checks to see if a metarecord has mods, if so returns true;
1748 __PACKAGE__->register_method(
1749 method => "biblio_mrid_check_mvr",
1750 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1751 notes => "Takes a metarecord ID or a metarecord object and returns true "
1752 . "if the metarecord already has an mvr associated with it."
1755 sub biblio_mrid_check_mvr {
1756 my( $self, $client, $mrid ) = @_;
1760 if(ref($mrid)) { $mr = $mrid; }
1761 else { ($mr, $evt) = _grab_metarecord($mrid); }
1762 return $evt if $evt;
1764 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1766 return _mr_to_mvr($mr) if $mr->mods();
1770 sub _grab_metarecord {
1772 my $e = new_editor();
1773 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1778 __PACKAGE__->register_method(
1779 method => "biblio_mrid_make_modsbatch",
1780 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1781 notes => "Takes either a metarecord ID or a metarecord object. "
1782 . "Forces the creations of an mvr for the given metarecord. "
1783 . "The created mvr is returned."
1786 sub biblio_mrid_make_modsbatch {
1787 my( $self, $client, $mrid ) = @_;
1789 my $e = new_editor();
1796 $mr = $e->retrieve_metabib_metarecord($mrid)
1797 or return $e->event;
1800 my $masterid = $mr->master_record;
1801 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1803 my $ids = $U->storagereq(
1804 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1805 return undef unless @$ids;
1807 my $master = $e->retrieve_biblio_record_entry($masterid)
1808 or return $e->event;
1810 # start the mods batch
1811 my $u = OpenILS::Utils::ModsParser->new();
1812 $u->start_mods_batch( $master->marc );
1814 # grab all of the sub-records and shove them into the batch
1815 my @ids = grep { $_ ne $masterid } @$ids;
1816 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1821 my $r = $e->retrieve_biblio_record_entry($i);
1822 push( @$subrecs, $r ) if $r;
1827 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1828 $u->push_mods_batch( $_->marc ) if $_->marc;
1832 # finish up and send to the client
1833 my $mods = $u->finish_mods_batch();
1834 $mods->doc_id($mrid);
1835 $client->respond_complete($mods);
1838 # now update the mods string in the db
1839 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1842 $e = new_editor(xact => 1);
1843 $e->update_metabib_metarecord($mr)
1844 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1851 # converts a mr id into a list of record ids
1853 foreach (qw/open-ils.search.biblio.metarecord_to_records
1854 open-ils.search.biblio.metarecord_to_records.staff/)
1856 __PACKAGE__->register_method(
1857 method => "biblio_mrid_to_record_ids",
1860 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1861 . "As usual, the .staff version of this method will include otherwise hidden records.",
1863 { desc => 'Metarecord ID', type => 'number' },
1864 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1867 desc => 'Results object like {count => $i, ids =>[...]}',
1875 sub biblio_mrid_to_record_ids {
1876 my( $self, $client, $mrid, $args ) = @_;
1878 my $format = $$args{format};
1879 my $org = $$args{org};
1880 my $depth = $$args{depth};
1882 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1883 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1884 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1886 return { count => scalar(@$recs), ids => $recs };
1890 __PACKAGE__->register_method(
1891 method => "biblio_record_to_marc_html",
1892 api_name => "open-ils.search.biblio.record.html"
1895 __PACKAGE__->register_method(
1896 method => "biblio_record_to_marc_html",
1897 api_name => "open-ils.search.authority.to_html"
1900 # Persistent parsers and setting objects
1901 my $parser = XML::LibXML->new();
1902 my $xslt = XML::LibXSLT->new();
1904 my $slim_marc_sheet;
1905 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1907 sub biblio_record_to_marc_html {
1908 my($self, $client, $recordid, $slim, $marcxml) = @_;
1911 my $dir = $settings_client->config_value("dirs", "xsl");
1914 unless($slim_marc_sheet) {
1915 my $xsl = $settings_client->config_value(
1916 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1918 $xsl = $parser->parse_file("$dir/$xsl");
1919 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1922 $sheet = $slim_marc_sheet;
1926 unless($marc_sheet) {
1927 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1928 my $xsl = $settings_client->config_value(
1929 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1930 $xsl = $parser->parse_file("$dir/$xsl");
1931 $marc_sheet = $xslt->parse_stylesheet($xsl);
1933 $sheet = $marc_sheet;
1938 my $e = new_editor();
1939 if($self->api_name =~ /authority/) {
1940 $record = $e->retrieve_authority_record_entry($recordid)
1941 or return $e->event;
1943 $record = $e->retrieve_biblio_record_entry($recordid)
1944 or return $e->event;
1946 $marcxml = $record->marc;
1949 my $xmldoc = $parser->parse_string($marcxml);
1950 my $html = $sheet->transform($xmldoc);
1951 return $html->documentElement->toString();
1954 __PACKAGE__->register_method(
1955 method => "send_event_email_output",
1956 api_name => "open-ils.search.biblio.record.email.send_output",
1958 sub send_event_email_output {
1959 my($self, $client, $auth, $event_id, $capkey, $capanswer) = @_;
1960 return undef unless $event_id;
1962 my $captcha_pass = 0;
1965 $real_answer = $cache->get_cache(md5_hex($capkey));
1966 $captcha_pass++ if ($real_answer eq $capanswer);
1969 my $e = new_editor(authtoken => $auth);
1970 return $e->die_event unless $captcha_pass || $e->checkauth;
1972 my $event = $e->retrieve_action_trigger_event([$event_id,{flesh => 1, flesh_fields => { atev => ['template_output']}}]);
1973 return undef unless ($event and $event->template_output);
1975 my $smtp = OpenSRF::Utils::SettingsClient
1977 ->config_value('email_notify', 'smtp_server');
1979 my $sender = Email::Send->new({mailer => 'SMTP'});
1980 $sender->mailer_args([Host => $smtp]);
1985 my $email = _create_mime_email($event->template_output->data);
1988 $stat = $sender->send($email);
1989 } catch Error with {
1990 $err = $stat = shift;
1991 $logger->error("send_event_email_output: Email failed with error: $err");
1994 if( !$err and $stat and $stat->type eq 'success' ) {
1995 $logger->info("send_event_email_output: successfully sent email");
1998 $logger->warn("send_event_email_output: unable to send email: ".Dumper($stat));
2003 sub _create_mime_email {
2004 my $template_output = shift;
2005 my $email = Email::MIME->new($template_output);
2006 for my $hfield (qw/From To Bcc Cc Reply-To Sender/) {
2007 my @headers = $email->header($hfield);
2008 $email->header_str_set($hfield => join(',', @headers)) if ($headers[0]);
2011 my @headers = $email->header('Subject');
2012 $email->header_str_set('Subject' => $headers[0]) if ($headers[0]);
2014 $email->header_set('MIME-Version' => '1.0');
2015 $email->header_set('Content-Type' => "text/plain; charset=UTF-8");
2016 $email->header_set('Content-Transfer-Encoding' => '8bit');
2020 __PACKAGE__->register_method(
2021 method => "format_biblio_record_entry",
2022 api_name => "open-ils.search.biblio.record.print.preview",
2025 __PACKAGE__->register_method(
2026 method => "format_biblio_record_entry",
2027 api_name => "open-ils.search.biblio.record.email.preview",
2030 __PACKAGE__->register_method(
2031 method => "format_biblio_record_entry",
2032 api_name => "open-ils.search.biblio.record.print",
2034 desc => 'Returns a printable version of the specified bib record',
2036 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
2037 { desc => 'Context library for holdings, if applicable', type => 'number' },
2038 { desc => 'Sort order, if applicable', type => 'string' },
2039 { desc => 'Sort direction, if applicable', type => 'string' },
2040 { desc => 'Definition Group Member id', type => 'number' },
2043 desc => q/An action_trigger.event object or error event./,
2048 __PACKAGE__->register_method(
2049 method => "format_biblio_record_entry",
2050 api_name => "open-ils.search.biblio.record.email",
2052 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
2054 { desc => 'Authentication token', type => 'string'},
2055 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
2056 { desc => 'Context library for holdings, if applicable', type => 'number' },
2057 { desc => 'Sort order, if applicable', type => 'string' },
2058 { desc => 'Sort direction, if applicable', type => 'string' },
2059 { desc => 'Definition Group Member id', type => 'number' },
2060 { desc => 'Whether to bypass auth due to captcha', type => 'bool' },
2061 { desc => 'Email address, if none for the user', type => 'string' },
2062 { desc => 'Subject, if customized', type => 'string' },
2065 desc => q/Undefined on success, otherwise an error event./,
2071 sub format_biblio_record_entry {
2072 my ($self, $conn) = splice @_, 0, 2;
2074 my $for_print = ($self->api_name =~ /print/);
2075 my $for_email = ($self->api_name =~ /email/);
2076 my $preview = ($self->api_name =~ /preview/);
2078 my ($auth, $captcha_pass, $email, $subject);
2081 if (@_ > 5) { # the stuff below is included in the params, safe to splice
2082 ($captcha_pass, $email, $subject) = splice @_, -3, 3;
2085 my ($bib_id, $holdings_context_org, $bib_sort, $sort_dir, $group_member) = @_;
2086 $holdings_context_org ||= $U->get_org_tree->id;
2087 $bib_sort ||= 'author';
2088 $sort_dir ||= 'ascending';
2090 my $e; my $event_context_org; my $type = 'brief';
2093 $event_context_org = $holdings_context_org;
2094 $e = new_editor(xact => 1);
2095 } elsif ($for_email) {
2096 $e = new_editor(authtoken => $auth, xact => 1);
2097 return $e->die_event unless $captcha_pass || $e->checkauth;
2098 $event_context_org = $e->requestor ? $e->requestor->home_ou : $holdings_context_org;
2099 $email ||= $e->requestor ? $e->requestor->email : '';
2102 if ($group_member) {
2103 $group_member = $e->retrieve_action_trigger_event_def_group_member($group_member);
2104 if ($group_member and $U->is_true($group_member->holdings)) {
2109 $holdings_context_org = $e->retrieve_actor_org_unit($holdings_context_org);
2112 if (ref $bib_id ne 'ARRAY') {
2113 $bib_ids = [ $bib_id ];
2118 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
2119 $bucket->btype('temp');
2120 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
2122 $bucket->owner($e->requestor || 1)
2126 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
2128 for my $id (@$bib_ids) {
2130 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
2132 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
2133 $bucket_entry->target_biblio_record_entry($bib);
2134 $bucket_entry->bucket($bucket_obj->id);
2135 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
2143 subject => $subject,
2144 context_org => $holdings_context_org->shortname,
2145 sort_by => $bib_sort,
2146 sort_dir => $sort_dir,
2152 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $event_context_org, undef, [ $usr_data ]);
2154 } elsif ($for_email) {
2156 return $U->fire_object_event(undef, 'biblio.format.record_entry.email', [ $bucket ], $event_context_org, undef, [ $usr_data ])
2159 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $event_context_org, undef, $usr_data, 1);
2166 __PACKAGE__->register_method(
2167 method => "retrieve_all_copy_statuses",
2168 api_name => "open-ils.search.config.copy_status.retrieve.all"
2171 sub retrieve_all_copy_statuses {
2172 my( $self, $client ) = @_;
2173 return new_editor()->retrieve_all_config_copy_status();
2177 __PACKAGE__->register_method(
2178 method => "copy_counts_per_org",
2179 api_name => "open-ils.search.biblio.copy_counts.retrieve"
2182 __PACKAGE__->register_method(
2183 method => "copy_counts_per_org",
2184 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
2187 sub copy_counts_per_org {
2188 my( $self, $client, $record_id ) = @_;
2190 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
2192 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
2193 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
2195 my $counts = $apputils->simple_scalar_request(
2196 "open-ils.storage", $method, $record_id );
2198 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
2203 __PACKAGE__->register_method(
2204 method => "copy_count_summary",
2205 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
2206 notes => "returns an array of these: "
2207 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2208 . "where statusx is a copy status name. The statuses are sorted by ID.",
2212 sub copy_count_summary {
2213 my( $self, $client, $rid, $org, $depth ) = @_;
2216 my $data = $U->storagereq(
2217 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
2220 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2222 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2226 __PACKAGE__->register_method(
2227 method => "copy_location_count_summary",
2228 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
2229 notes => "returns an array of these: "
2230 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
2231 . "where statusx is a copy status name. The statuses are sorted by ID.",
2234 sub copy_location_count_summary {
2235 my( $self, $client, $rid, $org, $depth ) = @_;
2238 my $data = $U->storagereq(
2239 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2242 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2244 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2246 || $a->[4] cmp $b->[4]
2250 __PACKAGE__->register_method(
2251 method => "copy_count_location_summary",
2252 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2253 notes => "returns an array of these: "
2254 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2255 . "where statusx is a copy status name. The statuses are sorted by ID."
2258 sub copy_count_location_summary {
2259 my( $self, $client, $rid, $org, $depth ) = @_;
2262 my $data = $U->storagereq(
2263 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2265 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2267 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2272 foreach (qw/open-ils.search.biblio.marc
2273 open-ils.search.biblio.marc.staff/)
2275 __PACKAGE__->register_method(
2276 method => "marc_search",
2279 desc => 'Fetch biblio IDs based on MARC record criteria. '
2280 . 'As usual, the .staff version of the search includes otherwise hidden records',
2283 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2284 'See perldoc ' . __PACKAGE__ . ' for more detail.',
2287 {desc => 'timeout (optional)', type => 'number'}
2290 desc => 'Results object like: { "count": $i, "ids": [...] }',
2297 =head3 open-ils.search.biblio.marc (arghash, timeout)
2299 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
2301 searches: complex query object (required)
2302 org_unit: The org ID to focus the search at
2303 depth : The org depth
2304 limit : integer search limit default: 10
2305 offset : integer search offset default: 0
2306 sort : What field to sort the results on? [ author | title | pubdate ]
2307 sort_dir: In what direction do we sort? [ asc | desc ]
2309 Additional keys to refine search criteria:
2312 language : Language (code)
2313 lit_form : Literary form
2314 item_form: Item form
2315 item_type: Item type
2316 format : The MARC format
2318 Please note that the specific strings to be used in the "addtional keys" will be entirely
2319 dependent on your loaded data.
2321 All keys except "searches" are optional.
2322 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
2324 For example, an arg hash might look like:
2346 The arghash is eventually passed to the SRF call:
2347 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2349 Presently, search uses the cache unconditionally.
2353 # FIXME: that example above isn't actually tested.
2354 # FIXME: sort and limit added. item_type not tested yet.
2355 # TODO: docache option?
2357 my( $self, $conn, $args, $timeout ) = @_;
2359 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2360 $method .= ".staff" if $self->api_name =~ /staff/;
2361 $method .= ".atomic";
2363 my $limit = $args->{limit} || 10;
2364 my $offset = $args->{offset} || 0;
2366 # allow caller to pass in a call timeout since MARC searches
2367 # can take longer than the default 60-second timeout.
2368 # Default to 2 mins. Arbitrarily cap at 5 mins.
2369 $timeout = 120 if !$timeout or $timeout > 300;
2372 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2373 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2375 my $recs = search_cache($ckey, $offset, $limit);
2379 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2380 my $req = $ses->request($method, %$args);
2381 my $resp = $req->recv($timeout);
2383 if($resp and $recs = $resp->content) {
2384 put_cache($ckey, scalar(@$recs), $recs);
2393 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2394 my @recs = map { $_->[0] } @$recs;
2396 return { ids => \@recs, count => $count };
2400 foreach my $isbn_method (qw/
2401 open-ils.search.biblio.isbn
2402 open-ils.search.biblio.isbn.staff
2404 __PACKAGE__->register_method(
2405 method => "biblio_search_isbn",
2406 api_name => $isbn_method,
2408 desc => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2410 {desc => 'ISBN', type => 'string'}
2413 desc => 'Results object like: { "count": $i, "ids": [...] }',
2420 sub biblio_search_isbn {
2421 my( $self, $client, $isbn ) = @_;
2422 $logger->debug("Searching ISBN $isbn");
2423 # the previous implementation of this method was essentially unlimited,
2424 # so we will set our limit very high and let multiclass.query provide any
2426 # XXX: if making this unlimited is deemed important, we might consider
2427 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2428 # which is functionally deprecated at this point, or a custom call to
2429 # 'open-ils.storage.biblio.multiclass.search_fts'
2431 my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2432 if ($self->api_name =~ m/.staff$/) {
2433 $isbn_method .= '.staff';
2436 my $method = $self->method_lookup($isbn_method);
2437 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2438 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2439 return { ids => \@recs, count => $search_result->{'count'} };
2442 __PACKAGE__->register_method(
2443 method => "biblio_search_isbn_batch",
2444 api_name => "open-ils.search.biblio.isbn_list",
2447 # XXX: see biblio_search_isbn() for note concerning 'limit'
2448 sub biblio_search_isbn_batch {
2449 my( $self, $client, $isbn_list ) = @_;
2450 $logger->debug("Searching ISBNs @$isbn_list");
2451 my @recs = (); my %rec_set = ();
2452 my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2453 foreach my $isbn ( @$isbn_list ) {
2454 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2455 my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2456 foreach my $rec (@recs_subset) {
2457 if (! $rec_set{ $rec }) {
2458 $rec_set{ $rec } = 1;
2463 return { ids => \@recs, count => int(scalar(@recs)) };
2466 foreach my $issn_method (qw/
2467 open-ils.search.biblio.issn
2468 open-ils.search.biblio.issn.staff
2470 __PACKAGE__->register_method(
2471 method => "biblio_search_issn",
2472 api_name => $issn_method,
2474 desc => 'Retrieve biblio IDs for a given ISSN',
2476 {desc => 'ISBN', type => 'string'}
2479 desc => 'Results object like: { "count": $i, "ids": [...] }',
2486 sub biblio_search_issn {
2487 my( $self, $client, $issn ) = @_;
2488 $logger->debug("Searching ISSN $issn");
2489 # the previous implementation of this method was essentially unlimited,
2490 # so we will set our limit very high and let multiclass.query provide any
2492 # XXX: if making this unlimited is deemed important, we might consider
2493 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2494 # which is functionally deprecated at this point, or a custom call to
2495 # 'open-ils.storage.biblio.multiclass.search_fts'
2497 my $issn_method = 'open-ils.search.biblio.multiclass.query';
2498 if ($self->api_name =~ m/.staff$/) {
2499 $issn_method .= '.staff';
2502 my $method = $self->method_lookup($issn_method);
2503 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2504 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2505 return { ids => \@recs, count => $search_result->{'count'} };
2509 __PACKAGE__->register_method(
2510 method => "fetch_mods_by_copy",
2511 api_name => "open-ils.search.biblio.mods_from_copy",
2514 desc => 'Retrieve MODS record given an attached copy ID',
2516 { desc => 'Copy ID', type => 'number' }
2519 desc => 'MODS record, event on error or uncataloged item'
2524 sub fetch_mods_by_copy {
2525 my( $self, $client, $copyid ) = @_;
2526 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2527 return $evt if $evt;
2528 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2529 return $apputils->record_to_mvr($record);
2533 # -------------------------------------------------------------------------------------
2535 __PACKAGE__->register_method(
2536 method => "cn_browse",
2537 api_name => "open-ils.search.callnumber.browse.target",
2538 notes => "Starts a callnumber browse"
2541 __PACKAGE__->register_method(
2542 method => "cn_browse",
2543 api_name => "open-ils.search.callnumber.browse.page_up",
2544 notes => "Returns the previous page of callnumbers",
2547 __PACKAGE__->register_method(
2548 method => "cn_browse",
2549 api_name => "open-ils.search.callnumber.browse.page_down",
2550 notes => "Returns the next page of callnumbers",
2554 # RETURNS array of arrays like so: label, owning_lib, record, id
2556 my( $self, $client, @params ) = @_;
2559 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2560 if( $self->api_name =~ /target/ );
2561 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2562 if( $self->api_name =~ /page_up/ );
2563 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2564 if( $self->api_name =~ /page_down/ );
2566 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2568 # -------------------------------------------------------------------------------------
2570 __PACKAGE__->register_method(
2571 method => "fetch_cn",
2572 api_name => "open-ils.search.callnumber.retrieve",
2574 notes => "retrieves a callnumber based on ID",
2578 my( $self, $client, $id ) = @_;
2580 my $e = new_editor();
2581 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2582 return $evt if $evt;
2586 __PACKAGE__->register_method(
2587 method => "fetch_fleshed_cn",
2588 api_name => "open-ils.search.callnumber.fleshed.retrieve",
2590 notes => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2593 sub fetch_fleshed_cn {
2594 my( $self, $client, $id ) = @_;
2596 my $e = new_editor();
2597 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2598 return $evt if $evt;
2603 __PACKAGE__->register_method(
2604 method => "fetch_copy_by_cn",
2605 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2607 Returns an array of copy ID's by callnumber ID
2608 @param cnid The callnumber ID
2609 @return An array of copy IDs
2613 sub fetch_copy_by_cn {
2614 my( $self, $conn, $cnid ) = @_;
2615 return $U->cstorereq(
2616 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2617 { call_number => $cnid, deleted => 'f' } );
2620 __PACKAGE__->register_method(
2621 method => 'fetch_cn_by_info',
2622 api_name => 'open-ils.search.call_number.retrieve_by_info',
2624 @param label The callnumber label
2625 @param record The record the cn is attached to
2626 @param org The owning library of the cn
2627 @return The callnumber object
2632 sub fetch_cn_by_info {
2633 my( $self, $conn, $label, $record, $org ) = @_;
2634 return $U->cstorereq(
2635 'open-ils.cstore.direct.asset.call_number.search',
2636 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2641 __PACKAGE__->register_method(
2642 method => 'bib_extras',
2643 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2646 __PACKAGE__->register_method(
2647 method => 'bib_extras',
2648 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2649 ctype => 'item_form'
2651 __PACKAGE__->register_method(
2652 method => 'bib_extras',
2653 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2654 ctype => 'item_type',
2656 __PACKAGE__->register_method(
2657 method => 'bib_extras',
2658 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2659 ctype => 'bib_level'
2661 __PACKAGE__->register_method(
2662 method => 'bib_extras',
2663 api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2669 $logger->warn("deprecation warning: " .$self->api_name);
2671 my $e = new_editor();
2673 my $ctype = $self->{ctype};
2674 my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2677 for my $ccvm (@$ccvms) {
2678 my $obj = "Fieldmapper::config::${ctype}_map"->new;
2679 $obj->value($ccvm->value);
2680 $obj->code($ccvm->code);
2681 $obj->description($ccvm->description) if $obj->can('description');
2690 __PACKAGE__->register_method(
2691 method => 'fetch_slim_record',
2692 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2694 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2696 { desc => 'Array of Record IDs', type => 'array' }
2699 desc => 'Array of biblio records, event on error'
2704 sub fetch_slim_record {
2705 my( $self, $conn, $ids ) = @_;
2707 my $editor = new_editor();
2710 return $editor->event unless
2711 my $r = $editor->retrieve_biblio_record_entry($_);
2718 __PACKAGE__->register_method(
2719 method => 'rec_hold_parts',
2720 api_name => 'open-ils.search.biblio.record_hold_parts',
2722 Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2726 sub rec_hold_parts {
2727 my( $self, $conn, $args ) = @_;
2729 my $rec = $$args{record};
2730 my $mrec = $$args{metarecord};
2731 my $pickup_lib = $$args{pickup_lib};
2732 my $e = new_editor();
2735 select => {bmp => ['id', 'label']},
2740 select => {'acpm' => ['part']},
2741 from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2743 '+acp' => {'deleted' => 'f'},
2744 '+bre' => {id => $rec}
2751 order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2754 if(defined $pickup_lib) {
2755 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2756 if($hard_boundary) {
2757 my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2758 $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2762 return $e->json_query($query);
2768 __PACKAGE__->register_method(
2769 method => 'rec_to_mr_rec_descriptors',
2770 api_name => 'open-ils.search.metabib.record_to_descriptors',
2772 specialized method...
2773 Given a biblio record id or a metarecord id,
2774 this returns a list of metabib.record_descriptor
2775 objects that live within the same metarecord
2776 @param args Object of args including:
2780 sub rec_to_mr_rec_descriptors {
2781 my( $self, $conn, $args ) = @_;
2783 my $rec = $$args{record};
2784 my $mrec = $$args{metarecord};
2785 my $item_forms = $$args{item_forms};
2786 my $item_types = $$args{item_types};
2787 my $item_lang = $$args{item_lang};
2788 my $pickup_lib = $$args{pickup_lib};
2790 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2792 my $e = new_editor();
2796 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2797 return $e->event unless @$map;
2798 $mrec = $$map[0]->metarecord;
2801 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2802 return $e->event unless @$recs;
2804 my @recs = map { $_->source } @$recs;
2805 my $search = { record => \@recs };
2806 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2807 $search->{item_type} = $item_types if $item_types and @$item_types;
2808 $search->{item_lang} = $item_lang if $item_lang;
2810 my $desc = $e->search_metabib_record_descriptor($search);
2814 select => { 'bre' => ['id'] },
2819 'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2825 '+bre' => { id => \@recs },
2830 "+ccs" => { holdable => 't' },
2831 "+acpl" => { holdable => 't', deleted => 'f' }
2835 if ($hard_boundary) { # 0 (or "top") is the same as no setting
2836 my $orgs = $e->json_query(
2837 { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2838 ) or return $e->die_event;
2840 $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2843 my $good_records = $e->json_query($query) or return $e->die_event;
2846 for my $d (@$desc) {
2847 if ( grep { $d->record == $_->{id} } @$good_records ) {
2854 return { metarecord => $mrec, descriptors => $desc };
2858 __PACKAGE__->register_method(
2859 method => 'fetch_age_protect',
2860 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2863 sub fetch_age_protect {
2864 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2868 __PACKAGE__->register_method(
2869 method => 'copies_by_cn_label',
2870 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2873 __PACKAGE__->register_method(
2874 method => 'copies_by_cn_label',
2875 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2878 sub copies_by_cn_label {
2879 my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2880 my $e = new_editor();
2881 my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2882 my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2883 my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2884 return [] unless @$cns;
2886 # show all non-deleted copies in the staff client ...
2887 if ($self->api_name =~ /staff$/o) {
2888 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2891 # ... otherwise, grab the copies ...
2892 my $copies = $e->search_asset_copy(
2893 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2894 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2898 # ... and test for location and status visibility
2899 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2902 __PACKAGE__->register_method(
2903 method => 'bib_copies',
2904 api_name => 'open-ils.search.bib.copies',
2907 __PACKAGE__->register_method(
2908 method => 'bib_copies',
2909 api_name => 'open-ils.search.bib.copies.staff',
2914 my ($self, $client, $rec_id, $org, $depth, $limit, $offset, $pref_ou) = @_;
2915 my $is_staff = ($self->api_name =~ /staff/);
2917 my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
2918 my $req = $cstore->request(
2919 'open-ils.cstore.json_query', mk_copy_query(
2920 $rec_id, $org, $depth, $limit, $offset, $pref_ou, $is_staff));
2923 while ($resp = $req->recv) {
2924 my $copy = $resp->content;
2927 # last_circ is an IDL query so it cannot be queried directly
2929 $copy->{last_circ} =
2930 new_editor()->retrieve_reporter_last_circ_date($copy->{id})
2934 $client->respond($copy);
2940 # TODO: this comes almost directly from WWW/EGCatLoader/Record.pm
2946 my $copy_limit = shift;
2947 my $copy_offset = shift;
2948 my $pref_ou = shift;
2949 my $is_staff = shift;
2950 my $base_query = shift;
2952 my $query = $base_query || $U->basic_opac_copy_query(
2953 $rec_id, undef, undef, $copy_limit, $copy_offset, $is_staff
2956 if ($org) { # TODO: root org test
2957 # no need to add the org join filter if we're not actually filtering
2958 $query->{from}->{acp}->[1] = { aou => {
2964 select => {aou => [{
2966 transform => 'actor.org_unit_descendants',
2967 result_field => 'id',
2971 where => {id => $org}
2978 # Make sure the pref OU is included in the results
2979 my $in = $query->{from}->{acp}->[1]->{aou}->{filter}->{id}->{in};
2980 delete $query->{from}->{acp}->[1]->{aou}->{filter}->{id};
2981 $query->{from}->{acp}->[1]->{aou}->{filter}->{'-or'} = [
2982 {id => {in => $in}},
2988 # Unsure if we want these in the shared function, leaving here for now
2989 unshift(@{$query->{order_by}},
2990 { class => "aou", field => 'id',
2991 transform => 'evergreen.rank_ou', params => [$org, $pref_ou]
2994 push(@{$query->{order_by}},
2995 { class => "acp", field => 'id',
2996 transform => 'evergreen.rank_cp'
3003 __PACKAGE__->register_method(
3004 method => 'record_urls',
3005 api_name => 'open-ils.search.biblio.record.resource_urls.retrieve',
3009 desc => q/Returns bib record 856 URL content./,
3011 {desc => 'Context org unit ID', type => 'number'},
3012 {desc => 'Record ID or Array of Record IDs', type => 'number or array'}
3015 desc => 'Stream of URL objects, one collection object per record',
3022 my ($self, $client, $org_id, $record_ids) = @_;
3024 $record_ids = [$record_ids] unless ref $record_ids eq 'ARRAY';
3026 my $e = new_editor();
3028 for my $record_id (@$record_ids) {
3032 # Start with scoped located URIs
3033 my $uris = $e->json_query({
3034 from => ['evergreen.located_uris_as_uris', $record_id, $org_id]});
3036 for my $uri (@$uris) {
3038 href => $uri->{href},
3039 label => $uri->{label},
3040 note => $uri->{use_restriction}
3044 # Logic copied from TPAC misc_utils.tts
3045 my $bib = $e->retrieve_biblio_record_entry($record_id)
3046 or return $e->event;
3048 my $marc_doc = $U->marc_xml_to_doc($bib->marc);
3050 for my $node ($marc_doc->findnodes('//*[@tag="856" and @ind1="4"]')) {
3053 next if $node->findnodes('./*[@code="9" or @code="w" or @code="n"]');
3056 my ($label) = $node->findnodes('./*[@code="y"]');
3057 my ($notes) = $node->findnodes('./*[@code="z" or @code="3"]');
3060 for my $href_node ($node->findnodes('./*[@code="u"]')) {
3061 next unless $href_node;
3063 # it's possible for multiple $u's to exist within 1 856 tag.
3064 # in that case, honor the label/notes data for the first $u, but
3065 # leave any subsequent $u's as unadorned href's.
3066 # use href/link/note keys to be consistent with args.uri's
3068 my $href = $href_node->textContent;
3071 label => ($first && $label) ? $label->textContent : $href,
3072 note => ($first && $notes) ? $notes->textContent : '',
3073 ind2 => $node->getAttribute('ind2')
3079 $client->respond({id => $record_id, urls => \@urls});
3085 __PACKAGE__->register_method(
3086 method => 'catalog_record_summary',
3087 api_name => 'open-ils.search.biblio.record.catalog_summary',
3089 max_bundle_count => 1,
3091 desc => 'Stream of record data suitable for catalog display',
3093 {desc => 'Context org unit ID', type => 'number'},
3094 {desc => 'Array of Record IDs', type => 'array'}
3098 Stream of record summary objects including id, record,
3099 hold_count, copy_counts, display (metabib display
3100 fields), and attributes (metabib record attrs). The
3101 metabib variant of the call gets metabib_id and
3102 metabib_records, and the regular record version also
3103 gets some metabib information, but returns them as
3104 staff_view_metabib_id, staff_view_metabib_records, and
3105 staff_view_metabib_attributes. This is to mitigate the
3106 need for code changes elsewhere where assumptions are
3107 made when certain fields are returned.
3113 __PACKAGE__->register_method(
3114 method => 'catalog_record_summary',
3115 api_name => 'open-ils.search.biblio.record.catalog_summary.staff',
3117 max_bundle_count => 1,
3118 signature => q/see open-ils.search.biblio.record.catalog_summary/
3120 __PACKAGE__->register_method(
3121 method => 'catalog_record_summary',
3122 api_name => 'open-ils.search.biblio.metabib.catalog_summary',
3124 max_bundle_count => 1,
3125 signature => q/see open-ils.search.biblio.record.catalog_summary/
3128 __PACKAGE__->register_method(
3129 method => 'catalog_record_summary',
3130 api_name => 'open-ils.search.biblio.metabib.catalog_summary.staff',
3132 max_bundle_count => 1,
3133 signature => q/see open-ils.search.biblio.record.catalog_summary/
3137 sub catalog_record_summary {
3138 my ($self, $client, $org_id, $record_ids, $options) = @_;
3139 my $e = new_editor();
3141 my $pref_ou = $options->{pref_ou};
3143 my $is_meta = ($self->api_name =~ /metabib/);
3144 my $is_staff = ($self->api_name =~ /staff/);
3146 my $holds_method = $is_meta ?
3147 'open-ils.circ.mmr.holds.count' :
3148 'open-ils.circ.bre.holds.count';
3150 my $copy_method = $is_meta ?
3151 'open-ils.search.biblio.metarecord.copy_count':
3152 'open-ils.search.biblio.record.copy_count';
3154 $copy_method .= '.staff' if $is_staff;
3156 $copy_method = $self->method_lookup($copy_method); # local method
3158 my $holdable_method = $is_meta ?
3159 'open-ils.search.biblio.metarecord.has_holdable_copy':
3160 'open-ils.search.biblio.record.has_holdable_copy';
3162 $holdable_method = $self->method_lookup($holdable_method); # local method
3164 my %MR_summary_cache;
3165 for my $rec_id (@$record_ids) {
3167 my $response = $is_meta ?
3168 get_one_metarecord_summary($self, $e, $org_id, $rec_id) :
3169 get_one_record_summary($self, $e, $org_id, $rec_id);
3171 # Let's get Formats & Editions data FIXME: consider peer bibs?
3172 my @metabib_records;
3174 my $meta_search = $e->search_metabib_metarecord_source_map({source => $rec_id});
3175 if (scalar(@$meta_search) > 0) {
3176 $response->{staff_view_metabib_id} = $meta_search->[0]->metarecord;
3177 my $maps = $e->search_metabib_metarecord_source_map({metarecord => $response->{staff_view_metabib_id}});
3178 @metabib_records = map { $_->source } @$maps;
3180 # XXX ugly hack for bibs without metarecord mappings, e.g. deleted bibs
3181 # where ingest.metarecord_mapping.preserve_on_delete is false
3182 @metabib_records = ( $rec_id );
3185 $response->{staff_view_metabib_records} = \@metabib_records;
3187 my $metabib_attr = {};
3189 if ($response->{staff_view_metabib_id} and $MR_summary_cache{$response->{staff_view_metabib_id}}) {
3190 $metabib_attr = $MR_summary_cache{$response->{staff_view_metabib_id}};
3192 $attributes = $U->get_bre_attrs(\@metabib_records);
3198 # "label":"National bibliographic agency"
3202 foreach my $bib_id ( keys %{ $attributes } ) {
3203 foreach my $ctype ( keys %{ $attributes->{$bib_id} } ) {
3205 # "srce":{ " ": { "label": "National bibliographic agency", "count" : 1 } },
3208 my $current_code = $attributes->{$bib_id}->{$ctype}->{code};
3209 my $code_label = $attributes->{$bib_id}->{$ctype}->{label};
3210 $metabib_attr->{$ctype} = {} unless $metabib_attr->{$ctype};
3211 if (! $metabib_attr->{$ctype}->{ $current_code }) {
3212 $metabib_attr->{$ctype}->{ $current_code } = {
3213 "label" => $code_label,
3217 $metabib_attr->{$ctype}->{ $current_code }->{count}++;
3223 if ($response->{staff_view_metabib_id}) {
3224 $MR_summary_cache{$response->{staff_view_metabib_id}} = $metabib_attr;
3226 $response->{staff_view_metabib_attributes} = $metabib_attr;
3229 ($response->{copy_counts}) = $copy_method->run($org_id, $rec_id);
3231 $response->{first_call_number} = get_first_call_number(
3232 $e, $rec_id, $org_id, $is_staff, $is_meta, $options);
3236 # If we already have the pref ou copy counts, avoid the extra fetch.
3238 grep {$_->{org_unit} eq $pref_ou} @{$response->{copy_counts}};
3241 my ($counts) = $copy_method->run($pref_ou, $rec_id);
3242 ($match) = grep {$_->{org_unit} eq $pref_ou} @$counts;
3245 $response->{pref_ou_copy_counts} = $match;
3248 $response->{hold_count} =
3249 $U->simplereq('open-ils.circ', $holds_method, $rec_id);
3251 if ($options->{flesh_copies}) {
3252 $response->{copies} = get_representative_copies(
3253 $e, $rec_id, $org_id, $is_staff, $is_meta, $options);
3256 ($response->{has_holdable_copy}) = $holdable_method->run($rec_id);
3258 $client->respond($response);
3264 # Returns a snapshot of copy information for a given record or metarecord,
3265 # sorted by pref org and search org.
3266 sub get_representative_copies {
3267 my ($e, $rec_id, $org_id, $is_staff, $is_meta, $options) = @_;
3270 my $limit = $options->{copy_limit};
3271 my $copy_depth = $options->{copy_depth};
3272 my $copy_offset = $options->{copy_offset};
3273 my $pref_ou = $options->{pref_ou};
3275 my $org_tree = $U->get_org_tree;
3276 if (!$org_id) { $org_id = $org_tree->id; }
3277 my $org = $U->find_org($org_tree, $org_id);
3279 return [] unless $org;
3281 my $func = 'unapi.biblio_record_entry_feed';
3282 my $includes = '{holdings_xml,acp,acnp,acns,circ}';
3283 my $limits = "acn=>$limit,acp=>$limit";
3286 $func = 'unapi.metabib_virtual_record_feed';
3287 $includes = '{holdings_xml,acp,acnp,acns,circ,mmr.unapi}';
3288 $limits .= ",bre=>$limit";
3291 my $xml_query = $e->json_query({from => [
3292 $func, '{'.$rec_id.'}', 'marcxml',
3293 $includes, $org->shortname, $copy_depth, $limits,
3294 undef, undef,undef, undef, undef,
3295 undef, undef, undef, $pref_ou
3298 my $xml = $xml_query->{$func};
3300 my $doc = XML::LibXML->new->parse_string($xml);
3303 for my $volume ($doc->documentElement->findnodes('//*[local-name()="volume"]')) {
3304 my $label = $volume->getAttribute('label');
3305 my $prefix = $volume->getElementsByTagName('call_number_prefix')->[0]->getAttribute('label');
3306 my $suffix = $volume->getElementsByTagName('call_number_suffix')->[0]->getAttribute('label');
3308 my $copies_node = $volume->findnodes('./*[local-name()="copies"]')->[0];
3310 for my $copy ($copies_node->findnodes('./*[local-name()="copy"]')) {
3312 my $status = $copy->getElementsByTagName('status')->[0]->textContent;
3313 my $location = $copy->getElementsByTagName('location')->[0]->textContent;
3314 my $circ_lib_sn = $copy->getElementsByTagName('circ_lib')->[0]->getAttribute('shortname');
3317 my $current_circ = $copy->findnodes('./*[local-name()="current_circulation"]')->[0];
3318 if (my $circ = $current_circ->findnodes('./*[local-name()="circ"]')) {
3319 $due_date = $circ->[0]->getAttribute('due_date');
3323 call_number_label => $label,
3324 call_number_prefix_label => $prefix,
3325 call_number_suffix_label => $suffix,
3326 circ_lib_sn => $circ_lib_sn,
3327 copy_status => $status,
3328 copy_location => $location,
3329 due_date => $due_date
3337 sub get_first_call_number {
3338 my ($e, $rec_id, $org_id, $is_staff, $is_meta, $options) = @_;
3340 my $limit = $options->{copy_limit};
3341 $options->{copy_limit} = 1;
3343 my $copies = get_representative_copies(
3344 $e, $rec_id, $org_id, $is_staff, $is_meta, $options);
3346 $options->{copy_limit} = $limit;
3348 return $copies->[0];
3351 sub get_one_rec_urls {
3352 my ($self, $e, $org_id, $bib_id) = @_;
3354 my ($resp) = $self->method_lookup(
3355 'open-ils.search.biblio.record.resource_urls.retrieve')
3356 ->run($org_id, $bib_id);
3358 return $resp->{urls};
3361 # Start with a bib summary and augment the data with additional
3362 # metarecord content.
3363 sub get_one_metarecord_summary {
3364 my ($self, $e, $org_id, $rec_id) = @_;
3366 my $meta = $e->retrieve_metabib_metarecord($rec_id) or return {};
3367 my $maps = $e->search_metabib_metarecord_source_map({metarecord => $rec_id});
3369 my $bre_id = $meta->master_record;
3371 my $response = get_one_record_summary($self, $e, $org_id, $bre_id);
3372 $response->{urls} = get_one_rec_urls($self, $e, $org_id, $bre_id);
3374 $response->{metabib_id} = $rec_id;
3375 $response->{metabib_records} = [map {$_->source} @$maps];
3377 # Find the sum of record note counts for all mapped bib records
3378 my @record_ids = map {$_->source} @$maps;
3379 my $notes = $e->search_biblio_record_note({ record => \@record_ids });
3380 my $record_note_count = scalar(@{ $notes });
3381 $response->{record_note_count} = $record_note_count;
3383 my @other_bibs = map {$_->source} grep {$_->source != $bre_id} @$maps;
3385 # Augment the record attributes with those of all of the records
3386 # linked to this metarecord.
3388 my $attrs = $e->search_metabib_record_attr_flat({id => \@other_bibs});
3390 my $attributes = $response->{attributes};
3392 for my $attr (@$attrs) {
3393 $attributes->{$attr->attr} = [] unless $attributes->{$attr->attr};
3394 push(@{$attributes->{$attr->attr}}, $attr->value) # avoid dupes
3395 unless grep {$_ eq $attr->value} @{$attributes->{$attr->attr}};
3402 sub get_one_record_summary {
3403 my ($self, $e, $org_id, $rec_id) = @_;
3405 my $bre = $e->retrieve_biblio_record_entry([$rec_id, {
3408 bre => [qw/compressed_display_entries mattrs creator editor/]
3412 # Compressed display fields are packaged as JSON
3414 $display->{$_->name} = OpenSRF::Utils::JSON->JSON2perl($_->value)
3415 foreach @{$bre->compressed_display_entries};
3417 # Create an object of 'mraf' attributes.
3418 # Any attribute can be multi so dedupe and array-ify all of them.
3419 my $attributes = {};
3420 for my $attr (@{$bre->mattrs}) {
3421 $attributes->{$attr->attr} = {} unless $attributes->{$attr->attr};
3422 $attributes->{$attr->attr}->{$attr->value} = 1; # avoid dupes
3424 $attributes->{$_} = [keys %{$attributes->{$_}}] for keys %$attributes;
3426 # Find the count of record notes on this record
3427 my $notes = $e->search_biblio_record_note({ record => $rec_id });
3428 my $record_note_count = scalar(@{ $notes });
3433 $bre->clear_compressed_display_entries;
3438 display => $display,
3439 attributes => $attributes,
3440 urls => get_one_rec_urls($self, $e, $org_id, $rec_id),
3441 record_note_count => $record_note_count
3445 __PACKAGE__->register_method(
3446 method => 'record_copy_counts_global',
3447 api_name => 'open-ils.search.biblio.record.copy_counts.global.staff',
3449 desc => q/Returns a count of copies and call numbers for each org
3450 unit, including items attached to each org unit plus
3451 a sum of counts for all descendants./,
3453 {desc => 'Record ID', type => 'number'}
3456 desc => 'Hash of org unit ID => {copy: $count, call_number: $id}'
3461 sub record_copy_counts_global {
3462 my ($self, $client, $rec_id) = @_;
3464 my $copies = new_editor()->json_query({
3466 acp => [{column => 'id', alias => 'copy_id'}, 'circ_lib'],
3467 acn => [{column => 'id', alias => 'cn_id'}, 'owning_lib']
3469 from => {acn => {acp => {type => 'left'}}},
3474 {id => undef} # left join
3477 '+acn' => {deleted => 'f', record => $rec_id}
3484 for my $copy (@$copies) {
3485 my $org = $copy->{circ_lib} || $copy->{owning_lib};
3486 $hash->{$org} = {copies => 0, call_numbers => 0} unless $hash->{$org};
3487 $hash->{$org}->{copies}++ if $copy->{circ_lib};
3489 if (!$seen_cn{$copy->{cn_id}}) {
3490 $seen_cn{$copy->{cn_id}} = 1;
3491 $hash->{$org}->{call_numbers}++;
3498 my $h = $hash->{$node->id} || {copies => 0, call_numbers => 0};
3501 for my $child (@{$node->children}) {
3502 my $vals = $sum->($child);
3503 $h->{copies} += $vals->{copies};
3504 $h->{call_numbers} += $vals->{call_numbers};
3507 $hash->{$node->id} = $h;
3512 $sum->($U->get_org_tree);