1 package OpenILS::Application::Serial::OPAC;
3 # This package contains methods for open-ils.serial that present data suitable
6 use base qw/OpenILS::Application/;
10 # All of the packages we might 'use' are already imported in
11 # OpenILS::Application::Serial. Only those that export symbols
12 # need to be mentioned explicitly here.
14 use OpenSRF::Utils::Logger qw/:logger/;
15 use OpenILS::Utils::CStoreEditor q/:funcs/;
17 my $U = "OpenILS::Application::AppUtils";
19 my %MFHD_SUMMARIZED_SUBFIELDS = (
20 enum => [ split //, "abcdef" ], # $g and $h intentionally omitted for now
21 chron => [ split //, "ijklm" ]
24 # This is a helper for scoped_holding_summary_tree_for_bib() a little further down
27 my ($node, $tree, $org_tree) = @_;
29 my @ancestry = reverse @{ $U->get_org_ancestors($node->{org_unit}, 1) };
30 shift @ancestry; # discard current org_unit
32 foreach (@ancestry) { # in leaf-to-root order
33 my $graft_point = _find_ou_in_holdings_tree($tree, $_);
36 push @{$graft_point->{children}}, $node;
41 holding_summaries => [],
47 # If we reach this point, we got all the way to the top of the org tree
48 # without finding corresponding nodes in $tree (holdings tree), so the
49 # latter must be empty, and we need to make $tree just contain what $node
55 # This is a helper for scoped_holding_summary_tree_for_bib() a little further down
57 sub _find_ou_in_holdings_tree {
60 return $tree if $tree->{org_unit} eq $id;
61 if (ref $tree->{children}) {
62 foreach (@{$tree->{children}}) {
63 my $maybe = _find_ou_in_holdings_tree($_, $id);
64 return $maybe if $maybe;
71 sub scoped_holding_summary_tree_for_bib {
73 $self, $client, $bib, $org_unit, $depth, $limit, $offset, $ascending
76 my $org_tree = $U->get_org_tree; # caches
78 $org_unit ||= $org_tree->id;
85 # What we want to know from this query is essentially the set of
86 # holdings related to a given bib and the org units that have said
89 # For this we would only need sasum, sdist and ssub, but
90 # because we also need to be able to page (and therefore must sort) the
91 # results we get, we need reasonable columns on which to do the sorting.
92 # So for that we join sitem (via sstr) so we can sort on the maximum
93 # date_expected (which is basically the issue pub date) for items that
94 # have been received. That maximum date_expected is actually the second
95 # sort key, however. The first is the holding lib's position in a
96 # depth-first representation of the org tree (if you think about it,
97 # paging through holdings held at diverse points in the tree only makes
98 # sense if you do it this way).
100 my $rows = $e->json_query({
102 sasum => [qw/summary_type id generated_coverage/],
103 sdist => ["holding_lib"],
105 {column => "date_expected", transform => "max", aggregate => 1}
114 join => {sitem => {}}
123 $U->get_org_descendants(int($org_unit), int($depth))
125 "+ssub" => {record_entry => int($bib)},
126 "+sitem" => {date_received => {"!=" => undef}}
128 limit => int($limit) + 1, # see comment below on "limit trick"
129 offset => int($offset),
133 field => "holding_lib",
134 transform => "actor.org_unit_simple_path",
135 params => [$org_tree->id]
139 field => "date_expected",
140 transform => "max", # to match select clause
141 direction => ($ascending ? "ASC" : "DESC")
144 }) or return $e->die_event;
148 # Now we build a tree out of our result set.
151 # Use our "limit trick" from above to cheaply determine whether there's
152 # another page of results, for the UI's benefit. Put $more into the
153 # result hash at the very end.
155 if (scalar(@$rows) > int($limit)) {
160 foreach my $row (@$rows) {
161 my $org_node_needs_placed = 0;
163 _find_ou_in_holdings_tree($result, $row->{holding_lib});
166 $org_node_needs_placed = 1;
168 org_unit => $row->{holding_lib},
169 holding_summaries => [],
174 # Make a very simple object for a single holding summary.
175 # generated_coverage is stored as JSON, and here we can unpack it.
178 summary_type => $row->{summary_type},
179 generated_coverage =>
180 OpenSRF::Utils::JSON->JSON2perl($row->{generated_coverage})
183 push @{$org_node->{holding_summaries}}, $summary;
185 if ($org_node_needs_placed) {
186 _place_org_node($org_node, $result, $org_tree);
190 $result->{more} = $more;
194 __PACKAGE__->register_method(
195 method => "scoped_holding_summary_tree_for_bib",
196 api_name => "open-ils.serial.holding_summary_tree.by_bib",
200 desc => 'Return a set of holding summaries organized into a tree
201 of nodes that look like:
202 {org_unit:<id>, holding_summaries:[], children:[]}
204 The root node has an extra key: "more". Its value is 1 if there
205 are more pages (in the limit/offset sense) of results that the caller
206 could potentially fetch.
208 All arguments except the first (bibid) are optional.
212 desc => "ID of the bre to which holdings belong",
215 { name => "org_unit", type => "number" },
216 { name => "depth (default 0)", type => "number" },
217 { name => "limit (default 10)", type => "number" },
218 { name => "offset (default 0)", type => "number" },
219 { name => "ascending (default false)", type => "boolean" },
224 # This is a helper for grouped_holdings_for_summary() later.
225 sub _label_holding_level {
226 my ($pattern_field, $subfield, $value, $mfhd_cache) = @_;
228 # This is naïve, in that a-f are sometimes chron fields and not enum.
229 # OpenILS::Utils::MFHD understands that, but so far I don't think our
232 my $cache_key = $subfield . $value;
234 if (not exists $mfhd_cache->{$cache_key}) {
235 my $link_id = (split(/\./, $pattern_field->subfield('8')))[0];
236 my $fake_holding = new MFHD::Holding(
238 new MARC::Field('863', '4', '1', '8', "$link_id.1"),
239 new MFHD::Caption($pattern_field->clone)
242 if ($subfield ge 'i') { # chron
243 $mfhd_cache->{$cache_key} = $fake_holding->format_single_chron(
244 {$subfield => $value}, $subfield, 1, 1
247 $mfhd_cache->{$cache_key} = $fake_holding->format_single_enum(
248 {$subfield => $value}, $subfield, 1
253 return $mfhd_cache->{$cache_key};
256 # This is a helper for grouped_holdings_for_summary() later.
257 sub _get_deepest_holding_level {
258 my ($display_grouping, $pattern_field) = @_;
260 my @present = grep { $pattern_field->subfield($_) } @{
261 $MFHD_SUMMARIZED_SUBFIELDS{$display_grouping}
267 # This is a helper for grouped_holdings_for_summary() later.
268 sub _opac_visible_unit_data {
269 my ($issuance_id_list, $dist_id, $staff, $e) = @_;
271 return {} unless @$issuance_id_list;
273 my $rows = $e->json_query(
274 $U->basic_opac_copy_query(
275 undef, $issuance_id_list, $dist_id,
276 1000, 0, # XXX no mechanism for users to page at this level yet
279 ) or return $e->die_event;
283 # Take the list of rows returned from json_query() and sort results into
284 # several smaller lists stored in a hash keyed by issuance ID.
285 foreach my $row (@$rows) {
286 $results->{$row->{issuance}} = [] unless
287 exists $results->{$row->{issuance}};
288 push @{ $results->{$row->{issuance}} }, $row;
294 # This is a helper for grouped_holdings_for_summary() later.
295 sub _make_grouped_holding_node {
297 $row, $subfield, $deepest_level, $pattern_field,
298 $unit_data, $mfhd_cache
302 $subfield eq $deepest_level ? (
303 label => $row->{label},
304 holding => $row->{id},
305 ($unit_data ? (units => ($unit_data->{$row->{id}} || [])) : ())
307 value => $row->{value},
308 label => _label_holding_level(
309 $pattern_field, $subfield, $row->{value}, $mfhd_cache
315 # This is a helper for grouped_holdings_for_summary() later.
316 sub _make_single_level_grouped_holding_query {
318 $subfield, $deepest_level, $summary_hint, $summary_id,
319 $subfield_joins, $subfield_where_clauses,
325 sstr => ["distribution"],
326 "smhc_$subfield" => ["value"], (
327 $subfield eq $deepest_level ?
328 (siss => [qw/id label date_published/]) : ()
340 join => {%$subfield_joins}
351 "+$summary_hint" => {id => $summary_id},
352 "+sitem" => {date_received => {"!=" => undef}},
353 %$subfield_where_clauses
355 distinct => 1, # sic, this goes here in json_query
356 limit => int($limit) + 1,
357 offset => int(shift(@$offsets)),
359 "smhc_$subfield" => {
361 direction => ($subfield eq $deepest_level ? "asc" : "desc")
368 sub grouped_holdings_for_summary {
370 $self, $client, $summary_type, $summary_id,
371 $expand_path, $limit, $offsets, $auto_expand_first, $with_units
374 # Validate input or set defaults.
375 ($summary_type .= "") =~ s/[^\w]//g;
376 $summary_id = int($summary_id);
379 $limit = 12 if $limit < 1;
382 foreach ($expand_path, $offsets) {
383 if (ref $_ ne 'ARRAY') {
384 return new OpenILS::Event(
385 "BAD_PARAMS", note =>
386 "'expand_path' and 'offsets' arguments must be arrays"
391 if (scalar(@$offsets) != scalar(@$expand_path) + 1) {
392 return new OpenILS::Event(
393 "BAD_PARAMS", note =>
394 "'offsets' array must be one element longer than 'expand_path'"
398 # Get the class hint for whichever type of summary we're expanding.
399 my $fmclass = "Fieldmapper::serial::${summary_type}_summary";
400 my $summary_hint = $Fieldmapper::fieldmap->{$fmclass}{hint} or
401 return new OpenILS::Event("BAD_PARAMS", note => "summary_type");
405 # First, get display grouping for requested summary (either chron or enum)
406 # and the pattern code. Even though we have to JOIN through sitem to get
407 # pattern_code from scap, we don't actually care about specific items yet.
408 my $row = $e->json_query({
409 select => {sdist => ["display_grouping"], scap => ["pattern_code"]},
430 "+$summary_hint" => {id => $summary_id},
431 "+sitem" => {date_received => {"!=" => undef}}
434 }) or return $e->die_event;
436 # Summaries without attached holdings constitute bad data, not benign
438 return new OpenILS::Event(
440 note => "Summary #$summary_id not found, or no holdings attached"
443 # Unless data has been disarranged, all holdings grouped together under
444 # the same summary should have the same pattern code, so we can take any
445 # result from the set we just got.
448 $pattern_field = new MARC::Field(
449 "853", # irrelevant for our purposes
450 @{ OpenSRF::Utils::JSON->JSON2perl($row->[0]->{pattern_code}) }
454 return new OpenILS::Event("SERIAL_CORRUPT_PATTERN_CODE", note => $@);
457 # And now we know which subfields we will care about from
458 # serial.materialized_holding_code.
459 my $display_grouping = $row->[0]->{display_grouping};
461 # This will tell us when to stop grouping and start showing actual
464 _get_deepest_holding_level($display_grouping, $pattern_field);
465 if (not defined $deepest_level) {
466 # corrupt pattern code
467 my $msg = "couldn't determine deepest holding level for " .
468 "$summary_type summary #$summary_id";
470 return new OpenILS::Event("SERIAL_CORRUPT_PATTERN_CODE", note => $msg);
473 my @subfields = @{ $MFHD_SUMMARIZED_SUBFIELDS{$display_grouping} };
475 # We look for holdings grouped at the top level once no matter what,
476 # then we'll look deeper with additional queries for every element of
477 # $expand_path later.
478 # Below we define parts of the SELECT and JOIN clauses that we'll
479 # potentially reuse if $expand_path has elements.
481 my $subfield = shift @subfields;
482 my %subfield_joins = ("smhc_$subfield" => {class => "smhc"});
483 my %subfield_where_clauses = ("+smhc_$subfield" => {subfield => $subfield});
485 # Now get the top level of holdings.
486 my $top = $e->json_query(
487 _make_single_level_grouped_holding_query(
488 $subfield, $deepest_level, $summary_hint, $summary_id,
489 \%subfield_joins, \%subfield_where_clauses,
492 ) or return $e->die_event;
494 # Deal with the extra row, if present, that tells are there are more pages
497 if (scalar(@$top) > int($limit)) {
502 # Distribution is the same for all rows anyway, but we may need it for a
504 my $dist_id = @$top ? $top->[0]->{distribution} : undef;
506 # This will help us avoid certain repetitive calculations. Examine
507 # _label_holding_level() to see what I mean.
510 # Prepare related unit data if appropriate.
513 if ($with_units and $subfield eq $deepest_level) {
514 $unit_data = _opac_visible_unit_data(
515 [map { $_->{id} } @$top], $dist_id, $with_units > 1, $e
517 return $unit_data if defined $U->event_code($unit_data);
520 # Make the tree we have so far.
522 { display_grouping => $display_grouping,
523 caption => $pattern_field->subfield($subfield) },
525 _make_grouped_holding_node(
526 $_, $subfield, $deepest_level, $pattern_field,
527 $unit_data, $mfhd_cache
531 ($top_more ? undef : ())
534 # We'll need a parent reference at each level as we descend.
537 # Will we be trying magic auto-expansion of the first top-level grouping?
538 if ($auto_expand_first and @$tree and not @$expand_path) {
539 $expand_path = [$tree->[1]->{value}];
543 # Ok, that got us the top level, with nothing expanded. Now we loop through
544 # the elements of @$expand_path, issuing similar queries to get us deeper
545 # groupings and even actual specific holdings.
546 foreach my $value (@$expand_path) {
547 my $prev_subfield = $subfield;
548 $subfield = shift @subfields;
550 # This wad of JOINs is additive over each iteration.
551 $subfield_joins{"smhc_$subfield"} = {class => "smhc"};
553 # The WHERE clauses also change and grow each time.
554 $subfield_where_clauses{"+smhc_$prev_subfield"}->{value} = $value;
555 $subfield_where_clauses{"+smhc_$subfield"}->{subfield} = $subfield;
557 my $level = $e->json_query(
558 _make_single_level_grouped_holding_query(
559 $subfield, $deepest_level, $summary_hint, $summary_id,
560 \%subfield_joins, \%subfield_where_clauses,
563 ) or return $e->die_event;
565 return $tree unless @$level;
567 # Deal with the extra row, if present, that tells are there are more
570 if (scalar(@$level) > int($limit)) {
575 # Find attachment point for our results.
576 my ($point) = grep { ref $_ and $_->{value} eq $value } @$parent;
578 # Prepare related unit data if appropriate.
579 if ($with_units and $subfield eq $deepest_level) {
580 $unit_data = _opac_visible_unit_data(
581 [map { $_->{id} } @$level], $dist_id, $with_units > 1, $e
583 return $unit_data if defined $U->event_code($unit_data);
586 # Set parent for the next iteration.
587 $parent = $point->{children} = [
588 { display_grouping => $display_grouping,
589 caption => $pattern_field->subfield($subfield) },
591 _make_grouped_holding_node(
592 $_, $subfield, $deepest_level, $pattern_field,
593 $unit_data, $mfhd_cache
597 ($level_more ? undef : ())
600 last if $subfield eq $deepest_level;
606 __PACKAGE__->register_method(
607 method => "grouped_holdings_for_summary",
608 api_name => "open-ils.serial.holdings.grouped_by_summary",
612 desc => q/Return a tree of holdings associated with a given summary
613 grouped by all but the last of either chron or enum units./,
615 { name => "summary_type", type => "string" },
616 { name => "summary_id", type => "number" },
617 { name => "expand_path", type => "array",
618 desc => "In root-to-leaf order, the values of the nodes along the axis you want to expand" },
619 { name => "limit (default 12)", type => "number" },
620 { name => "offsets", type => "array", desc =>
621 "This must be exactly one element longer than expand_path" },
622 { name => "auto_expand_first", type => "boolean", desc =>
623 "Only if expand_path is empty, automatically expand first top-level grouping" },
624 { name => "with_units", type => "number", desc => q/
625 If true at all, for each holding, if there are associated units,
626 add some information about them to the result tree. These units
627 will be filtered by OPAC visibility unless you provide a value
632 1 = opac visible units,
633 2 = all units (i.e. staff view)