1 package OpenILS::WWW::EGCatLoader;
6 use OpenSRF::Utils::Logger qw/$logger/;
7 use OpenILS::Utils::CStoreEditor qw/:funcs/;
8 use OpenILS::Utils::Fieldmapper;
9 use OpenILS::Utils::Normalize qw/search_normalize/;
10 use OpenILS::Application::AppUtils;
11 use OpenSRF::Utils::JSON;
12 use OpenSRF::Utils::Cache;
13 use OpenSRF::Utils::SettingsClient;
15 use Digest::MD5 qw/md5_hex/;
16 use Apache2::Const -compile => qw/OK/;
19 #$Data::Dumper::Indent = 0;
21 my $U = 'OpenILS::Application::AppUtils';
25 # Plain procedural functions start here.
27 sub _init_browse_cache {
28 if (not defined $browse_cache) {
29 my $conf = new OpenSRF::Utils::SettingsClient;
31 $browse_timeout = $conf->config_value(
32 "apps", "open-ils.search", "app_settings", "cache_timeout"
34 $browse_cache = new OpenSRF::Utils::Cache("global");
38 sub _get_authority_heading {
39 my ($field, $sf_lookup) = @_;
43 map { $_->[1] } grep { $sf_lookup->{$_->[0]} } $field->subfields
47 # Object methods start here.
50 # Returns cache key and a list of parameters for DB proc metabib.browse().
51 sub prepare_browse_parameters {
54 no warnings 'uninitialized';
56 # XXX TODO add config.global_flag rows for browse limit-limit and
57 # browse offset-limit?
59 my $limit = int($self->cgi->param('blimit') || 10);
60 my $offset = int($self->cgi->param('boffset') || 0);
61 my $force_backward = scalar($self->cgi->param('bback'));
64 scalar($self->cgi->param('qtype')),
65 scalar($self->cgi->param('bterm')),
66 $self->ctx->{copy_location_group_org} ||
67 $self->ctx->{aou_tree}->()->id,
68 $self->ctx->{copy_location_group},
69 $self->ctx->{is_staff} ? 't' : 'f',
70 scalar($self->cgi->param('bpivot')),
71 $force_backward ? 't' : 'f'
74 # We do need $limit, $offset, and $force_backward as part of the
75 # cache key, but we also need to keep them separate from other
76 # parameters for purposes of paging link generation.
78 "oils_browse_" . md5_hex(
79 OpenSRF::Utils::JSON->perl2JSON(
80 [@params, $limit, $offset, $force_backward]
83 $limit, $offset, $force_backward, @params
87 # Break out any Public General Notes (field 680) for display and
88 # hyperlinking. These are sometimes (erroneously?) called "scope notes."
89 # I say erroneously, tentatively, because LoC doesn't seem to document
90 # a "scope notes" field for authority records, while it does so for
91 # classification records, which are something else. But I am not a
93 sub extract_public_general_notes {
94 my ($self, $record, $row) = @_;
97 foreach my $note ($record->field('680')) {
101 foreach my $subfield ($note->subfields) {
102 my ($code, $value) = @$subfield;
106 } elsif ($code eq '5') {
108 my $org = $self->ctx->{get_aou_by_shortname}->($value);
109 $last_heading->{org_id} = $org->id if $org;
111 push @note, { institution => $value };
112 } elsif ($code eq 'a') {
114 heading => $value, bterm => search_normalize($value)
116 push @note, $last_heading;
120 push @notes, \@note if @note;
123 $row->{notes} = \@notes;
126 sub find_authority_headings_and_notes {
127 my ($self, $row) = @_;
130 $self->ctx->{get_authority_fields}->($row->{control_set});
132 $row->{headings} = [];
136 $record = new_from_xml MARC::Record($row->{marc});
139 $logger->warn("Problem with MARC from authority record #" .
140 $row->{id} . ": $@");
141 return $row; # We're called in map(), so we must move on without
145 $self->extract_public_general_notes($record, $row);
147 # By applying grep in this way, we get acsaf objects that *have* and
148 # therefore *aren't* main entries, which is what we want.
149 foreach my $acsaf (grep { $_->main_entry } values(%$acsaf_table)) {
150 my @fields = $record->field($acsaf->tag);
151 my %sf_lookup = map { $_ => 1 } split("", $acsaf->display_sf_list);
154 foreach my $field (@fields) {
155 my $h = { heading => _get_authority_heading($field, \%sf_lookup) };
157 # XXX I was getting "target" from authority.authority_linking, but
158 # that makes no sense: that table can only tell you that one
159 # authority record as a whole points at another record. It does
160 # not record when a specific *field* in one authority record
161 # points to another record (not that it makes much sense for
162 # one authority record to have links to multiple others, but I can't
163 # say there definitely aren't cases for that).
165 if ($field->subfield('0') || "") =~ /(^|\))(\d+)$/;
170 push @{$row->{headings}}, {$acsaf->id => \@headings} if @headings;
176 sub map_authority_headings_to_results {
177 my ($self, $linked, $results, $auth_ids) = @_;
179 # Use the linked authority records' control sets to find and pick
180 # out non-main-entry headings. Build the headings and make a
181 # combined data structure for the template's use.
182 my %linked_headings_by_auth_id = map {
183 $_->{id} => $self->find_authority_headings_and_notes($_)
186 # Graft this authority heading data onto our main result set at the
187 # "authorities" column.
188 foreach my $row (@$results) {
189 $row->{authorities} = [
190 map { $linked_headings_by_auth_id{$_} } @{$row->{authorities}}
194 # Get linked-bib counts for each of those authorities, and put THAT
195 # information into place in the data structure.
196 my $counts = $self->editor->json_query({
199 {column => "id", transform => "count",
200 alias => "count", aggregate => 1},
209 $U->unique_unnested_numbers(map { $_->{target} } @$linked)
215 my %auth_counts = map { $_->{authority} => $_->{count} } @$counts;
217 # Soooo nesty! We look for places where we'll need a count of bibs
218 # linked to an authority record, and put it there for the template to find.
219 for my $row (@$results) {
220 for my $auth (@{$row->{authorities}}) {
221 if ($auth->{headings}) {
222 for my $outer_heading (@{$auth->{headings}}) {
223 for my $heading_blob (@{(values %$outer_heading)[0]}) {
224 if ($heading_blob->{target}) {
225 $heading_blob->{target_count} =
226 $auth_counts{$heading_blob->{target}};
235 # flesh_browse_results() attaches data from authority records. It
236 # changes $results and returns 1 for success, undef for failure (in which
237 # case $self->editor->event should always point to the reason for failure).
238 # $results must be an arrayref of result rows from the DB's metabib.browse()
239 sub flesh_browse_results {
240 my ($self, $results) = @_;
242 # Turn comma-seprated strings of numbers in "authorities" column
244 $_->{authorities} = [split /,/, $_->{authorities}] foreach @$results;
246 # Group them in one arrray, not worrying about dupes because we're about
247 # to use them in an IN () comparison in a SQL query.
248 my @auth_ids = map { @{$_->{authorities}} } @$results;
251 # Get all linked authority records themselves
252 my $linked = $self->editor->json_query({
254 are => [qw/id marc control_set/],
255 aalink => [{column => "target", transform => "array_agg",
262 fkey => "id", field => "source"
266 where => {"+are" => {id => \@auth_ids}}
269 $self->map_authority_headings_to_results($linked, $results, \@auth_ids);
275 sub load_browse_impl {
276 my ($self, $limit, $offset, $force_backward, @params) = @_;
278 my $inner_limit = ($offset >= 0 and not $force_backward) ?
281 my $results = $self->editor->json_query({
283 "metabib.browse", (@params, $inner_limit, $offset)
287 if (not $results) { # DB error, not empty result set.
289 "error in browse (direct): " . $self->editor->event->{textcode}
291 $self->ctx->{browse_error} = 1;
294 } elsif (not $self->flesh_browse_results($results)) {
296 "error in browse (flesh): " . $self->editor->event->{textcode}
298 $self->ctx->{browse_error} = 1;
306 # $results can be modified by this function. This would be simpler
307 # but for the moving pivot concept that helps us avoid paging with
308 # large offsets (slow).
309 sub infer_browse_paging {
310 my ($self, $results, $limit, $offset, $force_backward) = @_;
312 # (All these comments assume a default limit of 10). For typical
313 # not-backwards requests not at the end of the result set, we
314 # should have an eleventh result that tells us what's next.
315 while (scalar @$results > $limit) {
316 $self->ctx->{forward_pivot} = (pop @$results)->{browse_entry};
317 $self->ctx->{more_forward} = 1;
320 # If we're going backwards by pivot id, we don't have an eleventh
321 # result to tell us we can page forward, but we can assume we can
322 # go forward because duh, we followed a link backward to get here.
323 if ($force_backward and $self->cgi->param('bpivot')) {
324 $self->ctx->{forward_pivot} = scalar($self->cgi->param('bpivot'));
325 $self->ctx->{more_forward} = 1;
328 # The pivot that the user can use for going backwards is the first
331 $self->ctx->{back_pivot} = $results->[0]->{browse_entry};
334 # The result of these tests relate to basic limit/offset paging.
336 # This comparison for setting more_forward does not fold into
337 # those for setting more_back.
338 if ($offset < 0 || $force_backward) {
339 $self->ctx->{more_forward} = 1;
342 if ($offset > 0 or (!$force_backward and $self->cgi->param('bpivot'))) {
343 $self->ctx->{more_back} = 1;
344 } elsif (scalar @$results < $limit) {
345 $self->ctx->{more_back} = 0;
346 } elsif (not($self->cgi->param('bterm') eq '0' and
347 not defined $self->cgi->param('bpivot'))) { # paging links
348 $self->ctx->{more_back} = 1;
352 sub leading_article_test {
353 my ($self, $qtype, $bterm) = @_;
355 my $flag_name = "opac.browse.warnable_regexp_per_class";
356 my $flag = $self->ctx->{get_cgf}->($flag_name);
358 return unless $flag->enabled eq 't';
362 eval { $map = OpenSRF::Utils::JSON->JSON2perl($flag->value); };
364 $logger->warn("cgf '$flag_name' enabled but value is invalid JSON? $@");
368 # Don't crash over any of the things that could go wrong in here:
370 if ($map->{$qtype}) {
371 if ($bterm =~ qr/$map->{$qtype}/i) {
372 $self->ctx->{browse_leading_article_warning} = 1;
377 $logger->warn("cgf '$flag_name' has valid JSON in value, but: $@");
384 _init_browse_cache();
386 $self->ctx->{more_forward} = 0;
387 $self->ctx->{more_back} = 0;
389 if ($self->cgi->param('qtype') and defined $self->cgi->param('bterm')) {
391 $self->leading_article_test(
392 $self->cgi->param('qtype'),
393 $self->cgi->param('bterm')
396 my ($cache_key, $limit, $offset, $force_backward, @params) =
397 $self->prepare_browse_parameters;
399 my $results = $browse_cache->get_cache($cache_key);
401 $results = $self->load_browse_impl(
402 $limit, $offset, $force_backward, @params
405 $browse_cache->put_cache($cache_key, $results, $browse_timeout);
410 $self->infer_browse_paging(
411 $results, $limit, $offset, $force_backward
413 $self->ctx->{browse_results} = $results;
416 # We don't need an else clause to send the user a 5XX error or
417 # anything. Errors will have been logged, and $ctx will be
418 # prepared so a template can show a nicer error to the user.
421 return Apache2::Const::OK;