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_";
42 $cache = OpenSRF::Utils::Cache->new('global');
43 my $sclient = OpenSRF::Utils::SettingsClient->new();
44 $cache_timeout = $sclient->config_value(
45 "apps", "open-ils.search", "app_settings", "cache_timeout" ) || 300;
47 $superpage_size = $sclient->config_value(
48 "apps", "open-ils.search", "app_settings", "superpage_size" ) || 500;
50 $max_superpages = $sclient->config_value(
51 "apps", "open-ils.search", "app_settings", "max_superpages" ) || 20;
53 $logger->info("Search cache timeout is $cache_timeout, ".
54 " superpage_size is $superpage_size, max_superpages is $max_superpages");
59 # ---------------------------------------------------------------------------
60 # takes a list of record id's and turns the docs into friendly
61 # mods structures. Creates one MODS structure for each doc id.
62 # ---------------------------------------------------------------------------
63 sub _records_to_mods {
69 my $session = OpenSRF::AppSession->create("open-ils.cstore");
70 my $request = $session->request(
71 "open-ils.cstore.direct.biblio.record_entry.search", { id => \@ids } );
73 while( my $resp = $request->recv ) {
74 my $content = $resp->content;
75 next if $content->id == OILS_PRECAT_RECORD;
76 my $u = OpenILS::Utils::ModsParser->new(); # FIXME: we really need a new parser for each object?
77 $u->start_mods_batch( $content->marc );
78 my $mods = $u->finish_mods_batch();
79 $mods->doc_id($content->id());
80 $mods->tcn($content->tcn_value);
84 $session->disconnect();
88 __PACKAGE__->register_method(
89 method => "record_id_to_mods",
90 api_name => "open-ils.search.biblio.record.mods.retrieve",
93 desc => "Provide ID, we provide the MODS object with copy count. "
94 . "Note: this method does NOT take an array of IDs like mods_slim.retrieve", # FIXME: do it here too
96 { desc => 'Record ID', type => 'number' }
99 desc => 'MODS object', type => 'object'
104 # converts a record into a mods object with copy counts attached
105 sub record_id_to_mods {
107 my( $self, $client, $org_id, $id ) = @_;
109 my $mods_list = _records_to_mods( $id );
110 my $mods_obj = $mods_list->[0];
111 my $cmethod = $self->method_lookup("open-ils.search.biblio.record.copy_count");
112 my ($count) = $cmethod->run($org_id, $id);
113 $mods_obj->copy_count($count);
120 __PACKAGE__->register_method(
121 method => "record_id_to_mods_slim",
122 api_name => "open-ils.search.biblio.record.mods_slim.retrieve",
126 desc => "Provide ID(s), we provide the MODS",
128 { desc => 'Record ID or array of IDs' }
131 desc => 'MODS object(s), event on error'
136 # converts a record into a mods object with NO copy counts attached
137 sub record_id_to_mods_slim {
138 my( $self, $client, $id ) = @_;
139 return undef unless defined $id;
141 if(ref($id) and ref($id) eq 'ARRAY') {
142 return _records_to_mods( @$id );
144 my $mods_list = _records_to_mods( $id );
145 my $mods_obj = $mods_list->[0];
146 return OpenILS::Event->new('BIBLIO_RECORD_ENTRY_NOT_FOUND') unless $mods_obj;
152 __PACKAGE__->register_method(
153 method => "record_id_to_mods_slim_batch",
154 api_name => "open-ils.search.biblio.record.mods_slim.batch.retrieve",
157 sub record_id_to_mods_slim_batch {
158 my($self, $conn, $id_list) = @_;
159 $conn->respond(_records_to_mods($_)->[0]) for @$id_list;
164 # Returns the number of copies attached to a record based on org location
165 __PACKAGE__->register_method(
166 method => "record_id_to_copy_count",
167 api_name => "open-ils.search.biblio.record.copy_count",
169 desc => q/Returns a copy summary for the given record for the context org
170 unit and all ancestor org units/,
172 {desc => 'Context org unit id', type => 'number'},
173 {desc => 'Record ID', type => 'number'}
176 desc => q/summary object per org unit in the set, where the set
177 includes the context org unit and all parent org units.
178 Object includes the keys "transcendant", "count", "org_unit", "depth",
179 "unshadow", "available". Each is a count, except "org_unit" which is
180 the context org unit and "depth" which is the depth of the context org unit
187 __PACKAGE__->register_method(
188 method => "record_id_to_copy_count",
189 api_name => "open-ils.search.biblio.record.copy_count.staff",
192 desc => q/Returns a copy summary for the given record for the context org
193 unit and all ancestor org units/,
195 {desc => 'Context org unit id', type => 'number'},
196 {desc => 'Record ID', type => 'number'}
199 desc => q/summary object per org unit in the set, where the set
200 includes the context org unit and all parent org units.
201 Object includes the keys "transcendant", "count", "org_unit", "depth",
202 "unshadow", "available". Each is a count, except "org_unit" which is
203 the context org unit and "depth" which is the depth of the context org unit
210 __PACKAGE__->register_method(
211 method => "record_id_to_copy_count",
212 api_name => "open-ils.search.biblio.metarecord.copy_count",
214 desc => q/Returns a copy summary for the given record for the context org
215 unit and all ancestor org units/,
217 {desc => 'Context org unit id', type => 'number'},
218 {desc => 'Record ID', type => 'number'}
221 desc => q/summary object per org unit in the set, where the set
222 includes the context org unit and all parent org units.
223 Object includes the keys "transcendant", "count", "org_unit", "depth",
224 "unshadow", "available". Each is a count, except "org_unit" which is
225 the context org unit and "depth" which is the depth of the context org unit
232 __PACKAGE__->register_method(
233 method => "record_id_to_copy_count",
234 api_name => "open-ils.search.biblio.metarecord.copy_count.staff",
236 desc => q/Returns a copy summary for the given record for the context org
237 unit and all ancestor org units/,
239 {desc => 'Context org unit id', type => 'number'},
240 {desc => 'Record ID', type => 'number'}
243 desc => q/summary object per org unit in the set, where the set
244 includes the context org unit and all parent org units.
245 Object includes the keys "transcendant", "count", "org_unit", "depth",
246 "unshadow", "available". Each is a count, except "org_unit" which is
247 the context org unit and "depth" which is the depth of the context org
248 unit. "depth" is always -1 when the count from a lasso search is
249 performed, since depth doesn't mean anything in a lasso context.
256 sub record_id_to_copy_count {
257 my( $self, $client, $org_id, $record_id ) = @_;
259 return [] unless $record_id;
261 my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
262 my $staff = $self->api_name =~ /staff/ ? 't' : 'f';
264 my $data = $U->cstorereq(
265 "open-ils.cstore.json_query.atomic",
266 { from => ['asset.' . $key . '_copy_count' => $org_id => $record_id => $staff] }
270 for my $d ( @$data ) { # fix up the key name change required by stored-proc version
271 $$d{count} = delete $$d{visible};
275 return [ sort { $a->{depth} <=> $b->{depth} } @count ];
278 __PACKAGE__->register_method(
279 method => "record_has_holdable_copy",
280 api_name => "open-ils.search.biblio.record.has_holdable_copy",
282 desc => q/Returns a boolean indicating if a record has any holdable copies./,
284 {desc => 'Record ID', type => 'number'}
287 desc => q/bool indicating if the record has any holdable copies/,
293 __PACKAGE__->register_method(
294 method => "record_has_holdable_copy",
295 api_name => "open-ils.search.biblio.metarecord.has_holdable_copy",
297 desc => q/Returns a boolean indicating if a record has any holdable copies./,
299 {desc => 'Record ID', type => 'number'}
302 desc => q/bool indicating if the record has any holdable copies/,
308 sub record_has_holdable_copy {
309 my($self, $client, $record_id ) = @_;
311 return 0 unless $record_id;
313 my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
315 my $data = $U->cstorereq(
316 "open-ils.cstore.json_query.atomic",
317 { from => ['asset.' . $key . '_has_holdable_copy' => $record_id ] }
320 return ${@$data[0]}{'asset.' . $key . '_has_holdable_copy'} eq 't';
324 __PACKAGE__->register_method(
325 method => "biblio_search_tcn",
326 api_name => "open-ils.search.biblio.tcn",
329 desc => "Retrieve related record ID(s) given a TCN",
331 { desc => 'TCN', type => 'string' },
332 { desc => 'Flag indicating to include deleted records', type => 'string' }
335 desc => 'Results object like: { "count": $i, "ids": [...] }',
342 sub biblio_search_tcn {
344 my( $self, $client, $tcn, $include_deleted ) = @_;
346 $tcn =~ s/^\s+|\s+$//og;
348 my $e = new_editor();
349 my $search = {tcn_value => $tcn};
350 $search->{deleted} = 'f' unless $include_deleted;
351 my $recs = $e->search_biblio_record_entry( $search, {idlist =>1} );
353 return { count => scalar(@$recs), ids => $recs };
357 # --------------------------------------------------------------------------------
359 __PACKAGE__->register_method(
360 method => "biblio_barcode_to_copy",
361 api_name => "open-ils.search.asset.copy.find_by_barcode",
363 sub biblio_barcode_to_copy {
364 my( $self, $client, $barcode ) = @_;
365 my( $copy, $evt ) = $U->fetch_copy_by_barcode($barcode);
370 __PACKAGE__->register_method(
371 method => "biblio_id_to_copy",
372 api_name => "open-ils.search.asset.copy.batch.retrieve",
374 sub biblio_id_to_copy {
375 my( $self, $client, $ids ) = @_;
376 $logger->info("Fetching copies @$ids");
377 return $U->cstorereq(
378 "open-ils.cstore.direct.asset.copy.search.atomic", { id => $ids } );
382 __PACKAGE__->register_method(
383 method => "biblio_id_to_uris",
384 api_name=> "open-ils.search.asset.uri.retrieve_by_bib",
388 @param BibID Which bib record contains the URIs
389 @param OrgID Where to look for URIs
390 @param OrgDepth Range adjustment for OrgID
391 @return A stream or list of 'auri' objects
395 sub biblio_id_to_uris {
396 my( $self, $client, $bib, $org, $depth ) = @_;
397 die "Org ID required" unless defined($org);
398 die "Bib ID required" unless defined($bib);
401 push @params, $depth if (defined $depth);
403 my $ids = $U->cstorereq( "open-ils.cstore.json_query.atomic",
404 { select => { auri => [ 'id' ] },
408 field => 'call_number',
414 filter => { active => 't' }
425 select => { aou => [ { column => 'id', transform => 'actor.org_unit_descendants', params => \@params, result_field => 'id' } ] },
427 where => { id => $org },
437 my $uris = $U->cstorereq(
438 "open-ils.cstore.direct.asset.uri.search.atomic",
439 { id => [ map { (values %$_) } @$ids ] }
442 $client->respond($_) for (@$uris);
448 __PACKAGE__->register_method(
449 method => "copy_retrieve",
450 api_name => "open-ils.search.asset.copy.retrieve",
453 desc => 'Retrieve a copy object based on the Copy ID',
455 { desc => 'Copy ID', type => 'number'}
458 desc => 'Copy object, event on error'
464 my( $self, $client, $cid ) = @_;
465 my( $copy, $evt ) = $U->fetch_copy($cid);
466 return $evt || $copy;
469 __PACKAGE__->register_method(
470 method => "volume_retrieve",
471 api_name => "open-ils.search.asset.call_number.retrieve"
473 sub volume_retrieve {
474 my( $self, $client, $vid ) = @_;
475 my $e = new_editor();
476 my $vol = $e->retrieve_asset_call_number($vid) or return $e->event;
480 __PACKAGE__->register_method(
481 method => "fleshed_copy_retrieve_batch",
482 api_name => "open-ils.search.asset.copy.fleshed.batch.retrieve",
486 sub fleshed_copy_retrieve_batch {
487 my( $self, $client, $ids ) = @_;
488 $logger->info("Fetching fleshed copies @$ids");
489 return $U->cstorereq(
490 "open-ils.cstore.direct.asset.copy.search.atomic",
493 flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries parts / ] }
498 __PACKAGE__->register_method(
499 method => "fleshed_copy_retrieve",
500 api_name => "open-ils.search.asset.copy.fleshed.retrieve",
503 sub fleshed_copy_retrieve {
504 my( $self, $client, $id ) = @_;
505 my( $c, $e) = $U->fetch_fleshed_copy($id);
510 __PACKAGE__->register_method(
511 method => 'fleshed_by_barcode',
512 api_name => "open-ils.search.asset.copy.fleshed2.find_by_barcode",
515 sub fleshed_by_barcode {
516 my( $self, $conn, $barcode ) = @_;
517 my $e = new_editor();
518 my $copyid = $e->search_asset_copy(
519 {barcode => $barcode, deleted => 'f'}, {idlist=>1})->[0]
521 return fleshed_copy_retrieve2( $self, $conn, $copyid);
525 __PACKAGE__->register_method(
526 method => "fleshed_copy_retrieve2",
527 api_name => "open-ils.search.asset.copy.fleshed2.retrieve",
531 sub fleshed_copy_retrieve2 {
532 my( $self, $client, $id ) = @_;
533 my $e = new_editor();
534 my $copy = $e->retrieve_asset_copy(
541 qw/ location status stat_cat_entry_copy_maps notes age_protect parts peer_record_maps /
543 ascecm => [qw/ stat_cat stat_cat_entry /],
547 ) or return $e->event;
549 # For backwards compatibility
550 #$copy->stat_cat_entries($copy->stat_cat_entry_copy_maps);
552 if( $copy->status->id == OILS_COPY_STATUS_CHECKED_OUT ) {
554 $e->search_action_circulation(
556 { target_copy => $copy->id },
558 order_by => { circ => 'xact_start desc' },
570 __PACKAGE__->register_method(
571 method => 'flesh_copy_custom',
572 api_name => 'open-ils.search.asset.copy.fleshed.custom',
576 sub flesh_copy_custom {
577 my( $self, $conn, $copyid, $fields ) = @_;
578 my $e = new_editor();
579 my $copy = $e->retrieve_asset_copy(
589 ) or return $e->event;
594 __PACKAGE__->register_method(
595 method => "biblio_barcode_to_title",
596 api_name => "open-ils.search.biblio.find_by_barcode",
599 sub biblio_barcode_to_title {
600 my( $self, $client, $barcode ) = @_;
602 my $title = $apputils->simple_scalar_request(
604 "open-ils.storage.biblio.record_entry.retrieve_by_barcode", $barcode );
606 return { ids => [ $title->id ], count => 1 } if $title;
607 return { count => 0 };
610 __PACKAGE__->register_method(
611 method => 'title_id_by_item_barcode',
612 api_name => 'open-ils.search.bib_id.by_barcode',
615 desc => 'Retrieve bib record id associated with the copy identified by the given barcode',
617 { desc => 'Item barcode', type => 'string' }
620 desc => 'Bib record id.'
625 __PACKAGE__->register_method(
626 method => 'title_id_by_item_barcode',
627 api_name => 'open-ils.search.multi_home.bib_ids.by_barcode',
630 desc => 'Retrieve bib record ids associated with the copy identified by the given barcode. This includes peer bibs for Multi-Home items.',
632 { desc => 'Item barcode', type => 'string' }
635 desc => 'Array of bib record ids. First element is the native bib for the item.'
641 sub title_id_by_item_barcode {
642 my( $self, $conn, $barcode ) = @_;
643 my $e = new_editor();
644 my $copies = $e->search_asset_copy(
646 { deleted => 'f', barcode => $barcode },
650 acp => [ 'call_number' ],
657 return $e->event unless @$copies;
659 if( $self->api_name =~ /multi_home/ ) {
660 my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
662 { target_copy => $$copies[0]->id }
665 my @temp = map { $_->peer_record } @{ $multi_home_list };
666 unshift @temp, $$copies[0]->call_number->record->id;
669 return $$copies[0]->call_number->record->id;
673 __PACKAGE__->register_method(
674 method => 'find_peer_bibs',
675 api_name => 'open-ils.search.peer_bibs.test',
678 desc => 'Tests to see if the specified record is a peer record.',
680 { desc => 'Biblio record entry Id', type => 'number' }
683 desc => 'True if specified id can be found in biblio.peer_bib_copy_map.peer_record.',
689 __PACKAGE__->register_method(
690 method => 'find_peer_bibs',
691 api_name => 'open-ils.search.peer_bibs',
694 desc => 'Return acps and mvrs for multi-home items linked to specified peer record.',
696 { desc => 'Biblio record entry Id', type => 'number' }
699 desc => '{ records => Array of mvrs, items => array of acps }',
706 my( $self, $client, $doc_id ) = @_;
707 my $e = new_editor();
709 my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
711 { peer_record => $doc_id },
715 bpbcm => [ 'target_copy', 'peer_type' ],
716 acp => [ 'call_number', 'location', 'status', 'peer_record_maps' ]
722 if ($self->api_name =~ /test/) {
723 return scalar( @{$multi_home_list} ) > 0 ? 1 : 0;
726 if (scalar(@{$multi_home_list})==0) {
730 # create a unique hash of the primary record MVRs for foreign copies
731 # XXX PLEASE let's change to unAPI2 (supports foreign copies) in the TT opac?!?
733 ($_->target_copy->call_number->record, _records_to_mods( $_->target_copy->call_number->record )->[0])
736 # set the foreign_copy_maps field to an empty array
737 map { $rec_hash{$_}->foreign_copy_maps([]) } keys( %rec_hash );
739 # push the maps onto the correct MVRs
740 for (@$multi_home_list) {
742 @{$rec_hash{ $_->target_copy->call_number->record }->foreign_copy_maps()},
747 return [sort {$a->title cmp $b->title} values(%rec_hash)];
750 __PACKAGE__->register_method(
751 method => "biblio_copy_to_mods",
752 api_name => "open-ils.search.biblio.copy.mods.retrieve",
755 # takes a copy object and returns it fleshed mods object
756 sub biblio_copy_to_mods {
757 my( $self, $client, $copy ) = @_;
759 my $volume = $U->cstorereq(
760 "open-ils.cstore.direct.asset.call_number.retrieve",
761 $copy->call_number() );
763 my $mods = _records_to_mods($volume->record());
764 $mods = shift @$mods;
765 $volume->copies([$copy]);
766 push @{$mods->call_numbers()}, $volume;
774 OpenILS::Application::Search::Biblio
780 =head3 open-ils.search.biblio.multiclass.query (arghash, query, docache)
782 For arghash and docache, see B<open-ils.search.biblio.multiclass>.
784 The query argument is a string, but built like a hash with key: value pairs.
785 Recognized search keys include:
787 keyword (kw) - search keyword(s) *
788 author (au) - search author(s) *
789 name (au) - same as author *
790 title (ti) - search title *
791 subject (su) - search subject *
792 series (se) - search series *
793 lang - limit by language (specify multiple langs with lang:l1 lang:l2 ...)
794 site - search at specified org unit, corresponds to actor.org_unit.shortname
795 pref_ou - extend search to specified org unit, corresponds to actor.org_unit.shortname
796 sort - sort type (title, author, pubdate)
797 dir - sort direction (asc, desc)
798 available - if set to anything other than "false" or "0", limits to available items
800 * Searching keyword, author, title, subject, and series supports additional search
801 subclasses, specified with a "|". For example, C<title|proper:gone with the wind>.
803 For more, see B<config.metabib_field>.
807 foreach (qw/open-ils.search.biblio.multiclass.query
808 open-ils.search.biblio.multiclass.query.staff
809 open-ils.search.metabib.multiclass.query
810 open-ils.search.metabib.multiclass.query.staff/)
812 __PACKAGE__->register_method(
814 method => 'multiclass_query',
816 desc => 'Perform a search query. The .staff version of the call includes otherwise hidden hits.',
818 {name => 'arghash', desc => 'Arg hash (see open-ils.search.biblio.multiclass)', type => 'object'},
819 {name => 'query', desc => 'Raw human-readable query (see perldoc '. __PACKAGE__ .')', type => 'string'},
820 {name => 'docache', desc => 'Flag for caching (see open-ils.search.biblio.multiclass)', type => 'object'},
823 desc => 'Search results from query, like: { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
824 type => 'object', # TODO: update as miker's new elements are included
830 sub multiclass_query {
831 # arghash only really supports limit/offset anymore
832 my($self, $conn, $arghash, $query, $docache, $phys_loc) = @_;
836 $query =~ s/^\s+//go;
837 $query =~ s/\s+/ /go;
838 $arghash->{query} = $query
841 $logger->debug("initial search query => $query") if $query;
843 (my $method = $self->api_name) =~ s/\.query/.staged/o;
844 return $self->method_lookup($method)->dispatch($arghash, $docache, $phys_loc);
848 __PACKAGE__->register_method(
849 method => 'cat_search_z_style_wrapper',
850 api_name => 'open-ils.search.biblio.zstyle',
852 signature => q/@see open-ils.search.biblio.multiclass/
855 __PACKAGE__->register_method(
856 method => 'cat_search_z_style_wrapper',
857 api_name => 'open-ils.search.biblio.zstyle.staff',
859 signature => q/@see open-ils.search.biblio.multiclass/
862 sub cat_search_z_style_wrapper {
865 my $authtoken = shift;
868 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
870 my $ou = $cstore->request(
871 'open-ils.cstore.direct.actor.org_unit.search',
872 { parent_ou => undef }
875 my $result = { service => 'native-evergreen-catalog', records => [] };
876 my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
878 $$searchhash{searches}{title}{term} = $$args{search}{title} if $$args{search}{title};
879 $$searchhash{searches}{author}{term} = $$args{search}{author} if $$args{search}{author};
880 $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
881 $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
882 $$searchhash{searches}{'identifier|isbn'}{term} = $$args{search}{isbn} if $$args{search}{isbn};
883 $$searchhash{searches}{'identifier|issn'}{term} = $$args{search}{issn} if $$args{search}{issn};
884 $$searchhash{searches}{'identifier|upc'}{term} = $$args{search}{upc} if $$args{search}{upc};
886 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn} if $$args{search}{tcn};
887 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
888 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate} if $$args{search}{pubdate};
889 $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
891 my $method = 'open-ils.search.biblio.multiclass.staged';
892 $method .= '.staff' if $self->api_name =~ /staff$/;
894 my ($list) = $self->method_lookup($method)->run( $searchhash );
896 if ($list->{count} > 0 and @{$list->{ids}}) {
897 $result->{count} = $list->{count};
899 my $records = $cstore->request(
900 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
901 { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
904 for my $rec ( @$records ) {
906 my $u = OpenILS::Utils::ModsParser->new();
907 $u->start_mods_batch( $rec->marc );
908 my $mods = $u->finish_mods_batch();
910 push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
916 $cstore->disconnect();
920 # ----------------------------------------------------------------------------
921 # These are the main OPAC search methods
922 # ----------------------------------------------------------------------------
924 __PACKAGE__->register_method(
925 method => 'the_quest_for_knowledge',
926 api_name => 'open-ils.search.biblio.multiclass',
928 desc => "Performs a multi class biblio or metabib search",
931 desc => "A search hash with keys: "
932 . "searches, org_unit, depth, limit, offset, format, sort, sort_dir. "
933 . "See perldoc " . __PACKAGE__ . " for more detail",
937 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
942 desc => 'An object of the form: '
943 . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
948 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
950 The search-hash argument can have the following elements:
952 searches: { "$class" : "$value", ...} [REQUIRED]
953 org_unit: The org id to focus the search at
954 depth : The org depth
955 limit : The search limit default: 10
956 offset : The search offset default: 0
957 format : The MARC format
958 sort : What field to sort the results on? [ author | title | pubdate ]
959 sort_dir: What direction do we sort? [ asc | desc ]
960 tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
961 will be tagged with an additional value ("1") as the last value in the record ID array for
962 each record. Requires the 'authtoken'
963 authtoken : Authentication token string; When actions are performed that require a user login
964 (e.g. tagging circulated records), the authentication token is required
966 The searches element is required, must have a hashref value, and the hashref must contain at least one
967 of the following classes as a key:
975 The value paired with a key is the associated search string.
977 The docache argument enables/disables searching and saving results in cache (default OFF).
979 The return object, if successful, will look like:
981 { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
985 __PACKAGE__->register_method(
986 method => 'the_quest_for_knowledge',
987 api_name => 'open-ils.search.biblio.multiclass.staff',
988 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
990 __PACKAGE__->register_method(
991 method => 'the_quest_for_knowledge',
992 api_name => 'open-ils.search.metabib.multiclass',
993 signature => q/@see open-ils.search.biblio.multiclass/
995 __PACKAGE__->register_method(
996 method => 'the_quest_for_knowledge',
997 api_name => 'open-ils.search.metabib.multiclass.staff',
998 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass/
1001 sub the_quest_for_knowledge {
1002 my( $self, $conn, $searchhash, $docache ) = @_;
1004 return { count => 0 } unless $searchhash and
1005 ref $searchhash->{searches} eq 'HASH';
1007 my $method = 'open-ils.storage.biblio.multiclass.search_fts';
1011 if($self->api_name =~ /metabib/) {
1013 $method =~ s/biblio/metabib/o;
1016 # do some simple sanity checking
1017 if(!$searchhash->{searches} or
1018 ( !grep { /^(?:title|author|subject|series|keyword|identifier\|is[bs]n)/ } keys %{$searchhash->{searches}} ) ) {
1019 return { count => 0 };
1022 my $offset = $searchhash->{offset} || 0; # user value or default in local var now
1023 my $limit = $searchhash->{limit} || 10; # user value or default in local var now
1024 my $end = $offset + $limit - 1;
1026 my $maxlimit = 5000;
1027 $searchhash->{offset} = 0; # possible user value overwritten in hash
1028 $searchhash->{limit} = $maxlimit; # possible user value overwritten in hash
1030 return { count => 0 } if $offset > $maxlimit;
1033 push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
1034 my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
1035 my $ckey = $pfx . md5_hex($method . $s);
1037 $logger->info("bib search for: $s");
1039 $searchhash->{limit} -= $offset;
1043 my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1047 $method .= ".staff" if($self->api_name =~ /staff/);
1048 $method .= ".atomic";
1050 for (keys %$searchhash) {
1051 delete $$searchhash{$_}
1052 unless defined $$searchhash{$_};
1055 $result = $U->storagereq( $method, %$searchhash );
1059 $docache = 0; # results came FROM cache, so we don't write back
1062 return {count => 0} unless ($result && $$result[0]);
1066 my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1069 # If we didn't get this data from the cache, put it into the cache
1070 # then return the correct offset of records
1071 $logger->debug("putting search cache $ckey\n");
1072 put_cache($ckey, $count, \@recs);
1076 # if we have the full set of data, trim out
1077 # the requested chunk based on limit and offset
1079 for ($offset..$end) {
1080 last unless $recs[$_];
1081 push(@t, $recs[$_]);
1086 return { ids => \@recs, count => $count };
1090 __PACKAGE__->register_method(
1091 method => 'staged_search',
1092 api_name => 'open-ils.search.biblio.multiclass.staged',
1094 desc => 'Staged search filters out unavailable items. This means that it relies on an estimation strategy for determining ' .
1095 'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering. See "estimation_strategy" in your SRF config.',
1098 desc => "A search hash with keys: "
1099 . "searches, limit, offset. The others are optional, but the 'searches' key/value pair is required, with the value being a hashref. "
1100 . "See perldoc " . __PACKAGE__ . " for more detail",
1104 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1109 desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids. '
1110 . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1115 __PACKAGE__->register_method(
1116 method => 'staged_search',
1117 api_name => 'open-ils.search.biblio.multiclass.staged.staff',
1118 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1120 __PACKAGE__->register_method(
1121 method => 'staged_search',
1122 api_name => 'open-ils.search.metabib.multiclass.staged',
1123 signature => q/@see open-ils.search.biblio.multiclass.staged/
1125 __PACKAGE__->register_method(
1126 method => 'staged_search',
1127 api_name => 'open-ils.search.metabib.multiclass.staged.staff',
1128 signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items. Otherwise, @see open-ils.search.biblio.multiclass.staged/
1131 my $estimation_strategy;
1133 my($self, $conn, $search_hash, $docache, $phys_loc) = @_;
1135 $phys_loc ||= $U->get_org_tree->id;
1137 my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1139 my $method = $IAmMetabib?
1140 'open-ils.storage.metabib.multiclass.staged.search_fts':
1141 'open-ils.storage.biblio.multiclass.staged.search_fts';
1143 $method .= '.staff' if $self->api_name =~ /staff$/;
1144 $method .= '.atomic';
1146 if (!$search_hash->{query}) {
1147 return {count => 0} unless (
1149 $search_hash->{searches} and
1150 int(scalar( keys %{$search_hash->{searches}} )));
1153 my $search_duration;
1154 my $user_offset = $search_hash->{offset} || 0; # user-specified offset
1155 my $user_limit = $search_hash->{limit} || 10;
1156 my $ignore_facet_classes = $search_hash->{ignore_facet_classes};
1157 $user_offset = ($user_offset >= 0) ? $user_offset : 0;
1158 $user_limit = ($user_limit >= 0) ? $user_limit : 10;
1161 # we're grabbing results on a per-superpage basis, which means the
1162 # limit and offset should coincide with superpage boundaries
1163 $search_hash->{offset} = 0;
1164 $search_hash->{limit} = $superpage_size;
1166 # force a well-known check_limit
1167 $search_hash->{check_limit} = $superpage_size;
1168 # restrict total tested to superpage size * number of superpages
1169 $search_hash->{core_limit} = $superpage_size * $max_superpages;
1171 # Set the configured estimation strategy, defaults to 'inclusion'.
1172 unless ($estimation_strategy) {
1173 $estimation_strategy = OpenSRF::Utils::SettingsClient
1176 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1179 $search_hash->{estimation_strategy} = $estimation_strategy;
1181 # pull any existing results from the cache
1182 my $key = search_cache_key($method, $search_hash);
1183 my $facet_key = $key.'_facets';
1184 my $cache_data = $cache->get_cache($key) || {};
1186 # First, we want to make sure that someone else isn't currently trying to perform exactly
1187 # this same search. The point is to allow just one instance of a search to fill the needs
1188 # of all concurrent, identical searches. This will avoid spammy searches killing the
1189 # database without requiring admins to start locking some IP addresses out entirely.
1191 # There's still a tiny race condition where 2 might run, but without sigificantly more code
1192 # and complexity, this is close to the best we can do.
1194 if ($cache_data->{running}) { # someone is already doing the search...
1195 my $stop_looping = time() + $cache_timeout;
1196 while ( sleep(1) and time() < $stop_looping ) { # sleep for a second ... maybe they'll finish
1197 $cache_data = $cache->get_cache($key) || {};
1198 last if (!$cache_data->{running});
1200 } elsif (!$cache_data->{0}) { # we're the first ... let's give it a try
1201 $cache->put_cache($key, { running => $$ }, $cache_timeout / 3);
1204 # keep retrieving results until we find enough to
1205 # fulfill the user-specified limit and offset
1206 my $all_results = [];
1207 my $page; # current superpage
1208 my $current_page_summary = {};
1209 my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1212 for($page = 0; $page < $max_superpages; $page++) {
1214 my $data = $cache_data->{$page};
1218 $logger->debug("staged search: analyzing superpage $page");
1221 # this window of results is already cached
1222 $logger->debug("staged search: found cached results");
1223 $summary = $data->{summary};
1224 $results = $data->{results};
1227 # retrieve the window of results from the database
1228 $logger->debug("staged search: fetching results from the database");
1229 $search_hash->{skip_check} = $page * $superpage_size;
1230 $search_hash->{return_query} = $page == 0 ? 1 : 0;
1233 $results = $U->storagereq($method, %$search_hash);
1234 $search_duration = time - $start;
1235 $summary = shift(@$results) if $results;
1238 $logger->info("search timed out: duration=$search_duration: params=".
1239 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1240 return {count => 0};
1243 $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1245 # Create backwards-compatible result structures
1247 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}, $_->{rel}, $_->{record}]} @$results];
1249 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}]} @$results];
1252 push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1253 $results = [grep {defined $_->[0]} @$results];
1254 cache_staged_search_page($key, $page, $summary, $results) if $docache;
1257 tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib)
1258 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1260 $current_page_summary = $summary;
1262 # add the new set of results to the set under construction
1263 push(@$all_results, @$results);
1265 my $current_count = scalar(@$all_results);
1267 if ($page == 0) { # all summaries are the same, just get the first
1268 for (keys %$summary) {
1269 $global_summary->{$_} = $summary->{$_};
1273 # we've found all the possible hits
1274 last if $current_count == $summary->{visible};
1276 # we've found enough results to satisfy the requested limit/offset
1277 last if $current_count >= ($user_limit + $user_offset);
1279 # we've scanned all possible hits
1280 last if($summary->{checked} < $superpage_size);
1283 # Let other backends grab our data now that we're done.
1284 $cache_data = $cache->get_cache($key);
1285 if ($$cache_data{running} and $$cache_data{running} == $$) {
1286 delete $$cache_data{running};
1287 $cache->put_cache($key, $cache_data, $cache_timeout);
1290 my $setting_names = [ qw/
1291 opac.did_you_mean.max_suggestions
1292 opac.did_you_mean.low_result_threshold
1293 search.symspell.min_suggestion_use_threshold
1294 search.symspell.soundex.weight
1295 search.symspell.pg_trgm.weight
1296 search.symspell.keyboard_distance.weight/ ];
1297 my %suggest_settings = $U->ou_ancestor_setting_batch_insecure(
1298 $phys_loc, $setting_names
1302 $suggest_settings{$_} ||= {value=>undef} for @$setting_names;
1304 # Pull this one off the front, it's not used for the function call
1305 my $max_suggestions_setting = shift @$setting_names;
1306 my $sugg_low_thresh_setting = shift @$setting_names;
1307 $max_suggestions_setting = $suggest_settings{$max_suggestions_setting}{value} // -1;
1308 my $suggest_low_threshold = $suggest_settings{$sugg_low_thresh_setting}{value} || 0;
1310 if ($global_summary->{visible} <= $suggest_low_threshold and $max_suggestions_setting != 0) {
1311 # For now, we're doing one-class/one-term suggestions only
1312 my ($class, $term) = one_class_one_term($global_summary->{query_struct});
1313 if ($class && $term) { # check for suggestions!
1314 my $suggestion_verbosity = 4;
1315 if ($max_suggestions_setting == -1) { # special value that means "only best suggestion, and not always"
1316 $max_suggestions_setting = 1;
1317 $suggestion_verbosity = 0;
1320 my @settings_params = map { $suggest_settings{$_}{value} } @$setting_names;
1321 my $suggs = new_editor()->json_query({
1323 'search.symspell_lookup',
1325 $suggestion_verbosity,
1329 limit => $max_suggestions_setting
1331 if (@$suggs and $$suggs[0]{suggestion} ne $term) {
1332 $global_summary->{suggestions}{'one_class_one_term'} = {
1335 suggestions => $suggs
1341 my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1343 $conn->respond_complete(
1345 global_summary => $global_summary,
1346 count => $global_summary->{visible},
1347 core_limit => $search_hash->{core_limit},
1349 superpage_size => $search_hash->{check_limit},
1350 superpage_summary => $current_page_summary,
1351 facet_key => $facet_key,
1356 $logger->info("Completed canonicalized search is: $$global_summary{canonicalized_query}");
1358 return cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1361 sub one_class_one_term {
1362 my $qstruct = shift;
1363 my $node = $$qstruct{children};
1370 or @{$$node{'&'}} != 1
1371 or ($$node{'&'}[0]{fields} and @{$$node{'&'}[0]{fields}} > 0)
1374 $class ||= $$node{'&'}[0]{class};
1375 $term ||= $$node{'&'}[0]{content};
1379 $node = $$node{'&'}[0]{children};
1382 return ($class, $term);
1385 sub fetch_display_fields {
1388 my $highlight_map = shift;
1392 $conn->respond_complete;
1396 my $hl_map_string = "";
1397 if (ref($highlight_map) =~ /HASH/) {
1398 for my $tsq (keys %$highlight_map) {
1399 my $field_list = join(',', @{$$highlight_map{$tsq}});
1400 $hl_map_string .= ' || ' if $hl_map_string;
1401 $hl_map_string .= "hstore(($tsq)\:\:TEXT,'$field_list')";
1405 my $e = new_editor();
1407 for my $record ( @records ) {
1408 next unless ($record && $hl_map_string);
1411 {from => ['search.highlight_display_fields', $record, $hl_map_string]}
1418 __PACKAGE__->register_method(
1419 method => 'fetch_display_fields',
1420 api_name => 'open-ils.search.fetch.metabib.display_field.highlight',
1425 sub tag_circulated_records {
1426 my ($auth, $results, $metabib) = @_;
1427 my $e = new_editor(authtoken => $auth);
1428 return $results unless $e->checkauth;
1431 select => { acn => [{ column => 'record', alias => 'tagme' }] },
1432 from => { auch => { acp => { join => 'acn' }} },
1433 where => { usr => $e->requestor->id },
1439 select => { mmrsm => [{ column => 'metarecord', alias => 'tagme' }] },
1441 where => { source => { in => $query } },
1446 # Give me the distinct set of bib records that exist in the user's visible circulation history
1447 my $circ_recs = $e->json_query( $query );
1449 # if the record appears in the circ history, push a 1 onto
1450 # the rec array structure to indicate truthiness
1451 for my $rec (@$results) {
1452 push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1458 # creates a unique token to represent the query in the cache
1459 sub search_cache_key {
1461 my $search_hash = shift;
1463 for my $key (sort keys %$search_hash) {
1464 push(@sorted, ($key => $$search_hash{$key}))
1465 unless $key eq 'limit' or
1467 $key eq 'skip_check';
1469 my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1470 return $pfx . md5_hex($method . $s);
1473 sub retrieve_cached_facets {
1479 return undef unless ($key and $key =~ /_facets$/);
1482 local $SIG{ALRM} = sub {die};
1483 alarm(10); # we'll sleep for as much as 10s
1485 die if $cache->get_cache($key . '_COMPLETE');
1486 } while (sleep(0.05));
1491 my $blob = $cache->get_cache($key) || {};
1495 for my $f ( keys %$blob ) {
1496 my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1497 @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1498 for my $s ( @sorted ) {
1499 my ($k) = keys(%$s);
1500 my ($v) = values(%$s);
1501 $$facets{$f}{$k} = $v;
1511 __PACKAGE__->register_method(
1512 method => "retrieve_cached_facets",
1513 api_name => "open-ils.search.facet_cache.retrieve",
1515 desc => 'Returns facet data derived from a specific search based on a key '.
1516 'generated by open-ils.search.biblio.multiclass.staged and friends.',
1519 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1524 desc => 'Two level hash of facet values. Top level key is the facet id defined on the config.metabib_field table. '.
1525 'Second level key is a string facet value. Datum attached to each facet value is the number of distinct records, '.
1526 'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1527 'facet retrieval. These counts are calculated for all superpages that have been checked for visibility.',
1535 # add facets for this search to the facet cache
1536 my($key, $results, $metabib, $ignore) = @_;
1537 my $data = $cache->get_cache($key);
1540 return undef unless (@$results);
1542 my $facets_function = $metabib ? 'search.facets_for_metarecord_set'
1543 : 'search.facets_for_record_set';
1544 my $results_str = '{' . join(',', @$results) . '}';
1545 my $ignore_str = ref($ignore) ? '{' . join(',', @$ignore) . '}'
1548 from => [ $facets_function, $ignore_str, $results_str ]
1551 my $facets = OpenILS::Utils::CStoreEditor->new->json_query($query, {substream => 1});
1553 for my $facet (@$facets) {
1554 next unless ($facet->{value});
1555 $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1558 $logger->info("facet compilation: cached with key=$key");
1560 $cache->put_cache($key, $data, $cache_timeout);
1561 $cache->put_cache($key.'_COMPLETE', 1, $cache_timeout);
1564 sub cache_staged_search_page {
1565 # puts this set of results into the cache
1566 my($key, $page, $summary, $results) = @_;
1567 my $data = $cache->get_cache($key);
1570 summary => $summary,
1574 $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1575 ($summary->{estimated_hit_count} || "none") .
1576 ", visible=" . ($summary->{visible} || "none")
1579 $cache->put_cache($key, $data, $cache_timeout);
1587 my $start = $offset;
1588 my $end = $offset + $limit - 1;
1590 $logger->debug("searching cache for $key : $start..$end\n");
1592 return undef unless $cache;
1593 my $data = $cache->get_cache($key);
1595 return undef unless $data;
1597 my $count = $data->[0];
1600 return undef unless $offset < $count;
1603 for( my $i = $offset; $i <= $end; $i++ ) {
1604 last unless my $d = $$data[$i];
1605 push( @result, $d );
1608 $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1615 my( $key, $count, $data ) = @_;
1616 return undef unless $cache;
1617 $logger->debug("search_cache putting ".
1618 scalar(@$data)." items at key $key with timeout $cache_timeout");
1619 $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1623 __PACKAGE__->register_method(
1624 method => "biblio_mrid_to_modsbatch_batch",
1625 api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1628 sub biblio_mrid_to_modsbatch_batch {
1629 my( $self, $client, $mrids) = @_;
1630 # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1632 my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1633 for my $id (@$mrids) {
1634 next unless defined $id;
1635 my ($m) = $method->run($id);
1642 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1643 open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1645 __PACKAGE__->register_method(
1646 method => "biblio_mrid_to_modsbatch",
1649 desc => "Returns the mvr associated with a given metarecod. If none exists, it is created. "
1650 . "As usual, the .staff version of this method will include otherwise hidden records.",
1652 { desc => 'Metarecord ID', type => 'number' },
1653 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1656 desc => 'MVR Object, event on error',
1662 sub biblio_mrid_to_modsbatch {
1663 my( $self, $client, $mrid, $args) = @_;
1665 # warn "Grabbing mvr for $mrid\n"; # unconditional warn
1667 my ($mr, $evt) = _grab_metarecord($mrid);
1668 return $evt unless $mr;
1670 my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1671 biblio_mrid_make_modsbatch($self, $client, $mr);
1673 return $mvr unless ref($args);
1675 # Here we find the lead record appropriate for the given filters
1676 # and use that for the title and author of the metarecord
1677 my $format = $$args{format};
1678 my $org = $$args{org};
1679 my $depth = $$args{depth};
1681 return $mvr unless $format or $org or $depth;
1683 my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1684 $method = "$method.staff" if $self->api_name =~ /staff/o;
1686 my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1688 if( my $mods = $U->record_to_mvr($rec) ) {
1690 $mvr->title( $mods->title );
1691 $mvr->author($mods->author);
1692 $logger->debug("mods_slim updating title and ".
1693 "author in mvr with ".$mods->title." : ".$mods->author);
1699 # converts a metarecord to an mvr
1702 my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1703 return Fieldmapper::metabib::virtual_record->new($perl);
1706 # checks to see if a metarecord has mods, if so returns true;
1708 __PACKAGE__->register_method(
1709 method => "biblio_mrid_check_mvr",
1710 api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1711 notes => "Takes a metarecord ID or a metarecord object and returns true "
1712 . "if the metarecord already has an mvr associated with it."
1715 sub biblio_mrid_check_mvr {
1716 my( $self, $client, $mrid ) = @_;
1720 if(ref($mrid)) { $mr = $mrid; }
1721 else { ($mr, $evt) = _grab_metarecord($mrid); }
1722 return $evt if $evt;
1724 # warn "Checking mvr for mr " . $mr->id . "\n"; # unconditional warn
1726 return _mr_to_mvr($mr) if $mr->mods();
1730 sub _grab_metarecord {
1732 my $e = new_editor();
1733 my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1738 __PACKAGE__->register_method(
1739 method => "biblio_mrid_make_modsbatch",
1740 api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1741 notes => "Takes either a metarecord ID or a metarecord object. "
1742 . "Forces the creations of an mvr for the given metarecord. "
1743 . "The created mvr is returned."
1746 sub biblio_mrid_make_modsbatch {
1747 my( $self, $client, $mrid ) = @_;
1749 my $e = new_editor();
1756 $mr = $e->retrieve_metabib_metarecord($mrid)
1757 or return $e->event;
1760 my $masterid = $mr->master_record;
1761 $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1763 my $ids = $U->storagereq(
1764 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1765 return undef unless @$ids;
1767 my $master = $e->retrieve_biblio_record_entry($masterid)
1768 or return $e->event;
1770 # start the mods batch
1771 my $u = OpenILS::Utils::ModsParser->new();
1772 $u->start_mods_batch( $master->marc );
1774 # grab all of the sub-records and shove them into the batch
1775 my @ids = grep { $_ ne $masterid } @$ids;
1776 #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1781 my $r = $e->retrieve_biblio_record_entry($i);
1782 push( @$subrecs, $r ) if $r;
1787 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1788 $u->push_mods_batch( $_->marc ) if $_->marc;
1792 # finish up and send to the client
1793 my $mods = $u->finish_mods_batch();
1794 $mods->doc_id($mrid);
1795 $client->respond_complete($mods);
1798 # now update the mods string in the db
1799 my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1802 $e = new_editor(xact => 1);
1803 $e->update_metabib_metarecord($mr)
1804 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1811 # converts a mr id into a list of record ids
1813 foreach (qw/open-ils.search.biblio.metarecord_to_records
1814 open-ils.search.biblio.metarecord_to_records.staff/)
1816 __PACKAGE__->register_method(
1817 method => "biblio_mrid_to_record_ids",
1820 desc => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1821 . "As usual, the .staff version of this method will include otherwise hidden records.",
1823 { desc => 'Metarecord ID', type => 'number' },
1824 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1827 desc => 'Results object like {count => $i, ids =>[...]}',
1835 sub biblio_mrid_to_record_ids {
1836 my( $self, $client, $mrid, $args ) = @_;
1838 my $format = $$args{format};
1839 my $org = $$args{org};
1840 my $depth = $$args{depth};
1842 my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1843 $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o;
1844 my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1846 return { count => scalar(@$recs), ids => $recs };
1850 __PACKAGE__->register_method(
1851 method => "biblio_record_to_marc_html",
1852 api_name => "open-ils.search.biblio.record.html"
1855 __PACKAGE__->register_method(
1856 method => "biblio_record_to_marc_html",
1857 api_name => "open-ils.search.authority.to_html"
1860 # Persistent parsers and setting objects
1861 my $parser = XML::LibXML->new();
1862 my $xslt = XML::LibXSLT->new();
1864 my $slim_marc_sheet;
1865 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1867 sub biblio_record_to_marc_html {
1868 my($self, $client, $recordid, $slim, $marcxml) = @_;
1871 my $dir = $settings_client->config_value("dirs", "xsl");
1874 unless($slim_marc_sheet) {
1875 my $xsl = $settings_client->config_value(
1876 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1878 $xsl = $parser->parse_file("$dir/$xsl");
1879 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1882 $sheet = $slim_marc_sheet;
1886 unless($marc_sheet) {
1887 my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1888 my $xsl = $settings_client->config_value(
1889 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1890 $xsl = $parser->parse_file("$dir/$xsl");
1891 $marc_sheet = $xslt->parse_stylesheet($xsl);
1893 $sheet = $marc_sheet;
1898 my $e = new_editor();
1899 if($self->api_name =~ /authority/) {
1900 $record = $e->retrieve_authority_record_entry($recordid)
1901 or return $e->event;
1903 $record = $e->retrieve_biblio_record_entry($recordid)
1904 or return $e->event;
1906 $marcxml = $record->marc;
1909 my $xmldoc = $parser->parse_string($marcxml);
1910 my $html = $sheet->transform($xmldoc);
1911 return $html->documentElement->toString();
1914 __PACKAGE__->register_method(
1915 method => "send_event_email_output",
1916 api_name => "open-ils.search.biblio.record.email.send_output",
1918 sub send_event_email_output {
1919 my($self, $client, $auth, $event_id, $capkey, $capanswer) = @_;
1920 return undef unless $event_id;
1922 my $captcha_pass = 0;
1925 $real_answer = $cache->get_cache(md5_hex($capkey));
1926 $captcha_pass++ if ($real_answer eq $capanswer);
1929 my $e = new_editor(authtoken => $auth);
1930 return $e->die_event unless $captcha_pass || $e->checkauth;
1932 my $event = $e->retrieve_action_trigger_event([$event_id,{flesh => 1, flesh_fields => { atev => ['template_output']}}]);
1933 return undef unless ($event and $event->template_output);
1935 my $smtp = OpenSRF::Utils::SettingsClient
1937 ->config_value('email_notify', 'smtp_server');
1939 my $sender = Email::Send->new({mailer => 'SMTP'});
1940 $sender->mailer_args([Host => $smtp]);
1945 my $email = _create_mime_email($event->template_output->data);
1948 $stat = $sender->send($email);
1949 } catch Error with {
1950 $err = $stat = shift;
1951 $logger->error("send_event_email_output: Email failed with error: $err");
1954 if( !$err and $stat and $stat->type eq 'success' ) {
1955 $logger->info("send_event_email_output: successfully sent email");
1958 $logger->warn("send_event_email_output: unable to send email: ".Dumper($stat));
1963 sub _create_mime_email {
1964 my $template_output = shift;
1965 my $email = Email::MIME->new($template_output);
1966 for my $hfield (qw/From To Bcc Cc Reply-To Sender/) {
1967 my @headers = $email->header($hfield);
1968 $email->header_str_set($hfield => join(',', @headers)) if ($headers[0]);
1971 my @headers = $email->header('Subject');
1972 $email->header_str_set('Subject' => $headers[0]) if ($headers[0]);
1974 $email->header_set('MIME-Version' => '1.0');
1975 $email->header_set('Content-Type' => "text/plain; charset=UTF-8");
1976 $email->header_set('Content-Transfer-Encoding' => '8bit');
1980 __PACKAGE__->register_method(
1981 method => "format_biblio_record_entry",
1982 api_name => "open-ils.search.biblio.record.print.preview",
1985 __PACKAGE__->register_method(
1986 method => "format_biblio_record_entry",
1987 api_name => "open-ils.search.biblio.record.email.preview",
1990 __PACKAGE__->register_method(
1991 method => "format_biblio_record_entry",
1992 api_name => "open-ils.search.biblio.record.print",
1994 desc => 'Returns a printable version of the specified bib record',
1996 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1997 { desc => 'Context library for holdings, if applicable', type => 'number' },
1998 { desc => 'Sort order, if applicable', type => 'string' },
1999 { desc => 'Sort direction, if applicable', type => 'string' },
2000 { desc => 'Definition Group Member id', type => 'number' },
2003 desc => q/An action_trigger.event object or error event./,
2008 __PACKAGE__->register_method(
2009 method => "format_biblio_record_entry",
2010 api_name => "open-ils.search.biblio.record.email",
2012 desc => 'Emails an A/T templated version of the specified bib records to the authorized user',
2014 { desc => 'Authentication token', type => 'string'},
2015 { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
2016 { desc => 'Context library for holdings, if applicable', type => 'number' },
2017 { desc => 'Sort order, if applicable', type => 'string' },
2018 { desc => 'Sort direction, if applicable', type => 'string' },
2019 { desc => 'Definition Group Member id', type => 'number' },
2020 { desc => 'Whether to bypass auth due to captcha', type => 'bool' },
2021 { desc => 'Email address, if none for the user', type => 'string' },
2022 { desc => 'Subject, if customized', type => 'string' },
2025 desc => q/Undefined on success, otherwise an error event./,
2031 sub format_biblio_record_entry {
2032 my ($self, $conn) = splice @_, 0, 2;
2034 my $for_print = ($self->api_name =~ /print/);
2035 my $for_email = ($self->api_name =~ /email/);
2036 my $preview = ($self->api_name =~ /preview/);
2038 my ($auth, $captcha_pass, $email, $subject);
2041 if (@_ > 5) { # the stuff below is included in the params, safe to splice
2042 ($captcha_pass, $email, $subject) = splice @_, -3, 3;
2045 my ($bib_id, $holdings_context_org, $bib_sort, $sort_dir, $group_member) = @_;
2046 $holdings_context_org ||= $U->get_org_tree->id;
2047 $bib_sort ||= 'author';
2048 $sort_dir ||= 'ascending';
2050 my $e; my $event_context_org; my $type = 'brief';
2053 $event_context_org = $holdings_context_org;
2054 $e = new_editor(xact => 1);
2055 } elsif ($for_email) {
2056 $e = new_editor(authtoken => $auth, xact => 1);
2057 return $e->die_event unless $captcha_pass || $e->checkauth;
2058 $event_context_org = $e->requestor ? $e->requestor->home_ou : $holdings_context_org;
2059 $email ||= $e->requestor ? $e->requestor->email : '';
2062 if ($group_member) {
2063 $group_member = $e->retrieve_action_trigger_event_def_group_member($group_member);
2064 if ($group_member and $U->is_true($group_member->holdings)) {
2069 $holdings_context_org = $e->retrieve_actor_org_unit($holdings_context_org);
2072 if (ref $bib_id ne 'ARRAY') {
2073 $bib_ids = [ $bib_id ];
2078 my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
2079 $bucket->btype('temp');
2080 $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
2082 $bucket->owner($e->requestor || 1)
2086 my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
2088 for my $id (@$bib_ids) {
2090 my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
2092 my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
2093 $bucket_entry->target_biblio_record_entry($bib);
2094 $bucket_entry->bucket($bucket_obj->id);
2095 $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
2103 subject => $subject,
2104 context_org => $holdings_context_org->shortname,
2105 sort_by => $bib_sort,
2106 sort_dir => $sort_dir,
2112 return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $event_context_org, undef, [ $usr_data ]);
2114 } elsif ($for_email) {
2116 return $U->fire_object_event(undef, 'biblio.format.record_entry.email', [ $bucket ], $event_context_org, undef, [ $usr_data ])
2119 $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $event_context_org, undef, $usr_data, 1);
2126 __PACKAGE__->register_method(
2127 method => "retrieve_all_copy_statuses",
2128 api_name => "open-ils.search.config.copy_status.retrieve.all"
2131 sub retrieve_all_copy_statuses {
2132 my( $self, $client ) = @_;
2133 return new_editor()->retrieve_all_config_copy_status();
2137 __PACKAGE__->register_method(
2138 method => "copy_counts_per_org",
2139 api_name => "open-ils.search.biblio.copy_counts.retrieve"
2142 __PACKAGE__->register_method(
2143 method => "copy_counts_per_org",
2144 api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
2147 sub copy_counts_per_org {
2148 my( $self, $client, $record_id ) = @_;
2150 warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
2152 my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
2153 if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
2155 my $counts = $apputils->simple_scalar_request(
2156 "open-ils.storage", $method, $record_id );
2158 $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
2163 __PACKAGE__->register_method(
2164 method => "copy_count_summary",
2165 api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
2166 notes => "returns an array of these: "
2167 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2168 . "where statusx is a copy status name. The statuses are sorted by ID.",
2172 sub copy_count_summary {
2173 my( $self, $client, $rid, $org, $depth ) = @_;
2176 my $data = $U->storagereq(
2177 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
2180 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2182 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2186 __PACKAGE__->register_method(
2187 method => "copy_location_count_summary",
2188 api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
2189 notes => "returns an array of these: "
2190 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
2191 . "where statusx is a copy status name. The statuses are sorted by ID.",
2194 sub copy_location_count_summary {
2195 my( $self, $client, $rid, $org, $depth ) = @_;
2198 my $data = $U->storagereq(
2199 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2202 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2204 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2206 || $a->[4] cmp $b->[4]
2210 __PACKAGE__->register_method(
2211 method => "copy_count_location_summary",
2212 api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2213 notes => "returns an array of these: "
2214 . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2215 . "where statusx is a copy status name. The statuses are sorted by ID."
2218 sub copy_count_location_summary {
2219 my( $self, $client, $rid, $org, $depth ) = @_;
2222 my $data = $U->storagereq(
2223 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2225 (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2227 (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2232 foreach (qw/open-ils.search.biblio.marc
2233 open-ils.search.biblio.marc.staff/)
2235 __PACKAGE__->register_method(
2236 method => "marc_search",
2239 desc => 'Fetch biblio IDs based on MARC record criteria. '
2240 . 'As usual, the .staff version of the search includes otherwise hidden records',
2243 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2244 'See perldoc ' . __PACKAGE__ . ' for more detail.',
2247 {desc => 'timeout (optional)', type => 'number'}
2250 desc => 'Results object like: { "count": $i, "ids": [...] }',
2257 =head3 open-ils.search.biblio.marc (arghash, timeout)
2259 As elsewhere the arghash is the required argument, and must be a hashref. The keys are:
2261 searches: complex query object (required)
2262 org_unit: The org ID to focus the search at
2263 depth : The org depth
2264 limit : integer search limit default: 10
2265 offset : integer search offset default: 0
2266 sort : What field to sort the results on? [ author | title | pubdate ]
2267 sort_dir: In what direction do we sort? [ asc | desc ]
2269 Additional keys to refine search criteria:
2272 language : Language (code)
2273 lit_form : Literary form
2274 item_form: Item form
2275 item_type: Item type
2276 format : The MARC format
2278 Please note that the specific strings to be used in the "addtional keys" will be entirely
2279 dependent on your loaded data.
2281 All keys except "searches" are optional.
2282 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".
2284 For example, an arg hash might look like:
2306 The arghash is eventually passed to the SRF call:
2307 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2309 Presently, search uses the cache unconditionally.
2313 # FIXME: that example above isn't actually tested.
2314 # FIXME: sort and limit added. item_type not tested yet.
2315 # TODO: docache option?
2317 my( $self, $conn, $args, $timeout ) = @_;
2319 my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2320 $method .= ".staff" if $self->api_name =~ /staff/;
2321 $method .= ".atomic";
2323 my $limit = $args->{limit} || 10;
2324 my $offset = $args->{offset} || 0;
2326 # allow caller to pass in a call timeout since MARC searches
2327 # can take longer than the default 60-second timeout.
2328 # Default to 2 mins. Arbitrarily cap at 5 mins.
2329 $timeout = 120 if !$timeout or $timeout > 300;
2332 push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2333 my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2335 my $recs = search_cache($ckey, $offset, $limit);
2339 my $ses = OpenSRF::AppSession->create('open-ils.storage');
2340 my $req = $ses->request($method, %$args);
2341 my $resp = $req->recv($timeout);
2343 if($resp and $recs = $resp->content) {
2344 put_cache($ckey, scalar(@$recs), $recs);
2353 $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2354 my @recs = map { $_->[0] } @$recs;
2356 return { ids => \@recs, count => $count };
2360 foreach my $isbn_method (qw/
2361 open-ils.search.biblio.isbn
2362 open-ils.search.biblio.isbn.staff
2364 __PACKAGE__->register_method(
2365 method => "biblio_search_isbn",
2366 api_name => $isbn_method,
2368 desc => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2370 {desc => 'ISBN', type => 'string'}
2373 desc => 'Results object like: { "count": $i, "ids": [...] }',
2380 sub biblio_search_isbn {
2381 my( $self, $client, $isbn ) = @_;
2382 $logger->debug("Searching ISBN $isbn");
2383 # the previous implementation of this method was essentially unlimited,
2384 # so we will set our limit very high and let multiclass.query provide any
2386 # XXX: if making this unlimited is deemed important, we might consider
2387 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2388 # which is functionally deprecated at this point, or a custom call to
2389 # 'open-ils.storage.biblio.multiclass.search_fts'
2391 my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2392 if ($self->api_name =~ m/.staff$/) {
2393 $isbn_method .= '.staff';
2396 my $method = $self->method_lookup($isbn_method);
2397 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2398 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2399 return { ids => \@recs, count => $search_result->{'count'} };
2402 __PACKAGE__->register_method(
2403 method => "biblio_search_isbn_batch",
2404 api_name => "open-ils.search.biblio.isbn_list",
2407 # XXX: see biblio_search_isbn() for note concerning 'limit'
2408 sub biblio_search_isbn_batch {
2409 my( $self, $client, $isbn_list ) = @_;
2410 $logger->debug("Searching ISBNs @$isbn_list");
2411 my @recs = (); my %rec_set = ();
2412 my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2413 foreach my $isbn ( @$isbn_list ) {
2414 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2415 my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2416 foreach my $rec (@recs_subset) {
2417 if (! $rec_set{ $rec }) {
2418 $rec_set{ $rec } = 1;
2423 return { ids => \@recs, count => int(scalar(@recs)) };
2426 foreach my $issn_method (qw/
2427 open-ils.search.biblio.issn
2428 open-ils.search.biblio.issn.staff
2430 __PACKAGE__->register_method(
2431 method => "biblio_search_issn",
2432 api_name => $issn_method,
2434 desc => 'Retrieve biblio IDs for a given ISSN',
2436 {desc => 'ISBN', type => 'string'}
2439 desc => 'Results object like: { "count": $i, "ids": [...] }',
2446 sub biblio_search_issn {
2447 my( $self, $client, $issn ) = @_;
2448 $logger->debug("Searching ISSN $issn");
2449 # the previous implementation of this method was essentially unlimited,
2450 # so we will set our limit very high and let multiclass.query provide any
2452 # XXX: if making this unlimited is deemed important, we might consider
2453 # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2454 # which is functionally deprecated at this point, or a custom call to
2455 # 'open-ils.storage.biblio.multiclass.search_fts'
2457 my $issn_method = 'open-ils.search.biblio.multiclass.query';
2458 if ($self->api_name =~ m/.staff$/) {
2459 $issn_method .= '.staff';
2462 my $method = $self->method_lookup($issn_method);
2463 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2464 my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2465 return { ids => \@recs, count => $search_result->{'count'} };
2469 __PACKAGE__->register_method(
2470 method => "fetch_mods_by_copy",
2471 api_name => "open-ils.search.biblio.mods_from_copy",
2474 desc => 'Retrieve MODS record given an attached copy ID',
2476 { desc => 'Copy ID', type => 'number' }
2479 desc => 'MODS record, event on error or uncataloged item'
2484 sub fetch_mods_by_copy {
2485 my( $self, $client, $copyid ) = @_;
2486 my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2487 return $evt if $evt;
2488 return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2489 return $apputils->record_to_mvr($record);
2493 # -------------------------------------------------------------------------------------
2495 __PACKAGE__->register_method(
2496 method => "cn_browse",
2497 api_name => "open-ils.search.callnumber.browse.target",
2498 notes => "Starts a callnumber browse"
2501 __PACKAGE__->register_method(
2502 method => "cn_browse",
2503 api_name => "open-ils.search.callnumber.browse.page_up",
2504 notes => "Returns the previous page of callnumbers",
2507 __PACKAGE__->register_method(
2508 method => "cn_browse",
2509 api_name => "open-ils.search.callnumber.browse.page_down",
2510 notes => "Returns the next page of callnumbers",
2514 # RETURNS array of arrays like so: label, owning_lib, record, id
2516 my( $self, $client, @params ) = @_;
2519 $method = 'open-ils.storage.asset.call_number.browse.target.atomic'
2520 if( $self->api_name =~ /target/ );
2521 $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2522 if( $self->api_name =~ /page_up/ );
2523 $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2524 if( $self->api_name =~ /page_down/ );
2526 return $apputils->simplereq( 'open-ils.storage', $method, @params );
2528 # -------------------------------------------------------------------------------------
2530 __PACKAGE__->register_method(
2531 method => "fetch_cn",
2532 api_name => "open-ils.search.callnumber.retrieve",
2534 notes => "retrieves a callnumber based on ID",
2538 my( $self, $client, $id ) = @_;
2540 my $e = new_editor();
2541 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2542 return $evt if $evt;
2546 __PACKAGE__->register_method(
2547 method => "fetch_fleshed_cn",
2548 api_name => "open-ils.search.callnumber.fleshed.retrieve",
2550 notes => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2553 sub fetch_fleshed_cn {
2554 my( $self, $client, $id ) = @_;
2556 my $e = new_editor();
2557 my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2558 return $evt if $evt;
2563 __PACKAGE__->register_method(
2564 method => "fetch_copy_by_cn",
2565 api_name => 'open-ils.search.copies_by_call_number.retrieve',
2567 Returns an array of copy ID's by callnumber ID
2568 @param cnid The callnumber ID
2569 @return An array of copy IDs
2573 sub fetch_copy_by_cn {
2574 my( $self, $conn, $cnid ) = @_;
2575 return $U->cstorereq(
2576 'open-ils.cstore.direct.asset.copy.id_list.atomic',
2577 { call_number => $cnid, deleted => 'f' } );
2580 __PACKAGE__->register_method(
2581 method => 'fetch_cn_by_info',
2582 api_name => 'open-ils.search.call_number.retrieve_by_info',
2584 @param label The callnumber label
2585 @param record The record the cn is attached to
2586 @param org The owning library of the cn
2587 @return The callnumber object
2592 sub fetch_cn_by_info {
2593 my( $self, $conn, $label, $record, $org ) = @_;
2594 return $U->cstorereq(
2595 'open-ils.cstore.direct.asset.call_number.search',
2596 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2601 __PACKAGE__->register_method(
2602 method => 'bib_extras',
2603 api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2606 __PACKAGE__->register_method(
2607 method => 'bib_extras',
2608 api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2609 ctype => 'item_form'
2611 __PACKAGE__->register_method(
2612 method => 'bib_extras',
2613 api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2614 ctype => 'item_type',
2616 __PACKAGE__->register_method(
2617 method => 'bib_extras',
2618 api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2619 ctype => 'bib_level'
2621 __PACKAGE__->register_method(
2622 method => 'bib_extras',
2623 api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2629 $logger->warn("deprecation warning: " .$self->api_name);
2631 my $e = new_editor();
2633 my $ctype = $self->{ctype};
2634 my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2637 for my $ccvm (@$ccvms) {
2638 my $obj = "Fieldmapper::config::${ctype}_map"->new;
2639 $obj->value($ccvm->value);
2640 $obj->code($ccvm->code);
2641 $obj->description($ccvm->description) if $obj->can('description');
2650 __PACKAGE__->register_method(
2651 method => 'fetch_slim_record',
2652 api_name => 'open-ils.search.biblio.record_entry.slim.retrieve',
2654 desc => "Retrieves one or more biblio.record_entry without the attached marcxml",
2656 { desc => 'Array of Record IDs', type => 'array' }
2659 desc => 'Array of biblio records, event on error'
2664 sub fetch_slim_record {
2665 my( $self, $conn, $ids ) = @_;
2667 my $editor = new_editor();
2670 return $editor->event unless
2671 my $r = $editor->retrieve_biblio_record_entry($_);
2678 __PACKAGE__->register_method(
2679 method => 'rec_hold_parts',
2680 api_name => 'open-ils.search.biblio.record_hold_parts',
2682 Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2686 sub rec_hold_parts {
2687 my( $self, $conn, $args ) = @_;
2689 my $rec = $$args{record};
2690 my $mrec = $$args{metarecord};
2691 my $pickup_lib = $$args{pickup_lib};
2692 my $e = new_editor();
2695 select => {bmp => ['id', 'label']},
2700 select => {'acpm' => ['part']},
2701 from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2703 '+acp' => {'deleted' => 'f'},
2704 '+bre' => {id => $rec}
2711 order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2714 if(defined $pickup_lib) {
2715 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2716 if($hard_boundary) {
2717 my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2718 $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2722 return $e->json_query($query);
2728 __PACKAGE__->register_method(
2729 method => 'rec_to_mr_rec_descriptors',
2730 api_name => 'open-ils.search.metabib.record_to_descriptors',
2732 specialized method...
2733 Given a biblio record id or a metarecord id,
2734 this returns a list of metabib.record_descriptor
2735 objects that live within the same metarecord
2736 @param args Object of args including:
2740 sub rec_to_mr_rec_descriptors {
2741 my( $self, $conn, $args ) = @_;
2743 my $rec = $$args{record};
2744 my $mrec = $$args{metarecord};
2745 my $item_forms = $$args{item_forms};
2746 my $item_types = $$args{item_types};
2747 my $item_lang = $$args{item_lang};
2748 my $pickup_lib = $$args{pickup_lib};
2750 my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2752 my $e = new_editor();
2756 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2757 return $e->event unless @$map;
2758 $mrec = $$map[0]->metarecord;
2761 $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2762 return $e->event unless @$recs;
2764 my @recs = map { $_->source } @$recs;
2765 my $search = { record => \@recs };
2766 $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2767 $search->{item_type} = $item_types if $item_types and @$item_types;
2768 $search->{item_lang} = $item_lang if $item_lang;
2770 my $desc = $e->search_metabib_record_descriptor($search);
2774 select => { 'bre' => ['id'] },
2779 'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2785 '+bre' => { id => \@recs },
2790 "+ccs" => { holdable => 't' },
2791 "+acpl" => { holdable => 't', deleted => 'f' }
2795 if ($hard_boundary) { # 0 (or "top") is the same as no setting
2796 my $orgs = $e->json_query(
2797 { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2798 ) or return $e->die_event;
2800 $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2803 my $good_records = $e->json_query($query) or return $e->die_event;
2806 for my $d (@$desc) {
2807 if ( grep { $d->record == $_->{id} } @$good_records ) {
2814 return { metarecord => $mrec, descriptors => $desc };
2818 __PACKAGE__->register_method(
2819 method => 'fetch_age_protect',
2820 api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2823 sub fetch_age_protect {
2824 return new_editor()->retrieve_all_config_rule_age_hold_protect();
2828 __PACKAGE__->register_method(
2829 method => 'copies_by_cn_label',
2830 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2833 __PACKAGE__->register_method(
2834 method => 'copies_by_cn_label',
2835 api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2838 sub copies_by_cn_label {
2839 my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2840 my $e = new_editor();
2841 my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2842 my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2843 my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2844 return [] unless @$cns;
2846 # show all non-deleted copies in the staff client ...
2847 if ($self->api_name =~ /staff$/o) {
2848 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2851 # ... otherwise, grab the copies ...
2852 my $copies = $e->search_asset_copy(
2853 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2854 {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2858 # ... and test for location and status visibility
2859 return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2862 __PACKAGE__->register_method(
2863 method => 'bib_copies',
2864 api_name => 'open-ils.search.bib.copies',
2867 __PACKAGE__->register_method(
2868 method => 'bib_copies',
2869 api_name => 'open-ils.search.bib.copies.staff',
2874 my ($self, $client, $rec_id, $org, $depth, $limit, $offset, $pref_ou) = @_;
2875 my $is_staff = ($self->api_name =~ /staff/);
2877 my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
2878 my $req = $cstore->request(
2879 'open-ils.cstore.json_query', mk_copy_query(
2880 $rec_id, $org, $depth, $limit, $offset, $pref_ou, $is_staff));
2883 while ($resp = $req->recv) {
2884 my $copy = $resp->content;
2887 # last_circ is an IDL query so it cannot be queried directly
2889 $copy->{last_circ} =
2890 new_editor()->retrieve_reporter_last_circ_date($copy->{id})
2894 $client->respond($copy);
2900 # TODO: this comes almost directly from WWW/EGCatLoader/Record.pm
2906 my $copy_limit = shift;
2907 my $copy_offset = shift;
2908 my $pref_ou = shift;
2909 my $is_staff = shift;
2910 my $base_query = shift;
2912 my $query = $base_query || $U->basic_opac_copy_query(
2913 $rec_id, undef, undef, $copy_limit, $copy_offset, $is_staff
2916 if ($org) { # TODO: root org test
2917 # no need to add the org join filter if we're not actually filtering
2918 $query->{from}->{acp}->[1] = { aou => {
2924 select => {aou => [{
2926 transform => 'actor.org_unit_descendants',
2927 result_field => 'id',
2931 where => {id => $org}
2938 # Make sure the pref OU is included in the results
2939 my $in = $query->{from}->{acp}->[1]->{aou}->{filter}->{id}->{in};
2940 delete $query->{from}->{acp}->[1]->{aou}->{filter}->{id};
2941 $query->{from}->{acp}->[1]->{aou}->{filter}->{'-or'} = [
2942 {id => {in => $in}},
2948 # Unsure if we want these in the shared function, leaving here for now
2949 unshift(@{$query->{order_by}},
2950 { class => "aou", field => 'id',
2951 transform => 'evergreen.rank_ou', params => [$org, $pref_ou]
2954 push(@{$query->{order_by}},
2955 { class => "acp", field => 'id',
2956 transform => 'evergreen.rank_cp'
2963 __PACKAGE__->register_method(
2964 method => 'record_urls',
2965 api_name => 'open-ils.search.biblio.record.resource_urls.retrieve',
2969 desc => q/Returns bib record 856 URL content./,
2971 {desc => 'Context org unit ID', type => 'number'},
2972 {desc => 'Record ID or Array of Record IDs', type => 'number or array'}
2975 desc => 'Stream of URL objects, one collection object per record',
2982 my ($self, $client, $org_id, $record_ids) = @_;
2984 $record_ids = [$record_ids] unless ref $record_ids eq 'ARRAY';
2986 my $e = new_editor();
2988 for my $record_id (@$record_ids) {
2992 # Start with scoped located URIs
2993 my $uris = $e->json_query({
2994 from => ['evergreen.located_uris_as_uris', $record_id, $org_id]});
2996 for my $uri (@$uris) {
2998 href => $uri->{href},
2999 label => $uri->{label},
3000 note => $uri->{use_restriction}
3004 # Logic copied from TPAC misc_utils.tts
3005 my $bib = $e->retrieve_biblio_record_entry($record_id)
3006 or return $e->event;
3008 my $marc_doc = $U->marc_xml_to_doc($bib->marc);
3010 for my $node ($marc_doc->findnodes('//*[@tag="856" and @ind1="4"]')) {
3013 next if $node->findnodes('./*[@code="9" or @code="w" or @code="n"]');
3016 my ($label) = $node->findnodes('./*[@code="y"]');
3017 my ($notes) = $node->findnodes('./*[@code="z" or @code="3"]');
3020 for my $href_node ($node->findnodes('./*[@code="u"]')) {
3021 next unless $href_node;
3023 # it's possible for multiple $u's to exist within 1 856 tag.
3024 # in that case, honor the label/notes data for the first $u, but
3025 # leave any subsequent $u's as unadorned href's.
3026 # use href/link/note keys to be consistent with args.uri's
3028 my $href = $href_node->textContent;
3031 label => ($first && $label) ? $label->textContent : $href,
3032 note => ($first && $notes) ? $notes->textContent : '',
3033 ind2 => $node->getAttribute('ind2')
3039 $client->respond({id => $record_id, urls => \@urls});
3045 __PACKAGE__->register_method(
3046 method => 'catalog_record_summary',
3047 api_name => 'open-ils.search.biblio.record.catalog_summary',
3049 max_bundle_count => 1,
3051 desc => 'Stream of record data suitable for catalog display',
3053 {desc => 'Context org unit ID', type => 'number'},
3054 {desc => 'Array of Record IDs', type => 'array'}
3058 Stream of record summary objects including id, record,
3059 hold_count, copy_counts, display (metabib display
3060 fields), attributes (metabib record attrs), plus
3061 metabib_id and metabib_records for the metabib variant.
3066 __PACKAGE__->register_method(
3067 method => 'catalog_record_summary',
3068 api_name => 'open-ils.search.biblio.record.catalog_summary.staff',
3070 max_bundle_count => 1,
3071 signature => q/see open-ils.search.biblio.record.catalog_summary/
3073 __PACKAGE__->register_method(
3074 method => 'catalog_record_summary',
3075 api_name => 'open-ils.search.biblio.metabib.catalog_summary',
3077 max_bundle_count => 1,
3078 signature => q/see open-ils.search.biblio.record.catalog_summary/
3081 __PACKAGE__->register_method(
3082 method => 'catalog_record_summary',
3083 api_name => 'open-ils.search.biblio.metabib.catalog_summary.staff',
3085 max_bundle_count => 1,
3086 signature => q/see open-ils.search.biblio.record.catalog_summary/
3090 sub catalog_record_summary {
3091 my ($self, $client, $org_id, $record_ids, $options) = @_;
3092 my $e = new_editor();
3094 my $pref_ou = $options->{pref_ou};
3096 my $is_meta = ($self->api_name =~ /metabib/);
3097 my $is_staff = ($self->api_name =~ /staff/);
3099 my $holds_method = $is_meta ?
3100 'open-ils.circ.mmr.holds.count' :
3101 'open-ils.circ.bre.holds.count';
3103 my $copy_method = $is_meta ?
3104 'open-ils.search.biblio.metarecord.copy_count':
3105 'open-ils.search.biblio.record.copy_count';
3107 $copy_method .= '.staff' if $is_staff;
3109 $copy_method = $self->method_lookup($copy_method); # local method
3111 my $holdable_method = $is_meta ?
3112 'open-ils.search.biblio.metarecord.has_holdable_copy':
3113 'open-ils.search.biblio.record.has_holdable_copy';
3115 $holdable_method = $self->method_lookup($holdable_method); # local method
3117 for my $rec_id (@$record_ids) {
3119 my $response = $is_meta ?
3120 get_one_metarecord_summary($self, $e, $org_id, $rec_id) :
3121 get_one_record_summary($self, $e, $org_id, $rec_id);
3123 ($response->{copy_counts}) = $copy_method->run($org_id, $rec_id);
3125 $response->{first_call_number} = get_first_call_number(
3126 $e, $rec_id, $org_id, $is_staff, $is_meta, $options);
3130 # If we already have the pref ou copy counts, avoid the extra fetch.
3132 grep {$_->{org_unit} eq $pref_ou} @{$response->{copy_counts}};
3135 my ($counts) = $copy_method->run($pref_ou, $rec_id);
3136 ($match) = grep {$_->{org_unit} eq $pref_ou} @$counts;
3139 $response->{pref_ou_copy_counts} = $match;
3142 $response->{hold_count} =
3143 $U->simplereq('open-ils.circ', $holds_method, $rec_id);
3145 if ($options->{flesh_copies}) {
3146 $response->{copies} = get_representative_copies(
3147 $e, $rec_id, $org_id, $is_staff, $is_meta, $options);
3150 ($response->{has_holdable_copy}) = $holdable_method->run($rec_id);
3152 $client->respond($response);
3158 # Returns a snapshot of copy information for a given record or metarecord,
3159 # sorted by pref org and search org.
3160 sub get_representative_copies {
3161 my ($e, $rec_id, $org_id, $is_staff, $is_meta, $options) = @_;
3164 my $limit = $options->{copy_limit};
3165 my $copy_depth = $options->{copy_depth};
3166 my $copy_offset = $options->{copy_offset};
3167 my $pref_ou = $options->{pref_ou};
3169 my $org_tree = $U->get_org_tree;
3170 if (!$org_id) { $org_id = $org_tree->id; }
3171 my $org = $U->find_org($org_tree, $org_id);
3173 return [] unless $org;
3175 my $func = 'unapi.biblio_record_entry_feed';
3176 my $includes = '{holdings_xml,acp,acnp,acns,circ}';
3177 my $limits = "acn=>$limit,acp=>$limit";
3180 $func = 'unapi.metabib_virtual_record_feed';
3181 $includes = '{holdings_xml,acp,acnp,acns,circ,mmr.unapi}';
3182 $limits .= ",bre=>$limit";
3185 my $xml_query = $e->json_query({from => [
3186 $func, '{'.$rec_id.'}', 'marcxml',
3187 $includes, $org->shortname, $copy_depth, $limits,
3188 undef, undef,undef, undef, undef,
3189 undef, undef, undef, $pref_ou
3192 my $xml = $xml_query->{$func};
3194 my $doc = XML::LibXML->new->parse_string($xml);
3197 for my $volume ($doc->documentElement->findnodes('//*[local-name()="volume"]')) {
3198 my $label = $volume->getAttribute('label');
3199 my $prefix = $volume->getElementsByTagName('call_number_prefix')->[0]->getAttribute('label');
3200 my $suffix = $volume->getElementsByTagName('call_number_suffix')->[0]->getAttribute('label');
3202 my $copies_node = $volume->findnodes('./*[local-name()="copies"]')->[0];
3204 for my $copy ($copies_node->findnodes('./*[local-name()="copy"]')) {
3206 my $status = $copy->getElementsByTagName('status')->[0]->textContent;
3207 my $location = $copy->getElementsByTagName('location')->[0]->textContent;
3208 my $circ_lib_sn = $copy->getElementsByTagName('circ_lib')->[0]->getAttribute('shortname');
3211 my $current_circ = $copy->findnodes('./*[local-name()="current_circulation"]')->[0];
3212 if (my $circ = $current_circ->findnodes('./*[local-name()="circ"]')) {
3213 $due_date = $circ->[0]->getAttribute('due_date');
3217 call_number_label => $label,
3218 call_number_prefix_label => $prefix,
3219 call_number_suffix_label => $suffix,
3220 circ_lib_sn => $circ_lib_sn,
3221 copy_status => $status,
3222 copy_location => $location,
3223 due_date => $due_date
3231 sub get_first_call_number {
3232 my ($e, $rec_id, $org_id, $is_staff, $is_meta, $options) = @_;
3234 my $limit = $options->{copy_limit};
3235 $options->{copy_limit} = 1;
3237 my $copies = get_representative_copies(
3238 $e, $rec_id, $org_id, $is_staff, $is_meta, $options);
3240 $options->{copy_limit} = $limit;
3242 return $copies->[0];
3245 sub get_one_rec_urls {
3246 my ($self, $e, $org_id, $bib_id) = @_;
3248 my ($resp) = $self->method_lookup(
3249 'open-ils.search.biblio.record.resource_urls.retrieve')
3250 ->run($org_id, $bib_id);
3252 return $resp->{urls};
3255 # Start with a bib summary and augment the data with additional
3256 # metarecord content.
3257 sub get_one_metarecord_summary {
3258 my ($self, $e, $org_id, $rec_id) = @_;
3260 my $meta = $e->retrieve_metabib_metarecord($rec_id) or return {};
3261 my $maps = $e->search_metabib_metarecord_source_map({metarecord => $rec_id});
3263 my $bre_id = $meta->master_record;
3265 my $response = get_one_record_summary($self, $e, $org_id, $bre_id);
3266 $response->{urls} = get_one_rec_urls($self, $e, $org_id, $bre_id);
3268 $response->{metabib_id} = $rec_id;
3269 $response->{metabib_records} = [map {$_->source} @$maps];
3271 my @other_bibs = map {$_->source} grep {$_->source != $bre_id} @$maps;
3273 # Augment the record attributes with those of all of the records
3274 # linked to this metarecord.
3276 my $attrs = $e->search_metabib_record_attr_flat({id => \@other_bibs});
3278 my $attributes = $response->{attributes};
3280 for my $attr (@$attrs) {
3281 $attributes->{$attr->attr} = [] unless $attributes->{$attr->attr};
3282 push(@{$attributes->{$attr->attr}}, $attr->value) # avoid dupes
3283 unless grep {$_ eq $attr->value} @{$attributes->{$attr->attr}};
3290 sub get_one_record_summary {
3291 my ($self, $e, $org_id, $rec_id) = @_;
3293 my $bre = $e->retrieve_biblio_record_entry([$rec_id, {
3296 bre => [qw/compressed_display_entries mattrs creator editor/]
3300 # Compressed display fields are pachaged as JSON
3302 $display->{$_->name} = OpenSRF::Utils::JSON->JSON2perl($_->value)
3303 foreach @{$bre->compressed_display_entries};
3305 # Create an object of 'mraf' attributes.
3306 # Any attribute can be multi so dedupe and array-ify all of them.
3307 my $attributes = {};
3308 for my $attr (@{$bre->mattrs}) {
3309 $attributes->{$attr->attr} = {} unless $attributes->{$attr->attr};
3310 $attributes->{$attr->attr}->{$attr->value} = 1; # avoid dupes
3312 $attributes->{$_} = [keys %{$attributes->{$_}}] for keys %$attributes;
3317 $bre->clear_compressed_display_entries;
3322 display => $display,
3323 attributes => $attributes,
3324 urls => get_one_rec_urls($self, $e, $org_id, $rec_id)
3328 __PACKAGE__->register_method(
3329 method => 'record_copy_counts_global',
3330 api_name => 'open-ils.search.biblio.record.copy_counts.global.staff',
3332 desc => q/Returns a count of copies and call numbers for each org
3333 unit, including items attached to each org unit plus
3334 a sum of counts for all descendants./,
3336 {desc => 'Record ID', type => 'number'}
3339 desc => 'Hash of org unit ID => {copy: $count, call_number: $id}'
3344 sub record_copy_counts_global {
3345 my ($self, $client, $rec_id) = @_;
3347 my $copies = new_editor()->json_query({
3349 acp => [{column => 'id', alias => 'copy_id'}, 'circ_lib'],
3350 acn => [{column => 'id', alias => 'cn_id'}, 'owning_lib']
3352 from => {acn => {acp => {type => 'left'}}},
3357 {id => undef} # left join
3360 '+acn' => {deleted => 'f', record => $rec_id}
3367 for my $copy (@$copies) {
3368 my $org = $copy->{circ_lib} || $copy->{owning_lib};
3369 $hash->{$org} = {copies => 0, call_numbers => 0} unless $hash->{$org};
3370 $hash->{$org}->{copies}++ if $copy->{circ_lib};
3372 if (!$seen_cn{$copy->{cn_id}}) {
3373 $seen_cn{$copy->{cn_id}} = 1;
3374 $hash->{$org}->{call_numbers}++;
3381 my $h = $hash->{$node->id} || {copies => 0, call_numbers => 0};
3384 for my $child (@{$node->children}) {
3385 my $vals = $sum->($child);
3386 $h->{copies} += $vals->{copies};
3387 $h->{call_numbers} += $vals->{call_numbers};
3390 $hash->{$node->id} = $h;
3395 $sum->($U->get_org_tree);