]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
tpac/kpac : search_filter_group cache repairs
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / WWW / EGCatLoader / Util.pm
1 package OpenILS::WWW::EGCatLoader;
2 use strict; use warnings;
3 use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
4 use File::Spec;
5 use OpenSRF::Utils::Logger qw/$logger/;
6 use OpenILS::Utils::CStoreEditor qw/:funcs/;
7 use OpenILS::Utils::Fieldmapper;
8 use OpenILS::Application::AppUtils;
9 use OpenSRF::MultiSession;
10 my $U = 'OpenILS::Application::AppUtils';
11
12 my $ro_object_subs; # cached subs
13 our %cache = ( # cached data
14     map => {aou => {}}, # others added dynamically as needed
15     list => {},
16     search => {},
17     org_settings => {},
18     eg_cache_hash => undef,
19     search_filter_groups => {}
20 );
21
22 sub init_ro_object_cache {
23     my $self = shift;
24     my $e = $self->editor;
25     my $ctx = $self->ctx;
26
27     # reset org unit setting cache on each page load to avoid the 
28     # requirement of reloading apache with each org-setting change
29     $cache{org_settings} = {};
30
31     if($ro_object_subs) {
32         # subs have been built.  insert into the context then move along.
33         $ctx->{$_} = $ro_object_subs->{$_} for keys %$ro_object_subs;
34         return;
35     }
36
37     # make all "field_safe" classes accesible by default in the template context
38     my @classes = grep {
39         ($Fieldmapper::fieldmap->{$_}->{field_safe} || '') =~ /true/i
40     } keys %{ $Fieldmapper::fieldmap };
41
42     for my $class (@classes) {
43
44         my $hint = $Fieldmapper::fieldmap->{$class}->{hint};
45         next if $hint eq 'aou'; # handled separately
46
47         my $ident_field =  $Fieldmapper::fieldmap->{$class}->{identity};
48         (my $eclass = $class) =~ s/Fieldmapper:://o;
49         $eclass =~ s/::/_/g;
50
51         my $list_key = "${hint}_list";
52         my $get_key = "get_$hint";
53         my $search_key = "search_$hint";
54
55         # Retrieve the full set of objects with class $hint
56         $ro_object_subs->{$list_key} = sub {
57             my $method = "retrieve_all_$eclass";
58             $cache{list}{$hint} = $e->$method() unless $cache{list}{$hint};
59             return $cache{list}{$hint};
60         };
61     
62         # locate object of class $hint with Ident field $id
63         $cache{map}{$hint} = {};
64         $ro_object_subs->{$get_key} = sub {
65             my $id = shift;
66             return $cache{map}{$hint}{$id} if $cache{map}{$hint}{$id}; 
67             ($cache{map}{$hint}{$id}) = grep { $_->$ident_field eq $id } @{$ro_object_subs->{$list_key}->()};
68             return $cache{map}{$hint}{$id};
69         };
70
71         # search for objects of class $hint where field=value
72         $cache{search}{$hint} = {};
73         $ro_object_subs->{$search_key} = sub {
74             my ($field, $val, $filterfield, $filterval) = @_;
75             my $method = "search_$eclass";
76             my $cacheval = $val;
77             if (ref $val) {
78                 $val = [sort(@$val)] if ref $val eq 'ARRAY';
79                 $cacheval = OpenSRF::Utils::JSON->perl2JSON($val); 
80                 #$self->apache->log->info("cacheval : $cacheval");
81             }
82             my $search_obj = {$field => $val};
83             if($filterfield) {
84                 $search_obj->{$filterfield} = $filterval;
85                 $cacheval .= ':' . $filterfield . ':' . $filterval;
86             }
87             $cache{search}{$hint}{$field} = {} unless $cache{search}{$hint}{$field};
88             $cache{search}{$hint}{$field}{$cacheval} = $e->$method($search_obj) 
89                 unless $cache{search}{$hint}{$field}{$cacheval};
90             return $cache{search}{$hint}{$field}{$cacheval};
91         };
92     }
93
94     $ro_object_subs->{aou_tree} = sub {
95
96         # fetch the org unit tree
97         unless($cache{aou_tree}) {
98             my $tree = $e->search_actor_org_unit([
99                             {   parent_ou => undef},
100                             {   flesh            => -1,
101                                     flesh_fields    => {aou =>  ['children']},
102                                     order_by        => {aou => 'name'}
103                             }
104                     ])->[0];
105
106             # flesh the org unit type for each org unit
107             # and simultaneously set the id => aou map cache
108             sub flesh_aout {
109                 my $node = shift;
110                 my $ro_object_subs = shift;
111                 $node->ou_type( $ro_object_subs->{get_aout}->($node->ou_type) );
112                 $cache{map}{aou}{$node->id} = $node;
113                 flesh_aout($_, $ro_object_subs) foreach @{$node->children};
114             };
115             flesh_aout($tree, $ro_object_subs);
116
117             $cache{aou_tree} = $tree;
118         }
119
120         return $cache{aou_tree};
121     };
122
123     # Add a special handler for the tree-shaped org unit cache
124     $ro_object_subs->{get_aou} = sub {
125         my $org_id = shift;
126         return undef unless defined $org_id;
127         $ro_object_subs->{aou_tree}->(); # force the org tree to load
128         return $cache{map}{aou}{$org_id};
129     };
130
131     # Returns a flat list of aou objects.  often easier to manage than a tree.
132     $ro_object_subs->{aou_list} = sub {
133         $ro_object_subs->{aou_tree}->(); # force the org tree to load
134         return [ values %{$cache{map}{aou}} ];
135     };
136
137     $ro_object_subs->{aouct_tree} = sub {
138
139         # fetch the org unit tree
140         unless(exists $cache{aouct_tree}) {
141             $cache{aouct_tree} = undef;
142
143             my $tree_id = $e->search_actor_org_unit_custom_tree(
144                 {purpose => 'opac', active => 't'},
145                 {idlist => 1}
146             )->[0];
147
148             if ($tree_id) {
149                 my $node_tree = $e->search_actor_org_unit_custom_tree_node([
150                 {parent_node => undef, tree => $tree_id},
151                 {   flesh        => -1,
152                     flesh_fields => {aouctn => ['children', 'org_unit']},
153                     order_by     => {aouctn => 'sibling_order'}
154                 }
155                 ])->[0];
156
157                 # tree-ify the org units.  note that since the orgs are fleshed
158                 # upon retrieval, this org tree will not clobber ctx->{aou_tree}.
159                 my @nodes = ($node_tree);
160                 while (my $node = shift(@nodes)) {
161                     my $aou = $node->org_unit;
162                     $aou->children([]);
163                     for my $cnode (@{$node->children}) {
164                         my $child_org = $cnode->org_unit;
165                         $child_org->parent_ou($aou->id);
166                         $child_org->ou_type( $ro_object_subs->{get_aout}->($child_org->ou_type) );
167                         push(@{$aou->children}, $child_org);
168                         push(@nodes, $cnode);
169                     }
170                 }
171
172                 $cache{aouct_tree} = $node_tree->org_unit;
173             }
174         }
175
176         return $cache{aouct_tree};
177     };
178
179     # turns an ISO date into something TT can understand
180     $ro_object_subs->{parse_datetime} = sub {
181         my $date = shift;
182         $date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date));
183         return sprintf(
184             "%0.2d:%0.2d:%0.2d %0.2d-%0.2d-%0.4d",
185             $date->hour,
186             $date->minute,
187             $date->second,
188             $date->day,
189             $date->month,
190             $date->year
191         );
192     };
193
194     # retrieve and cache org unit setting values
195     $ro_object_subs->{get_org_setting} = sub {
196         my($org_id, $setting) = @_;
197
198         $cache{org_settings}{$org_id} = {} 
199             unless $cache{org_settings}{$org_id};
200
201         $cache{org_settings}{$org_id}{$setting} = 
202             $U->ou_ancestor_setting_value($org_id, $setting)
203                 unless exists $cache{org_settings}{$org_id}{$setting};
204
205         return $cache{org_settings}{$org_id}{$setting};
206     };
207
208     $ctx->{$_} = $ro_object_subs->{$_} for keys %$ro_object_subs;
209 }
210
211 sub generic_redirect {
212     my $self = shift;
213     my $url = shift;
214     my $cookie = shift; # can be an array of cgi.cookie's
215
216     $self->apache->print(
217         $self->cgi->redirect(
218             -url => $url || 
219                 $self->cgi->param('redirect_to') || 
220                 $self->ctx->{referer} || 
221                 $self->ctx->{home_page},
222             -cookie => $cookie
223         )
224     );
225
226     return Apache2::Const::REDIRECT;
227 }
228
229 sub get_records_and_facets {
230     my ($self, $rec_ids, $facet_key, $unapi_args) = @_;
231
232     $unapi_args ||= {};
233     $unapi_args->{site} ||= $self->ctx->{aou_tree}->()->shortname;
234     $unapi_args->{depth} ||= $self->ctx->{aou_tree}->()->ou_type->depth;
235     $unapi_args->{flesh_depth} ||= 5;
236
237     my @data;
238     my $outer_self = $self;
239     $self->timelog("get_records_and_facets(): about to call multisession");
240     my $ses = OpenSRF::MultiSession->new(
241         app => 'open-ils.cstore',
242         cap => 10, # XXX config
243         success_handler => sub {
244             my($self, $req) = @_;
245             my $data = $req->{response}->[0]->content;
246
247             $outer_self->timelog("get_records_and_facets(): got response content");
248
249             # Protect against requests for non-existent records
250             return unless $data->{'unapi.bre'};
251
252             my $xml = XML::LibXML->new->parse_string($data->{'unapi.bre'})->documentElement;
253
254             $outer_self->timelog("get_records_and_facets(): parsed xml");
255             # Protect against legacy invalid MARCXML that might not have a 901c
256             my $bre_id;
257             my $bre_id_nodes =  $xml->find('*[@tag="901"]/*[@code="c"]');
258             if ($bre_id_nodes) {
259                 $bre_id =  $bre_id_nodes->[0]->textContent;
260             } else {
261                 $logger->warn("Missing 901 subfield 'c' in " . $xml->toString());
262             }
263             push(@data, {id => $bre_id, marc_xml => $xml});
264             $outer_self->timelog("get_records_and_facets(): end of success handler");
265         }
266     );
267
268     $self->timelog("get_records_and_facets(): about to call unapi.bre via json_query (rec_ids has " . scalar(@$rec_ids));
269
270     $ses->request(
271         'open-ils.cstore.json_query',
272          {from => [
273             'unapi.bre', $_, 'marcxml','record', 
274             $unapi_args->{flesh}, 
275             $unapi_args->{site}, 
276             $unapi_args->{depth}, 
277             'acn=>' . $unapi_args->{flesh_depth} . ',acp=>' . $unapi_args->{flesh_depth}, 
278             undef, undef, $unapi_args->{pref_lib}
279         ]}
280     ) for @$rec_ids;
281
282
283     $self->timelog("get_records_and_facets():almost ready to fetch facets");
284     # collect the facet data
285     my $search = OpenSRF::AppSession->create('open-ils.search');
286     my $facet_req = $search->request(
287         'open-ils.search.facet_cache.retrieve', $facet_key, 10
288     ) if $facet_key;
289
290     # gather up the unapi recs
291     $ses->session_wait(1);
292     $self->timelog("get_records_and_facets():past session wait");
293
294     my $facets = {};
295     if ($facet_key) {
296         my $tmp_facets = $facet_req->gather(1);
297         $self->timelog("get_records_and_facets(): gathered facet data");
298         for my $cmf_id (keys %$tmp_facets) {
299
300             # sort highest to lowest match count
301             my @entries;
302             my $entries = $tmp_facets->{$cmf_id};
303             for my $ent (keys %$entries) {
304                 push(@entries, {value => $ent, count => $$entries{$ent}});
305             };
306             @entries = sort { $b->{count} <=> $a->{count} } @entries;
307             $facets->{$cmf_id} = {
308                 cmf => $self->ctx->{get_cmf}->($cmf_id),
309                 data => \@entries
310             }
311         }
312         $self->timelog("get_records_and_facets(): gathered/sorted facet data");
313     } else {
314         $facets = undef;
315     }
316
317     $search->kill_me;
318
319     return ($facets, @data);
320 }
321
322 # TODO: blend this code w/ ^-- get_records_and_facets
323 sub fetch_marc_xml_by_id {
324     my ($self, $id_list) = @_;
325     $id_list = [$id_list] unless ref($id_list);
326
327     {
328         no warnings qw/numeric/;
329         $id_list = [map { int $_ } @$id_list];
330         $id_list = [grep { $_ > 0} @$id_list];
331     };
332
333     return {} if scalar(@$id_list) < 1;
334
335     # I'm just sure there needs to be some more efficient way to get all of
336     # this.
337     my $results = $self->editor->json_query({
338         "select" => {"bre" => ["id", "marc"]},
339         "from" => {"bre" => {}},
340         "where" => {"id" => $id_list}
341     }, {substream => 1}) or return $self->editor->die_event;
342
343     my $marc_xml = {};
344     for my $r (@$results) {
345         $marc_xml->{$r->{"id"}} =
346             (new XML::LibXML)->parse_string($r->{"marc"});
347     }
348
349     return $marc_xml;
350 }
351
352 sub _get_search_lib {
353     my $self = shift;
354     my $ctx = $self->ctx;
355
356     # avoid duplicate lookups
357     return $ctx->{search_ou} if $ctx->{search_ou};
358
359     my $loc = $ctx->{copy_location_group_org};
360     return $loc if $loc;
361
362     # loc param takes precedence
363     $loc = $self->cgi->param('loc');
364     return $loc if $loc;
365
366     my $pref_lib = $self->_get_pref_lib();
367     return $pref_lib if $pref_lib;
368
369     return $ctx->{aou_tree}->()->id;
370 }
371
372 sub _get_pref_lib {
373     my $self = shift;
374     my $ctx = $self->ctx;
375
376     # plib param takes precedence
377     my $plib = $self->cgi->param('plib');
378     return $plib if $plib;
379
380     if ($ctx->{user}) {
381         # See if the user has a search library preference
382         my $lset = $self->editor->search_actor_user_setting({
383             usr => $ctx->{user}->id, 
384             name => 'opac.default_search_location'
385         })->[0];
386         return OpenSRF::Utils::JSON->JSON2perl($lset->value) if $lset;
387
388         # Otherwise return the user's home library
389         return $ctx->{user}->home_ou;
390     }
391
392     if ($self->cgi->param('physical_loc')) {
393         return $self->cgi->param('physical_loc');
394     }
395
396 }
397
398 # This is defensively coded since we don't do much manual reading from the
399 # file system in this module.
400 sub load_eg_cache_hash {
401     my ($self) = @_;
402
403     # just a context helper
404     $self->ctx->{eg_cache_hash} = sub { return $cache{eg_cache_hash}; };
405
406     # Need to actually load the value? If already done, move on.
407     return if defined $cache{eg_cache_hash};
408
409     # In this way even if we fail, we won't slow things down by ever trying
410     # again within this Apache process' lifetime.
411     $cache{eg_cache_hash} = 0;
412
413     my $path = File::Spec->catfile(
414         $self->apache->document_root, "eg_cache_hash"
415     );
416
417     if (not open FH, "<$path") {
418         $self->apache->log->warn("error opening $path : $!");
419         return;
420     } else {
421         my $buf;
422         my $rv = read FH, $buf, 64;  # defensive
423         close FH;
424
425         if (not defined $rv) {  # error
426             $self->apache->log->warn("error reading $path : $!");
427         } elsif ($rv > 0) {     # no error, something read
428             chomp $buf;
429             $cache{eg_cache_hash} = $buf;
430         }
431     }
432 }
433
434 # Extracts the copy location org unit and group from the 
435 # "logc" param, which takes the form org_id:grp_id.
436 sub extract_copy_location_group_info {
437     my $self = shift;
438     my $ctx = $self->ctx;
439     if (my $clump = $self->cgi->param('locg')) {
440         my ($org, $grp) = split(/:/, $clump);
441         $ctx->{copy_location_group_org} = $org;
442         $ctx->{copy_location_group} = $grp if $grp;
443     }
444 }
445
446 sub load_copy_location_groups {
447     my $self = shift;
448     my $ctx = $self->ctx;
449
450     # User can access to the search location groups at the current 
451     # search lib, the physical location lib, and the patron's home ou.
452     my @ctx_orgs = $ctx->{search_ou};
453     push(@ctx_orgs, $ctx->{physical_loc}) if $ctx->{physical_loc};
454     push(@ctx_orgs, $ctx->{user}->home_ou) if $ctx->{user};
455
456     my $grps = $self->editor->search_asset_copy_location_group([
457         {
458             opac_visible => 't',
459             owner => {
460                 in => {
461                     select => {aou => [{
462                         column => 'id', 
463                         transform => 'actor.org_unit_full_path',
464                         result_field => 'id',
465                     }]},
466                     from => 'aou',
467                     where => {id => \@ctx_orgs}
468                 }
469             }
470         },
471         {order_by => {acplg => 'pos'}}
472     ]);
473
474     my %buckets;
475     push(@{$buckets{$_->owner}}, $_) for @$grps;
476     $ctx->{copy_location_groups} = \%buckets;
477 }
478
479 sub set_file_download_headers {
480     my $self = shift;
481     my $filename = shift;
482     my $ctype = shift || "text/plain; encoding=utf8";
483
484     $self->apache->content_type($ctype);
485
486     $self->apache->headers_out->add(
487         "Content-Disposition",
488         "attachment;filename=$filename"
489     );
490
491     return Apache2::Const::OK;
492 }
493
494 sub apache_log_if_event {
495     my ($self, $event, $prefix_text, $success_ok, $level) = @_;
496
497     $prefix_text ||= "Evergreen returned event";
498     $success_ok ||= 0;
499     $level ||= "warn";
500
501     chomp $prefix_text;
502     $prefix_text .= ": ";
503
504     my $code = $U->event_code($event);
505     if (defined $code and ($code or not $success_ok)) {
506         $self->apache->log->$level(
507             $prefix_text .
508             ($event->{textcode} || "") . " ($code)" .
509             ($event->{note} ? (": " . $event->{note}) : "")
510         );
511         return 1;
512     }
513
514     return;
515 }
516
517 sub load_search_filter_groups {
518     my $self = shift;
519     my $ctx_org = shift;
520     my $org_list = $U->get_org_ancestors($ctx_org, 1);
521
522     my %seen;
523     for my $org_id (@$org_list) {
524
525         my $grps;
526         if (! ($grps = $cache{search_filter_groups}{$org_id}) ) {
527             $grps = $self->editor->search_actor_search_filter_group([
528                 {owner => $org_id},
529                 {   flesh => 2, 
530                     flesh_fields => {
531                         asfg => ['entries'],
532                         asfge => ['query']
533                     }
534                 }
535             ]);
536             $cache{search_filter_groups}{$org_id} = $grps;
537         }
538
539         # for the current context, if a descendant org has a group 
540         # with a matching code replace the group from the parent.
541         $seen{$_->code} = $_ for @$grps;
542     }
543
544     return $self->ctx->{search_filter_groups} = \%seen;
545 }
546
547
548 sub check_for_temp_list_warning {
549     my $self = shift;
550     my $ctx = $self->ctx;
551     my $cgi = $self->cgi;
552
553     my $lib = $self->_get_search_lib;
554     my $warn = ($ctx->{get_org_setting}->($lib || 1, 'opac.patron.temporary_list_warn')) ? 1 : 0;
555
556     if ($warn && $ctx->{user}) {
557         $self->_load_user_with_prefs;
558         my $map = $ctx->{user_setting_map};
559         $warn = 0 if ($$map{'opac.temporary_list_no_warn'});
560     }
561
562     # Check for a cookie disabling the warning.
563     $warn = 0 if ($warn && $cgi->cookie('no_temp_list_warn'));
564
565     return $warn;
566 }
567
568 1;