]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
Fix some typos
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Search / Biblio.pm
1 package OpenILS::Application::Search::Biblio;
2 use base qw/OpenILS::Application/;
3 use strict; use warnings;
4
5
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;
12 use Encode;
13
14 use OpenSRF::Utils::Logger qw/:logger/;
15
16
17 use OpenSRF::Utils::JSON;
18
19 use Time::HiRes qw(time);
20 use OpenSRF::EX qw(:try);
21 use Digest::MD5 qw(md5_hex);
22
23 use XML::LibXML;
24 use XML::LibXSLT;
25
26 use Data::Dumper;
27 $Data::Dumper::Indent = 0;
28
29 use OpenILS::Const qw/:const/;
30
31 use OpenILS::Application::AppUtils;
32 my $apputils = "OpenILS::Application::AppUtils";
33 my $U = $apputils;
34
35 my $pfx = "open-ils.search_";
36
37 my $cache;
38 my $cache_timeout;
39 my $max_search_results;
40
41 sub initialize {
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;
46
47         my $superpage_size = $sclient->config_value(
48                         "apps", "open-ils.search", "app_settings", "superpage_size" ) || 500;
49
50         my $max_superpages = $sclient->config_value(
51                         "apps", "open-ils.search", "app_settings", "max_superpages" ) || 20;
52
53     $max_search_results = $sclient->config_value(
54             "apps", "open-ils.search", "app_settings", "max_search_results" ) || ($superpage_size * $max_superpages);
55
56         $logger->info("Search cache timeout is $cache_timeout, ".
57         " max_search_results is $max_search_results");
58 }
59
60
61
62 # ---------------------------------------------------------------------------
63 # takes a list of record id's and turns the docs into friendly 
64 # mods structures. Creates one MODS structure for each doc id.
65 # ---------------------------------------------------------------------------
66 sub _records_to_mods {
67         my @ids = @_;
68         
69         my @results;
70         my @marcxml_objs;
71
72         my $session = OpenSRF::AppSession->create("open-ils.cstore");
73         my $request = $session->request(
74                         "open-ils.cstore.direct.biblio.record_entry.search", { id => \@ids } );
75
76         while( my $resp = $request->recv ) {
77                 my $content = $resp->content;
78                 next if $content->id == OILS_PRECAT_RECORD;
79                 my $u = OpenILS::Utils::ModsParser->new();  # FIXME: we really need a new parser for each object?
80                 $u->start_mods_batch( $content->marc );
81                 my $mods = $u->finish_mods_batch();
82                 $mods->doc_id($content->id());
83                 $mods->tcn($content->tcn_value);
84                 push @results, $mods;
85         }
86
87         $session->disconnect();
88         return \@results;
89 }
90
91 __PACKAGE__->register_method(
92     method    => "record_id_to_mods",
93     api_name  => "open-ils.search.biblio.record.mods.retrieve",
94     argc      => 1,
95     signature => {
96         desc   => "Provide ID, we provide the MODS object with copy count.  " 
97                 . "Note: this method does NOT take an array of IDs like mods_slim.retrieve",    # FIXME: do it here too
98         params => [
99             { desc => 'Record ID', type => 'number' }
100         ],
101         return => {
102             desc => 'MODS object', type => 'object'
103         }
104     }
105 );
106
107 # converts a record into a mods object with copy counts attached
108 sub record_id_to_mods {
109
110     my( $self, $client, $org_id, $id ) = @_;
111
112     my $mods_list = _records_to_mods( $id );
113     my $mods_obj  = $mods_list->[0];
114     my $cmethod   = $self->method_lookup("open-ils.search.biblio.record.copy_count");
115     my ($count)   = $cmethod->run($org_id, $id);
116     $mods_obj->copy_count($count);
117
118     return $mods_obj;
119 }
120
121
122
123 __PACKAGE__->register_method(
124     method        => "record_id_to_mods_slim",
125     api_name      => "open-ils.search.biblio.record.mods_slim.retrieve",
126     argc          => 1,
127     authoritative => 1,
128     signature     => {
129         desc   => "Provide ID(s), we provide the MODS",
130         params => [
131             { desc => 'Record ID or array of IDs' }
132         ],
133         return => {
134             desc => 'MODS object(s), event on error'
135         }
136     }
137 );
138
139 # converts a record into a mods object with NO copy counts attached
140 sub record_id_to_mods_slim {
141         my( $self, $client, $id ) = @_;
142         return undef unless defined $id;
143
144         if(ref($id) and ref($id) == 'ARRAY') {
145                 return _records_to_mods( @$id );
146         }
147         my $mods_list = _records_to_mods( $id );
148         my $mods_obj  = $mods_list->[0];
149         return OpenILS::Event->new('BIBLIO_RECORD_ENTRY_NOT_FOUND') unless $mods_obj;
150         return $mods_obj;
151 }
152
153
154
155 __PACKAGE__->register_method(
156     method   => "record_id_to_mods_slim_batch",
157     api_name => "open-ils.search.biblio.record.mods_slim.batch.retrieve",
158     stream   => 1
159 );
160 sub record_id_to_mods_slim_batch {
161         my($self, $conn, $id_list) = @_;
162     $conn->respond(_records_to_mods($_)->[0]) for @$id_list;
163     return undef;
164 }
165
166
167 # Returns the number of copies attached to a record based on org location
168 __PACKAGE__->register_method(
169     method   => "record_id_to_copy_count",
170     api_name => "open-ils.search.biblio.record.copy_count",
171     signature => {
172         desc => q/Returns a copy summary for the given record for the context org
173             unit and all ancestor org units/,
174         params => [
175             {desc => 'Context org unit id', type => 'number'},
176             {desc => 'Record ID', type => 'number'}
177         ],
178         return => {
179             desc => q/summary object per org unit in the set, where the set
180                 includes the context org unit and all parent org units.  
181                 Object includes the keys "transcendant", "count", "org_unit", "depth", 
182                 "unshadow", "available".  Each is a count, except "org_unit" which is 
183                 the context org unit and "depth" which is the depth of the context org unit
184             /,
185             type => 'array'
186         }
187     }
188 );
189
190 __PACKAGE__->register_method(
191     method        => "record_id_to_copy_count",
192     api_name      => "open-ils.search.biblio.record.copy_count.staff",
193     authoritative => 1,
194     signature => {
195         desc => q/Returns a copy summary for the given record for the context org
196             unit and all ancestor org units/,
197         params => [
198             {desc => 'Context org unit id', type => 'number'},
199             {desc => 'Record ID', type => 'number'}
200         ],
201         return => {
202             desc => q/summary object per org unit in the set, where the set
203                 includes the context org unit and all parent org units.  
204                 Object includes the keys "transcendant", "count", "org_unit", "depth", 
205                 "unshadow", "available".  Each is a count, except "org_unit" which is 
206                 the context org unit and "depth" which is the depth of the context org unit
207             /,
208             type => 'array'
209         }
210     }
211 );
212
213 __PACKAGE__->register_method(
214     method   => "record_id_to_copy_count",
215     api_name => "open-ils.search.biblio.metarecord.copy_count",
216     signature => {
217         desc => q/Returns a copy summary for the given record for the context org
218             unit and all ancestor org units/,
219         params => [
220             {desc => 'Context org unit id', type => 'number'},
221             {desc => 'Record ID', type => 'number'}
222         ],
223         return => {
224             desc => q/summary object per org unit in the set, where the set
225                 includes the context org unit and all parent org units.  
226                 Object includes the keys "transcendant", "count", "org_unit", "depth", 
227                 "unshadow", "available".  Each is a count, except "org_unit" which is 
228                 the context org unit and "depth" which is the depth of the context org unit
229             /,
230             type => 'array'
231         }
232     }
233 );
234
235 __PACKAGE__->register_method(
236     method   => "record_id_to_copy_count",
237     api_name => "open-ils.search.biblio.metarecord.copy_count.staff",
238     signature => {
239         desc => q/Returns a copy summary for the given record for the context org
240             unit and all ancestor org units/,
241         params => [
242             {desc => 'Context org unit id', type => 'number'},
243             {desc => 'Record ID', type => 'number'}
244         ],
245         return => {
246             desc => q/summary object per org unit in the set, where the set
247                 includes the context org unit and all parent org units.  
248                 Object includes the keys "transcendant", "count", "org_unit", "depth", 
249                 "unshadow", "available".  Each is a count, except "org_unit" which is 
250                 the context org unit and "depth" which is the depth of the context org
251                 unit.  "depth" is always -1 when the count from a lasso search is
252                 performed, since depth doesn't mean anything in a lasso context.
253             /,
254             type => 'array'
255         }
256     }
257 );
258
259 sub record_id_to_copy_count {
260     my( $self, $client, $org_id, $record_id ) = @_;
261
262     return [] unless $record_id;
263
264     my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
265     my $staff = $self->api_name =~ /staff/ ? 't' : 'f';
266
267     my $data = $U->cstorereq(
268         "open-ils.cstore.json_query.atomic",
269         { from => ['asset.' . $key  . '_copy_count' => $org_id => $record_id => $staff] }
270     );
271
272     my @count;
273     for my $d ( @$data ) { # fix up the key name change required by stored-proc version
274         $$d{count} = delete $$d{visible};
275         push @count, $d;
276     }
277
278     return [ sort { $a->{depth} <=> $b->{depth} } @count ];
279 }
280
281 __PACKAGE__->register_method(
282     method   => "record_has_holdable_copy",
283     api_name => "open-ils.search.biblio.record.has_holdable_copy",
284     signature => {
285         desc => q/Returns a boolean indicating if a record has any holdable copies./,
286         params => [
287             {desc => 'Record ID', type => 'number'}
288         ],
289         return => {
290             desc => q/bool indicating if the record has any holdable copies/,
291             type => 'bool'
292         }
293     }
294 );
295
296 __PACKAGE__->register_method(
297     method   => "record_has_holdable_copy",
298     api_name => "open-ils.search.biblio.metarecord.has_holdable_copy",
299     signature => {
300         desc => q/Returns a boolean indicating if a record has any holdable copies./,
301         params => [
302             {desc => 'Record ID', type => 'number'}
303         ],
304         return => {
305             desc => q/bool indicating if the record has any holdable copies/,
306             type => 'bool'
307         }
308     }
309 );
310
311 sub record_has_holdable_copy {
312     my($self, $client, $record_id ) = @_;
313
314     return 0 unless $record_id;
315
316     my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
317
318     my $data = $U->cstorereq(
319         "open-ils.cstore.json_query.atomic",
320         { from => ['asset.' . $key . '_has_holdable_copy' => $record_id ] }
321     );
322
323     return ${@$data[0]}{'asset.' . $key . '_has_holdable_copy'} eq 't';
324
325 }
326
327 __PACKAGE__->register_method(
328     method   => "biblio_search_tcn",
329     api_name => "open-ils.search.biblio.tcn",
330     argc     => 1,
331     signature => {
332         desc   => "Retrieve related record ID(s) given a TCN",
333         params => [
334             { desc => 'TCN', type => 'string' },
335             { desc => 'Flag indicating to include deleted records', type => 'string' }
336         ],
337         return => {
338             desc => 'Results object like: { "count": $i, "ids": [...] }',
339             type => 'object'
340         }
341     }
342
343 );
344
345 sub biblio_search_tcn {
346
347     my( $self, $client, $tcn, $include_deleted ) = @_;
348
349     $tcn =~ s/^\s+|\s+$//og;
350
351     my $e = new_editor();
352     my $search = {tcn_value => $tcn};
353     $search->{deleted} = 'f' unless $include_deleted;
354     my $recs = $e->search_biblio_record_entry( $search, {idlist =>1} );
355         
356     return { count => scalar(@$recs), ids => $recs };
357 }
358
359
360 # --------------------------------------------------------------------------------
361
362 __PACKAGE__->register_method(
363     method   => "biblio_barcode_to_copy",
364     api_name => "open-ils.search.asset.copy.find_by_barcode",
365 );
366 sub biblio_barcode_to_copy { 
367         my( $self, $client, $barcode ) = @_;
368         my( $copy, $evt ) = $U->fetch_copy_by_barcode($barcode);
369         return $evt if $evt;
370         return $copy;
371 }
372
373 __PACKAGE__->register_method(
374     method   => "biblio_id_to_copy",
375     api_name => "open-ils.search.asset.copy.batch.retrieve",
376 );
377 sub biblio_id_to_copy { 
378         my( $self, $client, $ids ) = @_;
379         $logger->info("Fetching copies @$ids");
380         return $U->cstorereq(
381                 "open-ils.cstore.direct.asset.copy.search.atomic", { id => $ids } );
382 }
383
384
385 __PACKAGE__->register_method(
386         method  => "biblio_id_to_uris",
387         api_name=> "open-ils.search.asset.uri.retrieve_by_bib",
388         argc    => 2, 
389     stream  => 1,
390     signature => q#
391         @param BibID Which bib record contains the URIs
392         @param OrgID Where to look for URIs
393         @param OrgDepth Range adjustment for OrgID
394         @return A stream or list of 'auri' objects
395     #
396
397 );
398 sub biblio_id_to_uris { 
399         my( $self, $client, $bib, $org, $depth ) = @_;
400     die "Org ID required" unless defined($org);
401     die "Bib ID required" unless defined($bib);
402
403     my @params;
404     push @params, $depth if (defined $depth);
405
406         my $ids = $U->cstorereq( "open-ils.cstore.json_query.atomic",
407         {   select  => { auri => [ 'id' ] },
408             from    => {
409                 acn => {
410                     auricnm => {
411                         field   => 'call_number',
412                         fkey    => 'id',
413                         join    => {
414                             auri    => {
415                                 field => 'id',
416                                 fkey => 'uri',
417                                 filter  => { active => 't' }
418                             }
419                         }
420                     }
421                 }
422             },
423             where   => {
424                 '+acn'  => {
425                     record      => $bib,
426                     owning_lib  => {
427                         in  => {
428                             select  => { aou => [ { column => 'id', transform => 'actor.org_unit_descendants', params => \@params, result_field => 'id' } ] },
429                             from    => 'aou',
430                             where   => { id => $org },
431                             distinct=> 1
432                         }
433                     }
434                 }
435             },
436             distinct=> 1,
437         }
438     );
439
440         my $uris = $U->cstorereq(
441                 "open-ils.cstore.direct.asset.uri.search.atomic",
442         { id => [ map { (values %$_) } @$ids ] }
443     );
444
445     $client->respond($_) for (@$uris);
446
447     return undef;
448 }
449
450
451 __PACKAGE__->register_method(
452     method    => "copy_retrieve",
453     api_name  => "open-ils.search.asset.copy.retrieve",
454     argc      => 1,
455     signature => {
456         desc   => 'Retrieve a copy object based on the Copy ID',
457         params => [
458             { desc => 'Copy ID', type => 'number'}
459         ],
460         return => {
461             desc => 'Copy object, event on error'
462         }
463     }
464 );
465
466 sub copy_retrieve {
467         my( $self, $client, $cid ) = @_;
468         my( $copy, $evt ) = $U->fetch_copy($cid);
469         return $evt || $copy;
470 }
471
472 __PACKAGE__->register_method(
473     method   => "volume_retrieve",
474     api_name => "open-ils.search.asset.call_number.retrieve"
475 );
476 sub volume_retrieve {
477         my( $self, $client, $vid ) = @_;
478         my $e = new_editor();
479         my $vol = $e->retrieve_asset_call_number($vid) or return $e->event;
480         return $vol;
481 }
482
483 __PACKAGE__->register_method(
484     method        => "fleshed_copy_retrieve_batch",
485     api_name      => "open-ils.search.asset.copy.fleshed.batch.retrieve",
486     authoritative => 1,
487 );
488
489 sub fleshed_copy_retrieve_batch { 
490         my( $self, $client, $ids ) = @_;
491         $logger->info("Fetching fleshed copies @$ids");
492         return $U->cstorereq(
493                 "open-ils.cstore.direct.asset.copy.search.atomic",
494                 { id => $ids },
495                 { flesh => 1, 
496                   flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries parts / ] }
497                 });
498 }
499
500
501 __PACKAGE__->register_method(
502     method   => "fleshed_copy_retrieve",
503     api_name => "open-ils.search.asset.copy.fleshed.retrieve",
504 );
505
506 sub fleshed_copy_retrieve { 
507         my( $self, $client, $id ) = @_;
508         my( $c, $e) = $U->fetch_fleshed_copy($id);
509         return $e || $c;
510 }
511
512
513 __PACKAGE__->register_method(
514     method        => 'fleshed_by_barcode',
515     api_name      => "open-ils.search.asset.copy.fleshed2.find_by_barcode",
516     authoritative => 1,
517 );
518 sub fleshed_by_barcode {
519         my( $self, $conn, $barcode ) = @_;
520         my $e = new_editor();
521         my $copyid = $e->search_asset_copy(
522                 {barcode => $barcode, deleted => 'f'}, {idlist=>1})->[0]
523                 or return $e->event;
524         return fleshed_copy_retrieve2( $self, $conn, $copyid);
525 }
526
527
528 __PACKAGE__->register_method(
529     method        => "fleshed_copy_retrieve2",
530     api_name      => "open-ils.search.asset.copy.fleshed2.retrieve",
531     authoritative => 1,
532 );
533
534 sub fleshed_copy_retrieve2 { 
535         my( $self, $client, $id ) = @_;
536         my $e = new_editor();
537         my $copy = $e->retrieve_asset_copy(
538                 [
539                         $id,
540             {
541                 flesh        => 2,
542                 flesh_fields => {
543                     acp => [
544                         qw/ location status stat_cat_entry_copy_maps notes age_protect parts peer_record_maps /
545                     ],
546                     ascecm => [qw/ stat_cat stat_cat_entry /],
547                 }
548             }
549                 ]
550         ) or return $e->event;
551
552         # For backwards compatibility
553         #$copy->stat_cat_entries($copy->stat_cat_entry_copy_maps);
554
555         if( $copy->status->id == OILS_COPY_STATUS_CHECKED_OUT ) {
556                 $copy->circulations(
557                         $e->search_action_circulation( 
558                                 [       
559                                         { target_copy => $copy->id },
560                                         {
561                                                 order_by => { circ => 'xact_start desc' },
562                                                 limit => 1
563                                         }
564                                 ]
565                         )
566                 );
567         }
568
569         return $copy;
570 }
571
572
573 __PACKAGE__->register_method(
574     method        => 'flesh_copy_custom',
575     api_name      => 'open-ils.search.asset.copy.fleshed.custom',
576     authoritative => 1,
577 );
578
579 sub flesh_copy_custom {
580         my( $self, $conn, $copyid, $fields ) = @_;
581         my $e = new_editor();
582         my $copy = $e->retrieve_asset_copy(
583                 [
584                         $copyid,
585                         { 
586                                 flesh                           => 1,
587                                 flesh_fields    => { 
588                                         acp => $fields,
589                                 }
590                         }
591                 ]
592         ) or return $e->event;
593         return $copy;
594 }
595
596
597 __PACKAGE__->register_method(
598     method   => "biblio_barcode_to_title",
599     api_name => "open-ils.search.biblio.find_by_barcode",
600 );
601
602 sub biblio_barcode_to_title {
603         my( $self, $client, $barcode ) = @_;
604
605         my $title = $apputils->simple_scalar_request(
606                 "open-ils.storage",
607                 "open-ils.storage.biblio.record_entry.retrieve_by_barcode", $barcode );
608
609         return { ids => [ $title->id ], count => 1 } if $title;
610         return { count => 0 };
611 }
612
613 __PACKAGE__->register_method(
614     method        => 'title_id_by_item_barcode',
615     api_name      => 'open-ils.search.bib_id.by_barcode',
616     authoritative => 1,
617     signature => { 
618         desc   => 'Retrieve bib record id associated with the copy identified by the given barcode',
619         params => [
620             { desc => 'Item barcode', type => 'string' }
621         ],
622         return => {
623             desc => 'Bib record id.'
624         }
625     }
626 );
627
628 __PACKAGE__->register_method(
629     method        => 'title_id_by_item_barcode',
630     api_name      => 'open-ils.search.multi_home.bib_ids.by_barcode',
631     authoritative => 1,
632     signature => {
633         desc   => 'Retrieve bib record ids associated with the copy identified by the given barcode.  This includes peer bibs for Multi-Home items.',
634         params => [
635             { desc => 'Item barcode', type => 'string' }
636         ],
637         return => {
638             desc => 'Array of bib record ids.  First element is the native bib for the item.'
639         }
640     }
641 );
642
643
644 sub title_id_by_item_barcode {
645     my( $self, $conn, $barcode ) = @_;
646     my $e = new_editor();
647     my $copies = $e->search_asset_copy(
648         [
649             { deleted => 'f', barcode => $barcode },
650             {
651                 flesh => 2,
652                 flesh_fields => {
653                     acp => [ 'call_number' ],
654                     acn => [ 'record' ]
655                 }
656             }
657         ]
658     );
659
660     return $e->event unless @$copies;
661
662     if( $self->api_name =~ /multi_home/ ) {
663         my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
664             [
665                 { target_copy => $$copies[0]->id }
666             ]
667         );
668         my @temp =  map { $_->peer_record } @{ $multi_home_list };
669         unshift @temp, $$copies[0]->call_number->record->id;
670         return \@temp;
671     } else {
672         return $$copies[0]->call_number->record->id;
673     }
674 }
675
676 __PACKAGE__->register_method(
677     method        => 'find_peer_bibs',
678     api_name      => 'open-ils.search.peer_bibs.test',
679     authoritative => 1,
680     signature => {
681         desc   => 'Tests to see if the specified record is a peer record.',
682         params => [
683             { desc => 'Biblio record entry Id', type => 'number' }
684         ],
685         return => {
686             desc => 'True if specified id can be found in biblio.peer_bib_copy_map.peer_record.',
687             type => 'bool'
688         }
689     }
690 );
691
692 __PACKAGE__->register_method(
693     method        => 'find_peer_bibs',
694     api_name      => 'open-ils.search.peer_bibs',
695     authoritative => 1,
696     signature => {
697         desc   => 'Return acps and mvrs for multi-home items linked to specified peer record.',
698         params => [
699             { desc => 'Biblio record entry Id', type => 'number' }
700         ],
701         return => {
702             desc => '{ records => Array of mvrs, items => array of acps }',
703         }
704     }
705 );
706
707
708 sub find_peer_bibs {
709         my( $self, $client, $doc_id ) = @_;
710     my $e = new_editor();
711
712     my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
713         [
714             { peer_record => $doc_id },
715             {
716                 flesh => 2,
717                 flesh_fields => {
718                     bpbcm => [ 'target_copy', 'peer_type' ],
719                     acp => [ 'call_number', 'location', 'status', 'peer_record_maps' ]
720                 }
721             }
722         ]
723     );
724
725     if ($self->api_name =~ /test/) {
726         return scalar( @{$multi_home_list} ) > 0 ? 1 : 0;
727     }
728
729     if (scalar(@{$multi_home_list})==0) {
730         return [];
731     }
732
733     # create a unique hash of the primary record MVRs for foreign copies
734     # XXX PLEASE let's change to unAPI2 (supports foreign copies) in the TT opac?!?
735     my %rec_hash = map {
736         ($_->target_copy->call_number->record, _records_to_mods( $_->target_copy->call_number->record )->[0])
737     } @$multi_home_list;
738
739     # set the foreign_copy_maps field to an empty array
740     map { $rec_hash{$_}->foreign_copy_maps([]) } keys( %rec_hash );
741
742     # push the maps onto the correct MVRs
743     for (@$multi_home_list) {
744         push(
745             @{$rec_hash{ $_->target_copy->call_number->record }->foreign_copy_maps()},
746             $_
747         );
748     }
749
750     return [sort {$a->title cmp $b->title} values(%rec_hash)];
751 };
752
753 __PACKAGE__->register_method(
754     method   => "biblio_copy_to_mods",
755     api_name => "open-ils.search.biblio.copy.mods.retrieve",
756 );
757
758 # takes a copy object and returns it fleshed mods object
759 sub biblio_copy_to_mods {
760         my( $self, $client, $copy ) = @_;
761
762         my $volume = $U->cstorereq( 
763                 "open-ils.cstore.direct.asset.call_number.retrieve",
764                 $copy->call_number() );
765
766         my $mods = _records_to_mods($volume->record());
767         $mods = shift @$mods;
768         $volume->copies([$copy]);
769         push @{$mods->call_numbers()}, $volume;
770
771         return $mods;
772 }
773
774
775 =head1 NAME
776
777 OpenILS::Application::Search::Biblio
778
779 =head1 DESCRIPTION
780
781 =head2 API METHODS
782
783 =head3 open-ils.search.biblio.multiclass.query (arghash, query, docache)
784
785 For arghash and docache, see B<open-ils.search.biblio.multiclass>.
786
787 The query argument is a string, but built like a hash with key: value pairs.
788 Recognized search keys include: 
789
790  keyword (kw) - search keyword(s) *
791  author  (au) - search author(s)  *
792  name    (au) - same as author    *
793  title   (ti) - search title      *
794  subject (su) - search subject    *
795  series  (se) - search series     *
796  lang - limit by language (specify multiple langs with lang:l1 lang:l2 ...)
797  site - search at specified org unit, corresponds to actor.org_unit.shortname
798  pref_ou - extend search to specified org unit, corresponds to actor.org_unit.shortname
799  sort - sort type (title, author, pubdate)
800  dir  - sort direction (asc, desc)
801  available - if set to anything other than "false" or "0", limits to available items
802
803 * Searching keyword, author, title, subject, and series supports additional search 
804 subclasses, specified with a "|".  For example, C<title|proper:gone with the wind>.
805
806 For more, see B<config.metabib_field>.
807
808 =cut
809
810 foreach (qw/open-ils.search.biblio.multiclass.query
811             open-ils.search.biblio.multiclass.query.staff
812             open-ils.search.metabib.multiclass.query
813             open-ils.search.metabib.multiclass.query.staff/)
814 {
815 __PACKAGE__->register_method(
816     api_name  => $_,
817     method    => 'multiclass_query',
818     signature => {
819         desc   => 'Perform a search query.  The .staff version of the call includes otherwise hidden hits.',
820         params => [
821             {name => 'arghash', desc => 'Arg hash (see open-ils.search.biblio.multiclass)',         type => 'object'},
822             {name => 'query',   desc => 'Raw human-readable query (see perldoc '. __PACKAGE__ .')', type => 'string'},
823             {name => 'docache', desc => 'Flag for caching (see open-ils.search.biblio.multiclass)', type => 'object'},
824         ],
825         return => {
826             desc => 'Search results from query, like: { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
827             type => 'object',       # TODO: update as miker's new elements are included
828         }
829     }
830 );
831 }
832
833 sub multiclass_query {
834     my($self, $conn, $arghash, $query, $docache) = @_;
835
836     $logger->debug("initial search query => $query");
837     my $orig_query = $query;
838
839     $query =~ s/\+/ /go;
840     $query =~ s/^\s+//go;
841
842     # convert convenience classes (e.g. kw for keyword) to the full class name
843     # ensure that the convenience class isn't part of a word (e.g. 'playhouse')
844     $query =~ s/(^|\s)kw(:|\|)/$1keyword$2/go;
845     $query =~ s/(^|\s)ti(:|\|)/$1title$2/go;
846     $query =~ s/(^|\s)au(:|\|)/$1author$2/go;
847     $query =~ s/(^|\s)su(:|\|)/$1subject$2/go;
848     $query =~ s/(^|\s)se(:|\|)/$1series$2/go;
849     $query =~ s/(^|\s)name(:|\|)/$1author$2/og;
850
851     $logger->debug("cleansed query string => $query");
852     my $search = {};
853
854     my $simple_class_re  = qr/((?:\w+(?:\|\w+)?):[^:]+?)$/;
855     my $class_list_re    = qr/(?:keyword|title|author|subject|series)/;
856     my $modifier_list_re = qr/(?:site|dir|sort|lang|available|preflib)/;
857
858     my $tmp_value = '';
859     while ($query =~ s/$simple_class_re//so) {
860
861         my $qpart = $1;
862         my $where = index($qpart,':');
863         my $type  = substr($qpart, 0, $where++);
864         my $value = substr($qpart, $where);
865
866         if ($type !~ /^(?:$class_list_re|$modifier_list_re)/o) {
867             $tmp_value = "$qpart $tmp_value";
868             next;
869         }
870
871         if ($type =~ /$class_list_re/o ) {
872             $value .= $tmp_value;
873             $tmp_value = '';
874         }
875
876         next unless $type and $value;
877
878         $value =~ s/^\s*//og;
879         $value =~ s/\s*$//og;
880         $type = 'sort_dir' if $type eq 'dir';
881
882         if($type eq 'site') {
883             # 'site' is the org shortname.  when using this, we also want 
884             # to search at the requested org's depth
885             my $e = new_editor();
886             if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
887                 $arghash->{org_unit} = $org->id if $org;
888                 $arghash->{depth} = $e->retrieve_actor_org_unit_type($org->ou_type)->depth;
889             } else {
890                 $logger->warn("'site:' query used on invalid org shortname: $value ... ignoring");
891             }
892         } elsif($type eq 'pref_ou') {
893             # 'pref_ou' is the preferred org shortname.
894             my $e = new_editor();
895             if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
896                 $arghash->{pref_ou} = $org->id if $org;
897             } else {
898                 $logger->warn("'pref_ou:' query used on invalid org shortname: $value ... ignoring");
899             }
900
901         } elsif($type eq 'available') {
902             # limit to available
903             $arghash->{available} = 1 unless $value eq 'false' or $value eq '0';
904
905         } elsif($type eq 'lang') {
906             # collect languages into an array of languages
907             $arghash->{language} = [] unless $arghash->{language};
908             push(@{$arghash->{language}}, $value);
909
910         } elsif($type =~ /^sort/o) {
911             # sort and sort_dir modifiers
912             $arghash->{$type} = $value;
913
914         } else {
915             # append the search term to the term under construction
916             $search->{$type} =  {} unless $search->{$type};
917             $search->{$type}->{term} =  
918                 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $value" : $value;
919         }
920     }
921
922     $query .= " $tmp_value";
923     $query =~ s/\s+/ /go;
924     $query =~ s/^\s+//go;
925     $query =~ s/\s+$//go;
926
927     my $type = $arghash->{default_class} || 'keyword';
928     $type = ($type eq '-') ? 'keyword' : $type;
929     $type = ($type !~ /^(title|author|keyword|subject|series)(?:\|\w+)?$/o) ? 'keyword' : $type;
930
931     if($query) {
932         # This is the front part of the string before any special tokens were
933         # parsed OR colon-separated strings that do not denote a class.
934         # Add this data to the default search class
935         $search->{$type} =  {} unless $search->{$type};
936         $search->{$type}->{term} =
937             ($search->{$type}->{term}) ? $search->{$type}->{term} . " $query" : $query;
938     }
939     my $real_search = $arghash->{searches} = { $type => { term => $orig_query } };
940
941     # capture the original limit because the search method alters the limit internally
942     my $ol = $arghash->{limit};
943
944         my $sclient = OpenSRF::Utils::SettingsClient->new;
945
946     (my $method = $self->api_name) =~ s/\.query//o;
947
948     $method =~ s/multiclass/multiclass.staged/
949         if $sclient->config_value(apps => 'open-ils.search',
950             app_settings => 'use_staged_search') =~ /true/i;
951
952     # XXX This stops the session locale from doing the right thing.
953     # XXX Revisit this and have it translate to a lang instead of a locale.
954     #$arghash->{preferred_language} = $U->get_org_locale($arghash->{org_unit})
955     #    unless $arghash->{preferred_language};
956
957         $method = $self->method_lookup($method);
958     my ($data) = $method->run($arghash, $docache);
959
960     $arghash->{searches} = $search if (!$data->{complex_query});
961
962     $arghash->{limit} = $ol if $ol;
963     $data->{compiled_search} = $arghash;
964     $data->{query} = $orig_query;
965
966     $logger->info("compiled search is " . OpenSRF::Utils::JSON->perl2JSON($arghash));
967
968     return $data;
969 }
970
971 __PACKAGE__->register_method(
972     method    => 'cat_search_z_style_wrapper',
973     api_name  => 'open-ils.search.biblio.zstyle',
974     stream    => 1,
975     signature => q/@see open-ils.search.biblio.multiclass/
976 );
977
978 __PACKAGE__->register_method(
979     method    => 'cat_search_z_style_wrapper',
980     api_name  => 'open-ils.search.biblio.zstyle.staff',
981     stream    => 1,
982     signature => q/@see open-ils.search.biblio.multiclass/
983 );
984
985 sub cat_search_z_style_wrapper {
986         my $self = shift;
987         my $client = shift;
988         my $authtoken = shift;
989         my $args = shift;
990
991         my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
992
993         my $ou = $cstore->request(
994                 'open-ils.cstore.direct.actor.org_unit.search',
995                 { parent_ou => undef }
996         )->gather(1);
997
998         my $result = { service => 'native-evergreen-catalog', records => [] };
999         my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
1000
1001         $$searchhash{searches}{title}{term}   = $$args{search}{title}   if $$args{search}{title};
1002         $$searchhash{searches}{author}{term}  = $$args{search}{author}  if $$args{search}{author};
1003         $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
1004         $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
1005         $$searchhash{searches}{'identifier|isbn'}{term} = $$args{search}{isbn} if $$args{search}{isbn};
1006         $$searchhash{searches}{'identifier|issn'}{term} = $$args{search}{issn} if $$args{search}{issn};
1007
1008         $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn}       if $$args{search}{tcn};
1009         $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
1010         $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate}   if $$args{search}{pubdate};
1011         $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
1012
1013         my $list = the_quest_for_knowledge( $self, $client, $searchhash );
1014
1015         if ($list->{count} > 0 and @{$list->{ids}}) {
1016                 $result->{count} = $list->{count};
1017
1018                 my $records = $cstore->request(
1019                         'open-ils.cstore.direct.biblio.record_entry.search.atomic',
1020                         { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
1021                 )->gather(1);
1022
1023                 for my $rec ( @$records ) {
1024                         
1025                         my $u = OpenILS::Utils::ModsParser->new();
1026                         $u->start_mods_batch( $rec->marc );
1027                         my $mods = $u->finish_mods_batch();
1028
1029                         push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
1030
1031                 }
1032
1033         }
1034
1035     $cstore->disconnect();
1036         return $result;
1037 }
1038
1039 # ----------------------------------------------------------------------------
1040 # These are the main OPAC search methods
1041 # ----------------------------------------------------------------------------
1042
1043 __PACKAGE__->register_method(
1044     method    => 'the_quest_for_knowledge',
1045     api_name  => 'open-ils.search.biblio.multiclass',
1046     signature => {
1047         desc => "Performs a multi class biblio or metabib search",
1048         params => [
1049             {
1050                 desc => "A search hash with keys: "
1051                       . "searches, org_unit, depth, limit, offset, format, sort, sort_dir.  "
1052                       . "See perldoc " . __PACKAGE__ . " for more detail",
1053                 type => 'object',
1054             },
1055             {
1056                 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
1057                 type => 'string',
1058             }
1059         ],
1060         return => {
1061             desc => 'An object of the form: '
1062                   . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
1063         }
1064     }
1065 );
1066
1067 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
1068
1069 The search-hash argument can have the following elements:
1070
1071     searches: { "$class" : "$value", ...}           [REQUIRED]
1072     org_unit: The org id to focus the search at
1073     depth   : The org depth     
1074     limit   : The search limit      default: 10
1075     offset  : The search offset     default:  0
1076     format  : The MARC format
1077     sort    : What field to sort the results on? [ author | title | pubdate ]
1078     sort_dir: What direction do we sort? [ asc | desc ]
1079     tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
1080         will be tagged with an additional value ("1") as the last value in the record ID array for
1081         each record.  Requires the 'authtoken'
1082     authtoken : Authentication token string;  When actions are performed that require a user login
1083         (e.g. tagging circulated records), the authentication token is required
1084
1085 The searches element is required, must have a hashref value, and the hashref must contain at least one 
1086 of the following classes as a key:
1087
1088     title
1089     author
1090     subject
1091     series
1092     keyword
1093
1094 The value paired with a key is the associated search string.
1095
1096 The docache argument enables/disables searching and saving results in cache (default OFF).
1097
1098 The return object, if successful, will look like:
1099
1100     { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
1101
1102 =cut
1103
1104 __PACKAGE__->register_method(
1105     method    => 'the_quest_for_knowledge',
1106     api_name  => 'open-ils.search.biblio.multiclass.staff',
1107     signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items.  Otherwise, @see open-ils.search.biblio.multiclass/
1108 );
1109 __PACKAGE__->register_method(
1110     method    => 'the_quest_for_knowledge',
1111     api_name  => 'open-ils.search.metabib.multiclass',
1112     signature => q/@see open-ils.search.biblio.multiclass/
1113 );
1114 __PACKAGE__->register_method(
1115     method    => 'the_quest_for_knowledge',
1116     api_name  => 'open-ils.search.metabib.multiclass.staff',
1117     signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items.  Otherwise, @see open-ils.search.biblio.multiclass/
1118 );
1119
1120 sub the_quest_for_knowledge {
1121         my( $self, $conn, $searchhash, $docache ) = @_;
1122
1123         return { count => 0 } unless $searchhash and
1124                 ref $searchhash->{searches} eq 'HASH';
1125
1126         my $method = 'open-ils.storage.biblio.multiclass.search_fts';
1127         my $ismeta = 0;
1128         my @recs;
1129
1130         if($self->api_name =~ /metabib/) {
1131                 $ismeta = 1;
1132                 $method =~ s/biblio/metabib/o;
1133         }
1134
1135         # do some simple sanity checking
1136         if(!$searchhash->{searches} or
1137                 ( !grep { /^(?:title|author|subject|series|keyword|identifier\|is[bs]n)/ } keys %{$searchhash->{searches}} ) ) {
1138                 return { count => 0 };
1139         }
1140
1141     my $offset = $searchhash->{offset} ||  0;   # user value or default in local var now
1142     my $limit  = $searchhash->{limit}  || 10;   # user value or default in local var now
1143     my $end    = $offset + $limit - 1;
1144
1145         my $maxlimit = 5000;
1146     $searchhash->{offset} = 0;                  # possible user value overwritten in hash
1147     $searchhash->{limit}  = $maxlimit;          # possible user value overwritten in hash
1148
1149         return { count => 0 } if $offset > $maxlimit;
1150
1151         my @search;
1152         push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
1153         my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
1154         my $ckey = $pfx . md5_hex($method . $s);
1155
1156         $logger->info("bib search for: $s");
1157
1158         $searchhash->{limit} -= $offset;
1159
1160
1161     my $trim = 0;
1162         my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1163
1164         if(!$result) {
1165
1166                 $method .= ".staff" if($self->api_name =~ /staff/);
1167                 $method .= ".atomic";
1168         
1169                 for (keys %$searchhash) { 
1170                         delete $$searchhash{$_} 
1171                                 unless defined $$searchhash{$_}; 
1172                 }
1173         
1174                 $result = $U->storagereq( $method, %$searchhash );
1175         $trim = 1;
1176
1177         } else { 
1178                 $docache = 0;   # results came FROM cache, so we don't write back
1179         }
1180
1181         return {count => 0} unless ($result && $$result[0]);
1182
1183         @recs = @$result;
1184
1185         my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1186
1187         if($docache) {
1188                 # If we didn't get this data from the cache, put it into the cache
1189                 # then return the correct offset of records
1190                 $logger->debug("putting search cache $ckey\n");
1191                 put_cache($ckey, $count, \@recs);
1192         }
1193
1194     if($trim) {
1195         # if we have the full set of data, trim out 
1196         # the requested chunk based on limit and offset
1197         my @t;
1198         for ($offset..$end) {
1199             last unless $recs[$_];
1200             push(@t, $recs[$_]);
1201         }
1202         @recs = @t;
1203     }
1204
1205         return { ids => \@recs, count => $count };
1206 }
1207
1208
1209 __PACKAGE__->register_method(
1210     method    => 'staged_search',
1211     api_name  => 'open-ils.search.biblio.multiclass.staged',
1212     signature => {
1213         desc   => 'Staged search filters out unavailable items.  This means that it relies on an estimation strategy for determining ' .
1214                   'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering.  See "estimation_strategy" in your SRF config.',
1215         params => [
1216             {
1217                 desc => "A search hash with keys: "
1218                       . "searches, limit, offset.  The others are optional, but the 'searches' key/value pair is required, with the value being a hashref.  "
1219                       . "See perldoc " . __PACKAGE__ . " for more detail",
1220                 type => 'object',
1221             },
1222             {
1223                 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1224                 type => 'string',
1225             }
1226         ],
1227         return => {
1228             desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids.  '
1229                   . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1230             type => 'object',
1231         }
1232     }
1233 );
1234 __PACKAGE__->register_method(
1235     method    => 'staged_search',
1236     api_name  => 'open-ils.search.biblio.multiclass.staged.staff',
1237     signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items.  Otherwise, @see open-ils.search.biblio.multiclass.staged/
1238 );
1239 __PACKAGE__->register_method(
1240     method    => 'staged_search',
1241     api_name  => 'open-ils.search.metabib.multiclass.staged',
1242     signature => q/@see open-ils.search.biblio.multiclass.staged/
1243 );
1244 __PACKAGE__->register_method(
1245     method    => 'staged_search',
1246     api_name  => 'open-ils.search.metabib.multiclass.staged.staff',
1247     signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items.  Otherwise, @see open-ils.search.biblio.multiclass.staged/
1248 );
1249
1250 sub staged_search {
1251         my($self, $conn, $search_hash, $docache) = @_;
1252
1253     my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1254
1255     my $method = $IAmMetabib?
1256         'open-ils.storage.metabib.multiclass.staged.search_fts':
1257         'open-ils.storage.biblio.multiclass.staged.search_fts';
1258
1259     $method .= '.staff' if $self->api_name =~ /staff$/;
1260     $method .= '.atomic';
1261                 
1262     return {count => 0} unless (
1263         $search_hash and 
1264         $search_hash->{searches} and 
1265         scalar( keys %{$search_hash->{searches}} ));
1266
1267     my $search_duration;
1268     my $user_offset = $search_hash->{offset} ||  0; # user-specified offset
1269     my $user_limit  = $search_hash->{limit}  || 10;
1270     my $ignore_facet_classes  = $search_hash->{ignore_facet_classes};
1271     $user_offset = ($user_offset >= 0) ? $user_offset :  0;
1272     $user_limit  = ($user_limit  >= 0) ? $user_limit  : 10;
1273
1274     # restrict DB query to our max results
1275     $search_hash->{core_limit}  = $max_search_results;
1276
1277     # pull any existing results from the cache
1278     my $key = search_cache_key($method, $search_hash);
1279     my $facet_key = $key.'_facets';
1280     my $cache_data = $cache->get_cache($key) || {};
1281
1282     # keep retrieving results until we find enough to 
1283     # fulfill the user-specified limit and offset
1284     my $all_results = [];
1285
1286     my $results;
1287     my $summary;
1288     my $new_ids = [];
1289
1290     if($cache_data->{summary}) {
1291         # this window of results is already cached
1292         $logger->debug("staged search: found cached results");
1293         $summary = $cache_data->{summary};
1294         $results = $cache_data->{results};
1295
1296     } else {
1297         # retrieve the window of results from the database
1298         $logger->debug("staged search: fetching results from the database");
1299         my $start = time;
1300         $results = $U->storagereq($method, %$search_hash);
1301         $search_duration = time - $start;
1302         $summary = shift(@$results) if $results;
1303
1304         unless($summary) {
1305             $logger->info("search timed out: duration=$search_duration: params=".
1306                 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1307             return {count => 0};
1308         }
1309
1310         $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1311
1312         my $hc = $summary->{visible};
1313         if($hc == 0) {
1314             $logger->info("search returned 0 results: duration=$search_duration: params=".
1315                 OpenSRF::Utils::JSON->perl2JSON($search_hash));
1316         }
1317
1318         # Create backwards-compatible result structures
1319         if($IAmMetabib) {
1320             $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1321         } else {
1322             $results = [map {[$_->{id}]} @$results];
1323         }
1324
1325         push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1326         $results = [grep {defined $_->[0]} @$results];
1327         cache_staged_search($key, $summary, $results) if $docache;
1328     }
1329
1330     tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib) 
1331         if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1332
1333     # add the new set of results to the set under construction
1334     push(@$all_results, @$results);
1335
1336     my $current_count = scalar(@$all_results);
1337
1338     $logger->debug("staged search: located $current_count, visible=".$summary->{visible});
1339
1340     my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1341
1342     $conn->respond_complete(
1343         {
1344             count             => $summary->{visible},
1345             core_limit        => $search_hash->{core_limit},
1346             facet_key         => $facet_key,
1347             ids               => \@results
1348         }
1349     );
1350
1351     cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1352
1353     return undef;
1354 }
1355
1356 sub tag_circulated_records {
1357     my ($auth, $results, $metabib) = @_;
1358     my $e = new_editor(authtoken => $auth);
1359     return $results unless $e->checkauth;
1360
1361     my $query = {
1362         select   => { acn => [{ column => 'record', alias => 'tagme' }] }, 
1363         from     => { acp => 'acn' }, 
1364         where    => { id => { in => { from => ['action.usr_visible_circ_copies', $e->requestor->id] } } },
1365         distinct => 1
1366     };
1367
1368     if ($metabib) {
1369         $query = {
1370             select   => { mmsm => [{ column => 'metarecord', alias => 'tagme' }] },
1371             from     => 'mmsm',
1372             where    => { source => { in => $query } },
1373             distinct => 1
1374         };
1375     }
1376
1377     # Give me the distinct set of bib records that exist in the user's visible circulation history
1378     my $circ_recs = $e->json_query( $query );
1379
1380     # if the record appears in the circ history, push a 1 onto 
1381     # the rec array structure to indicate truthiness
1382     for my $rec (@$results) {
1383         push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1384     }
1385
1386     $results
1387 }
1388
1389 # creates a unique token to represent the query in the cache
1390 sub search_cache_key {
1391     my $method = shift;
1392     my $search_hash = shift;
1393         my @sorted;
1394     for my $key (sort keys %$search_hash) {
1395             push(@sorted, ($key => $$search_hash{$key})) 
1396             unless $key eq 'limit'  or 
1397                    $key eq 'offset' or 
1398                    $key eq 'skip_check';
1399     }
1400         my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1401         return $pfx . md5_hex($method . $s);
1402 }
1403
1404 sub retrieve_cached_facets {
1405     my $self   = shift;
1406     my $client = shift;
1407     my $key    = shift;
1408     my $limit    = shift;
1409
1410     return undef unless ($key and $key =~ /_facets$/);
1411
1412     my $blob = $cache->get_cache($key) || {};
1413
1414     my $facets = {};
1415     if ($limit) {
1416        for my $f ( keys %$blob ) {
1417             my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1418             @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1419             for my $s ( @sorted ) {
1420                 my ($k) = keys(%$s);
1421                 my ($v) = values(%$s);
1422                 $$facets{$f}{$k} = $v;
1423             }
1424         }
1425     } else {
1426         $facets = $blob;
1427     }
1428
1429     return $facets;
1430 }
1431
1432 __PACKAGE__->register_method(
1433     method   => "retrieve_cached_facets",
1434     api_name => "open-ils.search.facet_cache.retrieve",
1435     signature => {
1436         desc   => 'Returns facet data derived from a specific search based on a key '.
1437                   'generated by open-ils.search.biblio.multiclass.staged and friends.',
1438         params => [
1439             {
1440                 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1441                 type => 'string',
1442             }
1443         ],
1444         return => {
1445             desc => 'Two level hash of facet values.  Top level key is the facet id defined on the config.metabib_field table.  '.
1446                     'Second level key is a string facet value.  Datum attached to each facet value is the number of distinct records, '.
1447                     'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1448                     'facet retrieval.  These counts are calculated for all superpages that have been checked for visibility.',
1449             type => 'object',
1450         }
1451     }
1452 );
1453
1454
1455 sub cache_facets {
1456     # add facets for this search to the facet cache
1457     my($key, $results, $metabib, $ignore) = @_;
1458     my $data = $cache->get_cache($key);
1459     $data ||= {};
1460
1461     return undef unless (@$results);
1462
1463     # The query we're constructing
1464     #
1465     # select  mfae.field as id,
1466     #         mfae.value,
1467     #         count(distinct mmrsm.appropriate-id-field )
1468     #   from  metabib.facet_entry mfae
1469     #         join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1470     #   where mmrsm.appropriate-id-field in IDLIST
1471     #   group by 1,2;
1472
1473     my $count_field = $metabib ? 'metarecord' : 'source';
1474     my $query = {   
1475         select  => {
1476             mfae => [ { column => 'field', alias => 'id'}, 'value' ],
1477             mmrsm => [{
1478                 transform => 'count',
1479                 distinct => 1,
1480                 column => $count_field,
1481                 alias => 'count',
1482                 aggregate => 1
1483             }]
1484         },
1485         from    => {
1486             mfae => {
1487                 mmrsm => { field => 'source', fkey => 'source' },
1488                 cmf   => { field => 'id', fkey => 'field' }
1489             }
1490         },
1491         where   => {
1492             '+mmrsm' => { $count_field => $results },
1493             '+cmf'   => { facet_field => 't' }
1494         }
1495     };
1496
1497     $query->{where}->{'+cmf'}->{field_class} = {'not in' => $ignore}
1498         if ref($ignore) and @$ignore > 0;
1499
1500     my $facets = $U->cstorereq("open-ils.cstore.json_query.atomic", $query);
1501
1502     for my $facet (@$facets) {
1503         next unless ($facet->{value});
1504         $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1505     }
1506
1507     $logger->info("facet compilation: cached with key=$key");
1508
1509     $cache->put_cache($key, $data, $cache_timeout);
1510 }
1511
1512 sub cache_staged_search {
1513     # puts this set of results into the cache
1514     my($key, $summary, $results) = @_;
1515     my $data =  {
1516         summary => $summary,
1517         results => $results
1518     };
1519
1520     $logger->info("staged search: cached with key=$key, visible=".$summary->{visible});
1521
1522     $cache->put_cache($key, $data, $cache_timeout);
1523 }
1524
1525 sub search_cache {
1526
1527         my $key         = shift;
1528         my $offset      = shift;
1529         my $limit       = shift;
1530         my $start       = $offset;
1531         my $end         = $offset + $limit - 1;
1532
1533         $logger->debug("searching cache for $key : $start..$end\n");
1534
1535         return undef unless $cache;
1536         my $data = $cache->get_cache($key);
1537
1538         return undef unless $data;
1539
1540         my $count = $data->[0];
1541         $data = $data->[1];
1542
1543         return undef unless $offset < $count;
1544
1545         my @result;
1546         for( my $i = $offset; $i <= $end; $i++ ) {
1547                 last unless my $d = $$data[$i];
1548                 push( @result, $d );
1549         }
1550
1551         $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1552
1553         return \@result;
1554 }
1555
1556
1557 sub put_cache {
1558         my( $key, $count, $data ) = @_;
1559         return undef unless $cache;
1560         $logger->debug("search_cache putting ".
1561                 scalar(@$data)." items at key $key with timeout $cache_timeout");
1562         $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1563 }
1564
1565
1566 __PACKAGE__->register_method(
1567     method   => "biblio_mrid_to_modsbatch_batch",
1568     api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1569 );
1570
1571 sub biblio_mrid_to_modsbatch_batch {
1572         my( $self, $client, $mrids) = @_;
1573         # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1574         my @mods;
1575         my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1576         for my $id (@$mrids) {
1577                 next unless defined $id;
1578                 my ($m) = $method->run($id);
1579                 push @mods, $m;
1580         }
1581         return \@mods;
1582 }
1583
1584
1585 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1586              open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1587     {
1588     __PACKAGE__->register_method(
1589         method    => "biblio_mrid_to_modsbatch",
1590         api_name  => $_,
1591         signature => {
1592             desc   => "Returns the mvr associated with a given metarecod. If none exists, it is created.  "
1593                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1594             params => [
1595                 { desc => 'Metarecord ID', type => 'number' },
1596                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1597             ],
1598             return => {
1599                 desc => 'MVR Object, event on error',
1600             }
1601         }
1602     );
1603 }
1604
1605 sub biblio_mrid_to_modsbatch {
1606         my( $self, $client, $mrid, $args) = @_;
1607
1608         # warn "Grabbing mvr for $mrid\n";    # unconditional warn
1609
1610         my ($mr, $evt) = _grab_metarecord($mrid);
1611         return $evt unless $mr;
1612
1613         my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1614               biblio_mrid_make_modsbatch($self, $client, $mr);
1615
1616         return $mvr unless ref($args);  
1617
1618         # Here we find the lead record appropriate for the given filters 
1619         # and use that for the title and author of the metarecord
1620     my $format = $$args{format};
1621     my $org    = $$args{org};
1622     my $depth  = $$args{depth};
1623
1624         return $mvr unless $format or $org or $depth;
1625
1626         my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1627         $method = "$method.staff" if $self->api_name =~ /staff/o; 
1628
1629         my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1630
1631         if( my $mods = $U->record_to_mvr($rec) ) {
1632
1633         $mvr->title( $mods->title );
1634         $mvr->author($mods->author);
1635                 $logger->debug("mods_slim updating title and ".
1636                         "author in mvr with ".$mods->title." : ".$mods->author);
1637         }
1638
1639         return $mvr;
1640 }
1641
1642 # converts a metarecord to an mvr
1643 sub _mr_to_mvr {
1644         my $mr = shift;
1645         my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1646         return Fieldmapper::metabib::virtual_record->new($perl);
1647 }
1648
1649 # checks to see if a metarecord has mods, if so returns true;
1650
1651 __PACKAGE__->register_method(
1652     method   => "biblio_mrid_check_mvr",
1653     api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1654     notes    => "Takes a metarecord ID or a metarecord object and returns true "
1655               . "if the metarecord already has an mvr associated with it."
1656 );
1657
1658 sub biblio_mrid_check_mvr {
1659         my( $self, $client, $mrid ) = @_;
1660         my $mr; 
1661
1662         my $evt;
1663         if(ref($mrid)) { $mr = $mrid; } 
1664         else { ($mr, $evt) = _grab_metarecord($mrid); }
1665         return $evt if $evt;
1666
1667         # warn "Checking mvr for mr " . $mr->id . "\n";   # unconditional warn
1668
1669         return _mr_to_mvr($mr) if $mr->mods();
1670         return undef;
1671 }
1672
1673 sub _grab_metarecord {
1674         my $mrid = shift;
1675         #my $e = OpenILS::Utils::Editor->new;
1676         my $e = new_editor();
1677         my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1678         return ($mr);
1679 }
1680
1681
1682 __PACKAGE__->register_method(
1683     method   => "biblio_mrid_make_modsbatch",
1684     api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1685     notes    => "Takes either a metarecord ID or a metarecord object. "
1686               . "Forces the creations of an mvr for the given metarecord. "
1687               . "The created mvr is returned."
1688 );
1689
1690 sub biblio_mrid_make_modsbatch {
1691         my( $self, $client, $mrid ) = @_;
1692
1693         #my $e = OpenILS::Utils::Editor->new;
1694         my $e = new_editor();
1695
1696         my $mr;
1697         if( ref($mrid) ) {
1698                 $mr = $mrid;
1699                 $mrid = $mr->id;
1700         } else {
1701                 $mr = $e->retrieve_metabib_metarecord($mrid) 
1702                         or return $e->event;
1703         }
1704
1705         my $masterid = $mr->master_record;
1706         $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1707
1708         my $ids = $U->storagereq(
1709                 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1710         return undef unless @$ids;
1711
1712         my $master = $e->retrieve_biblio_record_entry($masterid)
1713                 or return $e->event;
1714
1715         # start the mods batch
1716         my $u = OpenILS::Utils::ModsParser->new();
1717         $u->start_mods_batch( $master->marc );
1718
1719         # grab all of the sub-records and shove them into the batch
1720         my @ids = grep { $_ ne $masterid } @$ids;
1721         #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1722
1723         my $subrecs = [];
1724         if(@$ids) {
1725                 for my $i (@$ids) {
1726                         my $r = $e->retrieve_biblio_record_entry($i);
1727                         push( @$subrecs, $r ) if $r;
1728                 }
1729         }
1730
1731         for(@$subrecs) {
1732                 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1733                 $u->push_mods_batch( $_->marc ) if $_->marc;
1734         }
1735
1736
1737         # finish up and send to the client
1738         my $mods = $u->finish_mods_batch();
1739         $mods->doc_id($mrid);
1740         $client->respond_complete($mods);
1741
1742
1743         # now update the mods string in the db
1744         my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1745         $mr->mods($string);
1746
1747         #$e = OpenILS::Utils::Editor->new(xact => 1);
1748         $e = new_editor(xact => 1);
1749         $e->update_metabib_metarecord($mr) 
1750                 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1751         $e->finish;
1752
1753         return undef;
1754 }
1755
1756
1757 # converts a mr id into a list of record ids
1758
1759 foreach (qw/open-ils.search.biblio.metarecord_to_records
1760             open-ils.search.biblio.metarecord_to_records.staff/)
1761 {
1762     __PACKAGE__->register_method(
1763         method    => "biblio_mrid_to_record_ids",
1764         api_name  => $_,
1765         signature => {
1766             desc   => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1767                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1768             params => [
1769                 { desc => 'Metarecord ID', type => 'number' },
1770                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1771             ],
1772             return => {
1773                 desc => 'Results object like {count => $i, ids =>[...]}',
1774                 type => 'object'
1775             }
1776             
1777         }
1778     );
1779 }
1780
1781 sub biblio_mrid_to_record_ids {
1782         my( $self, $client, $mrid, $args ) = @_;
1783
1784     my $format = $$args{format};
1785     my $org    = $$args{org};
1786     my $depth  = $$args{depth};
1787
1788         my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1789         $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o; 
1790         my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1791
1792         return { count => scalar(@$recs), ids => $recs };
1793 }
1794
1795
1796 __PACKAGE__->register_method(
1797     method   => "biblio_record_to_marc_html",
1798     api_name => "open-ils.search.biblio.record.html"
1799 );
1800
1801 __PACKAGE__->register_method(
1802     method   => "biblio_record_to_marc_html",
1803     api_name => "open-ils.search.authority.to_html"
1804 );
1805
1806 # Persistent parsers and setting objects
1807 my $parser = XML::LibXML->new();
1808 my $xslt   = XML::LibXSLT->new();
1809 my $marc_sheet;
1810 my $slim_marc_sheet;
1811 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1812
1813 sub biblio_record_to_marc_html {
1814         my($self, $client, $recordid, $slim, $marcxml) = @_;
1815
1816     my $sheet;
1817         my $dir = $settings_client->config_value("dirs", "xsl");
1818
1819     if($slim) {
1820         unless($slim_marc_sheet) {
1821                     my $xsl = $settings_client->config_value(
1822                             "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1823             if($xsl) {
1824                         $xsl = $parser->parse_file("$dir/$xsl");
1825                         $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1826             }
1827         }
1828         $sheet = $slim_marc_sheet;
1829     }
1830
1831     unless($sheet) {
1832         unless($marc_sheet) {
1833             my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1834                     my $xsl = $settings_client->config_value(
1835                             "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1836                     $xsl = $parser->parse_file("$dir/$xsl");
1837                     $marc_sheet = $xslt->parse_stylesheet($xsl);
1838         }
1839         $sheet = $marc_sheet;
1840     }
1841
1842     my $record;
1843     unless($marcxml) {
1844         my $e = new_editor();
1845         if($self->api_name =~ /authority/) {
1846             $record = $e->retrieve_authority_record_entry($recordid)
1847                 or return $e->event;
1848         } else {
1849             $record = $e->retrieve_biblio_record_entry($recordid)
1850                 or return $e->event;
1851         }
1852         $marcxml = $record->marc;
1853     }
1854
1855         my $xmldoc = $parser->parse_string($marcxml);
1856         my $html = $sheet->transform($xmldoc);
1857         return $html->documentElement->toString();
1858 }
1859
1860 __PACKAGE__->register_method(
1861     method    => "format_biblio_record_entry",
1862     api_name  => "open-ils.search.biblio.record.print",
1863     signature => {
1864         desc   => 'Returns a printable version of the specified bib record',
1865         params => [
1866             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1867         ],
1868         return => {
1869             desc => q/An action_trigger.event object or error event./,
1870             type => 'object',
1871         }
1872     }
1873 );
1874 __PACKAGE__->register_method(
1875     method    => "format_biblio_record_entry",
1876     api_name  => "open-ils.search.biblio.record.email",
1877     signature => {
1878         desc   => 'Emails an A/T templated version of the specified bib records to the authorized user',
1879         params => [
1880             { desc => 'Authentication token',  type => 'string'},
1881             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1882         ],
1883         return => {
1884             desc => q/Undefined on success, otherwise an error event./,
1885             type => 'object',
1886         }
1887     }
1888 );
1889
1890 sub format_biblio_record_entry {
1891     my($self, $conn, $arg1, $arg2) = @_;
1892
1893     my $for_print = ($self->api_name =~ /print/);
1894     my $for_email = ($self->api_name =~ /email/);
1895
1896     my $e; my $auth; my $bib_id; my $context_org;
1897
1898     if ($for_print) {
1899         $bib_id = $arg1;
1900         $context_org = $arg2 || $U->get_org_tree->id;
1901         $e = new_editor(xact => 1);
1902     } elsif ($for_email) {
1903         $auth = $arg1;
1904         $bib_id = $arg2;
1905         $e = new_editor(authtoken => $auth, xact => 1);
1906         return $e->die_event unless $e->checkauth;
1907         $context_org = $e->requestor->home_ou;
1908     }
1909
1910     my $bib_ids;
1911     if (ref $bib_id ne 'ARRAY') {
1912         $bib_ids = [ $bib_id ];
1913     } else {
1914         $bib_ids = $bib_id;
1915     }
1916
1917     my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1918     $bucket->btype('temp');
1919     $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1920     if ($for_email) {
1921         $bucket->owner($e->requestor) 
1922     } else {
1923         $bucket->owner(1);
1924     }
1925     my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1926
1927     for my $id (@$bib_ids) {
1928
1929         my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1930
1931         my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1932         $bucket_entry->target_biblio_record_entry($bib);
1933         $bucket_entry->bucket($bucket_obj->id);
1934         $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1935     }
1936
1937     $e->commit;
1938
1939     if ($for_print) {
1940
1941         return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1942
1943     } elsif ($for_email) {
1944
1945         $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1946     }
1947
1948     return undef;
1949 }
1950
1951
1952 __PACKAGE__->register_method(
1953     method   => "retrieve_all_copy_statuses",
1954     api_name => "open-ils.search.config.copy_status.retrieve.all"
1955 );
1956
1957 sub retrieve_all_copy_statuses {
1958         my( $self, $client ) = @_;
1959         return new_editor()->retrieve_all_config_copy_status();
1960 }
1961
1962
1963 __PACKAGE__->register_method(
1964     method   => "copy_counts_per_org",
1965     api_name => "open-ils.search.biblio.copy_counts.retrieve"
1966 );
1967
1968 __PACKAGE__->register_method(
1969     method   => "copy_counts_per_org",
1970     api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1971 );
1972
1973 sub copy_counts_per_org {
1974         my( $self, $client, $record_id ) = @_;
1975
1976         warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1977
1978         my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1979         if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1980
1981         my $counts = $apputils->simple_scalar_request(
1982                 "open-ils.storage", $method, $record_id );
1983
1984         $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1985         return $counts;
1986 }
1987
1988
1989 __PACKAGE__->register_method(
1990     method   => "copy_count_summary",
1991     api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1992     notes    => "returns an array of these: "
1993               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
1994               . "where statusx is a copy status name.  The statuses are sorted by ID.",
1995 );
1996                 
1997
1998 sub copy_count_summary {
1999         my( $self, $client, $rid, $org, $depth ) = @_;
2000     $org   ||= 1;
2001     $depth ||= 0;
2002     my $data = $U->storagereq(
2003                 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
2004
2005     return [ sort {
2006         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2007         cmp
2008         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2009     } @$data ];
2010 }
2011
2012 __PACKAGE__->register_method(
2013     method   => "copy_location_count_summary",
2014     api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
2015     notes    => "returns an array of these: "
2016               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
2017               . "where statusx is a copy status name.  The statuses are sorted by ID.",
2018 );
2019
2020 sub copy_location_count_summary {
2021     my( $self, $client, $rid, $org, $depth ) = @_;
2022     $org   ||= 1;
2023     $depth ||= 0;
2024     my $data = $U->storagereq(
2025                 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2026
2027     return [ sort {
2028         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2029         cmp
2030         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2031
2032         || $a->[4] cmp $b->[4]
2033     } @$data ];
2034 }
2035
2036 __PACKAGE__->register_method(
2037     method   => "copy_count_location_summary",
2038     api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2039     notes    => "returns an array of these: "
2040               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2041               . "where statusx is a copy status name.  The statuses are sorted by ID."
2042 );
2043
2044 sub copy_count_location_summary {
2045     my( $self, $client, $rid, $org, $depth ) = @_;
2046     $org   ||= 1;
2047     $depth ||= 0;
2048     my $data = $U->storagereq(
2049         'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2050     return [ sort {
2051         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2052         cmp
2053         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2054     } @$data ];
2055 }
2056
2057
2058 foreach (qw/open-ils.search.biblio.marc
2059             open-ils.search.biblio.marc.staff/)
2060 {
2061 __PACKAGE__->register_method(
2062     method    => "marc_search",
2063     api_name  => $_,
2064     signature => {
2065         desc   => 'Fetch biblio IDs based on MARC record criteria.  '
2066                 . 'As usual, the .staff version of the search includes otherwise hidden records',
2067         params => [
2068             {
2069                 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2070                         'See perldoc ' . __PACKAGE__ . ' for more detail.',
2071                 type => 'object'
2072             },
2073             {desc => 'limit (optional)',  type => 'number'},
2074             {desc => 'offset (optional)', type => 'number'}
2075         ],
2076         return => {
2077             desc => 'Results object like: { "count": $i, "ids": [...] }',
2078             type => 'object'
2079         }
2080     }
2081 );
2082 }
2083
2084 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
2085
2086 As elsewhere the arghash is the required argument, and must be a hashref.  The keys are:
2087
2088     searches: complex query object  (required)
2089     org_unit: The org ID to focus the search at
2090     depth   : The org depth     
2091     limit   : integer search limit      default: 10
2092     offset  : integer search offset     default:  0
2093     sort    : What field to sort the results on? [ author | title | pubdate ]
2094     sort_dir: In what direction do we sort? [ asc | desc ]
2095
2096 Additional keys to refine search criteria:
2097
2098     audience : Audience
2099     language : Language (code)
2100     lit_form : Literary form
2101     item_form: Item form
2102     item_type: Item type
2103     format   : The MARC format
2104
2105 Please note that the specific strings to be used in the "addtional keys" will be entirely
2106 dependent on your loaded data.  
2107
2108 All keys except "searches" are optional.
2109 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".  
2110
2111 For example, an arg hash might look like:
2112
2113     $arghash = {
2114         searches => [
2115             {
2116                 term     => "harry",
2117                 restrict => [
2118                     {
2119                         tag => 245,
2120                         subfield => "a"
2121                     }
2122                     # ...
2123                 ]
2124             }
2125             # ...
2126         ],
2127         org_unit  => 1,
2128         limit     => 5,
2129         sort      => "author",
2130         item_type => "g"
2131     }
2132
2133 The arghash is eventually passed to the SRF call:
2134 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2135
2136 Presently, search uses the cache unconditionally.
2137
2138 =cut
2139
2140 # FIXME: that example above isn't actually tested.
2141 # TODO: docache option?
2142 sub marc_search {
2143         my( $self, $conn, $args, $limit, $offset, $timeout ) = @_;
2144
2145         my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2146         $method .= ".staff" if $self->api_name =~ /staff/;
2147         $method .= ".atomic";
2148
2149     $limit  ||= 10;     # FIXME: what about $args->{limit} ?
2150     $offset ||=  0;     # FIXME: what about $args->{offset} ?
2151
2152     # allow caller to pass in a call timeout since MARC searches
2153     # can take longer than the default 60-second timeout.  
2154     # Default to 2 mins.  Arbitrarily cap at 5 mins.
2155     $timeout = 120 if !$timeout or $timeout > 300;
2156
2157         my @search;
2158         push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2159         my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2160
2161         my $recs = search_cache($ckey, $offset, $limit);
2162
2163         if(!$recs) {
2164
2165         my $ses = OpenSRF::AppSession->create('open-ils.storage');
2166         my $req = $ses->request($method, %$args);
2167         my $resp = $req->recv($timeout);
2168
2169         if($resp and $recs = $resp->content) {
2170                         put_cache($ckey, scalar(@$recs), $recs);
2171                         $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2172                 } else {
2173                         $recs = [];
2174                 }
2175
2176         $ses->kill_me;
2177         }
2178
2179         my $count = 0;
2180         $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2181         my @recs = map { $_->[0] } @$recs;
2182
2183         return { ids => \@recs, count => $count };
2184 }
2185
2186
2187 foreach my $isbn_method (qw/
2188     open-ils.search.biblio.isbn
2189     open-ils.search.biblio.isbn.staff
2190 /) {
2191 __PACKAGE__->register_method(
2192     method    => "biblio_search_isbn",
2193     api_name  => $isbn_method,
2194     signature => {
2195         desc   => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2196         params => [
2197             {desc => 'ISBN', type => 'string'}
2198         ],
2199         return => {
2200             desc => 'Results object like: { "count": $i, "ids": [...] }',
2201             type => 'object'
2202         }
2203     }
2204 );
2205 }
2206
2207 sub biblio_search_isbn { 
2208         my( $self, $client, $isbn ) = @_;
2209         $logger->debug("Searching ISBN $isbn");
2210         # the previous implementation of this method was essentially unlimited,
2211         # so we will set our limit very high and let multiclass.query provide any
2212         # actual limit
2213         # XXX: if making this unlimited is deemed important, we might consider
2214         # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2215         # which is functionally deprecated at this point, or a custom call to
2216         # 'open-ils.storage.biblio.multiclass.search_fts'
2217
2218     my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2219     if ($self->api_name =~ m/.staff$/) {
2220         $isbn_method .= '.staff';
2221     }
2222
2223         my $method = $self->method_lookup($isbn_method);
2224         my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2225         my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2226         return { ids => \@recs, count => $search_result->{'count'} };
2227 }
2228
2229 __PACKAGE__->register_method(
2230     method   => "biblio_search_isbn_batch",
2231     api_name => "open-ils.search.biblio.isbn_list",
2232 );
2233
2234 # XXX: see biblio_search_isbn() for note concerning 'limit'
2235 sub biblio_search_isbn_batch { 
2236         my( $self, $client, $isbn_list ) = @_;
2237         $logger->debug("Searching ISBNs @$isbn_list");
2238         my @recs = (); my %rec_set = ();
2239         my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2240         foreach my $isbn ( @$isbn_list ) {
2241                 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2242                 my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2243                 foreach my $rec (@recs_subset) {
2244                         if (! $rec_set{ $rec }) {
2245                                 $rec_set{ $rec } = 1;
2246                                 push @recs, $rec;
2247                         }
2248                 }
2249         }
2250         return { ids => \@recs, count => scalar(@recs) };
2251 }
2252
2253 foreach my $issn_method (qw/
2254     open-ils.search.biblio.issn
2255     open-ils.search.biblio.issn.staff
2256 /) {
2257 __PACKAGE__->register_method(
2258     method   => "biblio_search_issn",
2259     api_name => $issn_method,
2260     signature => {
2261         desc   => 'Retrieve biblio IDs for a given ISSN',
2262         params => [
2263             {desc => 'ISBN', type => 'string'}
2264         ],
2265         return => {
2266             desc => 'Results object like: { "count": $i, "ids": [...] }',
2267             type => 'object'
2268         }
2269     }
2270 );
2271 }
2272
2273 sub biblio_search_issn { 
2274         my( $self, $client, $issn ) = @_;
2275         $logger->debug("Searching ISSN $issn");
2276         # the previous implementation of this method was essentially unlimited,
2277         # so we will set our limit very high and let multiclass.query provide any
2278         # actual limit
2279         # XXX: if making this unlimited is deemed important, we might consider
2280         # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2281         # which is functionally deprecated at this point, or a custom call to
2282         # 'open-ils.storage.biblio.multiclass.search_fts'
2283
2284     my $issn_method = 'open-ils.search.biblio.multiclass.query';
2285     if ($self->api_name =~ m/.staff$/) {
2286         $issn_method .= '.staff';
2287     }
2288
2289         my $method = $self->method_lookup($issn_method);
2290         my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2291         my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2292         return { ids => \@recs, count => $search_result->{'count'} };
2293 }
2294
2295
2296 __PACKAGE__->register_method(
2297     method    => "fetch_mods_by_copy",
2298     api_name  => "open-ils.search.biblio.mods_from_copy",
2299     argc      => 1,
2300     signature => {
2301         desc    => 'Retrieve MODS record given an attached copy ID',
2302         params  => [
2303             { desc => 'Copy ID', type => 'number' }
2304         ],
2305         returns => {
2306             desc => 'MODS record, event on error or uncataloged item'
2307         }
2308     }
2309 );
2310
2311 sub fetch_mods_by_copy {
2312         my( $self, $client, $copyid ) = @_;
2313         my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2314         return $evt if $evt;
2315         return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2316         return $apputils->record_to_mvr($record);
2317 }
2318
2319
2320 # -------------------------------------------------------------------------------------
2321
2322 __PACKAGE__->register_method(
2323     method   => "cn_browse",
2324     api_name => "open-ils.search.callnumber.browse.target",
2325     notes    => "Starts a callnumber browse"
2326 );
2327
2328 __PACKAGE__->register_method(
2329     method   => "cn_browse",
2330     api_name => "open-ils.search.callnumber.browse.page_up",
2331     notes    => "Returns the previous page of callnumbers",
2332 );
2333
2334 __PACKAGE__->register_method(
2335     method   => "cn_browse",
2336     api_name => "open-ils.search.callnumber.browse.page_down",
2337     notes    => "Returns the next page of callnumbers",
2338 );
2339
2340
2341 # RETURNS array of arrays like so: label, owning_lib, record, id
2342 sub cn_browse {
2343         my( $self, $client, @params ) = @_;
2344         my $method;
2345
2346         $method = 'open-ils.storage.asset.call_number.browse.target.atomic' 
2347                 if( $self->api_name =~ /target/ );
2348         $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2349                 if( $self->api_name =~ /page_up/ );
2350         $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2351                 if( $self->api_name =~ /page_down/ );
2352
2353         return $apputils->simplereq( 'open-ils.storage', $method, @params );
2354 }
2355 # -------------------------------------------------------------------------------------
2356
2357 __PACKAGE__->register_method(
2358     method        => "fetch_cn",
2359     api_name      => "open-ils.search.callnumber.retrieve",
2360     authoritative => 1,
2361     notes         => "retrieves a callnumber based on ID",
2362 );
2363
2364 sub fetch_cn {
2365         my( $self, $client, $id ) = @_;
2366
2367         my $e = new_editor();
2368         my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2369         return $evt if $evt;
2370         return $cn;
2371 }
2372
2373 __PACKAGE__->register_method(
2374     method        => "fetch_fleshed_cn",
2375     api_name      => "open-ils.search.callnumber.fleshed.retrieve",
2376     authoritative => 1,
2377     notes         => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2378 );
2379
2380 sub fetch_fleshed_cn {
2381         my( $self, $client, $id ) = @_;
2382
2383         my $e = new_editor();
2384         my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2385         return $evt if $evt;
2386         return $cn;
2387 }
2388
2389
2390 __PACKAGE__->register_method(
2391     method    => "fetch_copy_by_cn",
2392     api_name  => 'open-ils.search.copies_by_call_number.retrieve',
2393     signature => q/
2394                 Returns an array of copy ID's by callnumber ID
2395                 @param cnid The callnumber ID
2396                 @return An array of copy IDs
2397         /
2398 );
2399
2400 sub fetch_copy_by_cn {
2401         my( $self, $conn, $cnid ) = @_;
2402         return $U->cstorereq(
2403                 'open-ils.cstore.direct.asset.copy.id_list.atomic', 
2404                 { call_number => $cnid, deleted => 'f' } );
2405 }
2406
2407 __PACKAGE__->register_method(
2408     method    => 'fetch_cn_by_info',
2409     api_name  => 'open-ils.search.call_number.retrieve_by_info',
2410     signature => q/
2411                 @param label The callnumber label
2412                 @param record The record the cn is attached to
2413                 @param org The owning library of the cn
2414                 @return The callnumber object
2415         /
2416 );
2417
2418
2419 sub fetch_cn_by_info {
2420         my( $self, $conn, $label, $record, $org ) = @_;
2421         return $U->cstorereq(
2422                 'open-ils.cstore.direct.asset.call_number.search',
2423                 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2424 }
2425
2426
2427
2428 __PACKAGE__->register_method(
2429     method   => 'bib_extras',
2430     api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2431     ctype => 'lit_form'
2432 );
2433 __PACKAGE__->register_method(
2434     method   => 'bib_extras',
2435     api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2436     ctype => 'item_form'
2437 );
2438 __PACKAGE__->register_method(
2439     method   => 'bib_extras',
2440     api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2441     ctype => 'item_type',
2442 );
2443 __PACKAGE__->register_method(
2444     method   => 'bib_extras',
2445     api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2446     ctype => 'bib_level'
2447 );
2448 __PACKAGE__->register_method(
2449     method   => 'bib_extras',
2450     api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2451     ctype => 'audience'
2452 );
2453
2454 sub bib_extras {
2455         my $self = shift;
2456     $logger->warn("deprecation warning: " .$self->api_name);
2457
2458         my $e = new_editor();
2459
2460     my $ctype = $self->{ctype};
2461     my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2462
2463     my @objs;
2464     for my $ccvm (@$ccvms) {
2465         my $obj = "Fieldmapper::config::${ctype}_map"->new;
2466         $obj->value($ccvm->value);
2467         $obj->code($ccvm->code);
2468         $obj->description($ccvm->description) if $obj->can('description');
2469         push(@objs, $obj);
2470     }
2471
2472     return \@objs;
2473 }
2474
2475
2476
2477 __PACKAGE__->register_method(
2478     method    => 'fetch_slim_record',
2479     api_name  => 'open-ils.search.biblio.record_entry.slim.retrieve',
2480     signature => {
2481         desc   => "Retrieves one or more biblio.record_entry without the attached marcxml",
2482         params => [
2483             { desc => 'Array of Record IDs', type => 'array' }
2484         ],
2485         return => { 
2486             desc => 'Array of biblio records, event on error'
2487         }
2488     }
2489 );
2490
2491 sub fetch_slim_record {
2492     my( $self, $conn, $ids ) = @_;
2493
2494 #my $editor = OpenILS::Utils::Editor->new;
2495     my $editor = new_editor();
2496         my @res;
2497     for( @$ids ) {
2498         return $editor->event unless
2499             my $r = $editor->retrieve_biblio_record_entry($_);
2500         $r->clear_marc;
2501         push(@res, $r);
2502     }
2503     return \@res;
2504 }
2505
2506 __PACKAGE__->register_method(
2507     method    => 'rec_hold_parts',
2508     api_name  => 'open-ils.search.biblio.record_hold_parts',
2509     signature => q/
2510        Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2511         /
2512 );
2513
2514 sub rec_hold_parts {
2515         my( $self, $conn, $args ) = @_;
2516
2517     my $rec        = $$args{record};
2518     my $mrec       = $$args{metarecord};
2519     my $pickup_lib = $$args{pickup_lib};
2520     my $e = new_editor();
2521
2522     my $query = {
2523         select => {bmp => ['id', 'label']},
2524         from => 'bmp',
2525         where => {
2526             id => {
2527                 in => {
2528                     select => {'acpm' => ['part']},
2529                     from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2530                     where => {
2531                         '+acp' => {'deleted' => 'f'},
2532                         '+bre' => {id => $rec}
2533                     },
2534                     distinct => 1,
2535                 }
2536             }
2537         },
2538         order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2539     };
2540
2541     if(defined $pickup_lib) {
2542         my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2543         if($hard_boundary) {
2544             my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2545             $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2546         }
2547     }
2548
2549     return $e->json_query($query);
2550 }
2551
2552
2553
2554
2555 __PACKAGE__->register_method(
2556     method    => 'rec_to_mr_rec_descriptors',
2557     api_name  => 'open-ils.search.metabib.record_to_descriptors',
2558     signature => q/
2559                 specialized method...
2560                 Given a biblio record id or a metarecord id, 
2561                 this returns a list of metabib.record_descriptor
2562                 objects that live within the same metarecord
2563                 @param args Object of args including:
2564         /
2565 );
2566
2567 sub rec_to_mr_rec_descriptors {
2568         my( $self, $conn, $args ) = @_;
2569
2570     my $rec        = $$args{record};
2571     my $mrec       = $$args{metarecord};
2572     my $item_forms = $$args{item_forms};
2573     my $item_types = $$args{item_types};
2574     my $item_lang  = $$args{item_lang};
2575     my $pickup_lib = $$args{pickup_lib};
2576
2577     my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2578
2579         my $e = new_editor();
2580         my $recs;
2581
2582         if( !$mrec ) {
2583                 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2584                 return $e->event unless @$map;
2585                 $mrec = $$map[0]->metarecord;
2586         }
2587
2588         $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2589         return $e->event unless @$recs;
2590
2591         my @recs = map { $_->source } @$recs;
2592         my $search = { record => \@recs };
2593         $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2594         $search->{item_type} = $item_types if $item_types and @$item_types;
2595         $search->{item_lang} = $item_lang  if $item_lang;
2596
2597         my $desc = $e->search_metabib_record_descriptor($search);
2598
2599         my $query = {
2600                 distinct => 1,
2601                 select   => { 'bre' => ['id'] },
2602                 from     => {
2603                         'bre' => {
2604                                 'acn' => {
2605                                         'join' => {
2606                                                 'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2607                                           }
2608                                   }
2609                          }
2610                 },
2611                 where => {
2612                         '+bre' => { id => \@recs },
2613                         '+acp' => {
2614                                 holdable => 't',
2615                                 deleted  => 'f'
2616                         },
2617                         "+ccs" => { holdable => 't' },
2618                         "+acpl" => { holdable => 't' }
2619                 }
2620         };
2621
2622         if ($hard_boundary) { # 0 (or "top") is the same as no setting
2623                 my $orgs = $e->json_query(
2624                         { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2625                 ) or return $e->die_event;
2626
2627                 $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2628         }
2629
2630         my $good_records = $e->json_query($query) or return $e->die_event;
2631
2632         my @keep;
2633         for my $d (@$desc) {
2634                 if ( grep { $d->record == $_->{id} } @$good_records ) {
2635                         push @keep, $d;
2636                 }
2637         }
2638
2639         $desc = \@keep;
2640
2641         return { metarecord => $mrec, descriptors => $desc };
2642 }
2643
2644
2645 __PACKAGE__->register_method(
2646     method   => 'fetch_age_protect',
2647     api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2648 );
2649
2650 sub fetch_age_protect {
2651         return new_editor()->retrieve_all_config_rule_age_hold_protect();
2652 }
2653
2654
2655 __PACKAGE__->register_method(
2656     method   => 'copies_by_cn_label',
2657     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2658 );
2659
2660 __PACKAGE__->register_method(
2661     method   => 'copies_by_cn_label',
2662     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2663 );
2664
2665 sub copies_by_cn_label {
2666         my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2667         my $e = new_editor();
2668     my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2669     my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2670         my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2671         return [] unless @$cns;
2672
2673         # show all non-deleted copies in the staff client ...
2674         if ($self->api_name =~ /staff$/o) {
2675                 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2676         }
2677
2678         # ... otherwise, grab the copies ...
2679         my $copies = $e->search_asset_copy(
2680                 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2681                   {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2682                 ]
2683         );
2684
2685         # ... and test for location and status visibility
2686         return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2687 }
2688
2689
2690 1;
2691