<field reporter:label="Bind Unit Template" name="bind_unit_template" reporter:datatype="link"/>
<field reporter:label="Unit Label Prefix" name="unit_label_prefix" reporter:datatype="text"/>
<field reporter:label="Unit Label Suffix" name="unit_label_suffix" reporter:datatype="text"/>
+ <field reporter:label="Display Grouping" name="display_grouping" reporter:datatype="text"/>
<field reporter:label="Streams" name="streams" oils_persist:virtual="true" reporter:datatype="link"/>
<field reporter:label="Notes" name="notes" oils_persist:virtual="true" reporter:datatype="link"/>
<field reporter:label="Basic Issue Summary" name="basic_summary" oils_persist:virtual="true" reporter:datatype="link"/>
</links>
<!-- Not available via PCRUD at this time -->
</class>
-
+ <class id="sasum" controller="open-ils.cstore" oils_obj:fieldmapper="serial::any_summary" oils_persist:tablename="serial.any_summary" reporter:label="All Issues' Summaries" oils_persist:readonly="true">
+ <fields>
+ <field name="summary_type" reporter:label="Summary Type" reporter:datatype="text" />
+ <field name="id" reporter:label="Native ID" reporter:datatype="int" /><!-- not datatype="id", because id is not unique in this view -->
+ <field name="distribution" reporter:label="Distribution" reporter:datatype="link" />
+ <field name="generated_coverage" reporter:label="Generated Coverage" reporter:datatype="text" />
+ <field name="show_generated" reporter:label="Show Generated?" reporter:datatype="bool" />
+ </fields>
+ <links>
+ <link field="distribution" reltype="has_a" key="id" map="" class="sdist"/>
+ </links>
+ </class>
+ <class id="smhc" controller="open-ils.cstore" oils_obj:fieldmapper="serial::materialized_holding_code" oils_persist:tablename="serial.materialized_holding_code" reporter:label="Materialized Holding Code" oils_persist:readonly="true">
+ <fields oils_persist:primary="id" oils_persist:sequence="serial.materialized_holding_code_id_seq">
+ <field name="id" reporter:label="ID" reporter:datatype="id" />
+ <field name="issuance" reporter:label="Issuance" reporter:datatype="link" />
+ <field name="holding_type" reporter:label="Holding Type" reporter:datatype="text" />
+ <field name="ind1" reporter:label="First Indicator" reporter:datatype="text" />
+ <field name="ind2" reporter:label="Second Indicator" reporter:datatype="text" />
+ <field name="subfield" reporter:label="Subfield" reporter:datatype="text" />
+ <field name="value" reporter:label="Value" reporter:datatype="text" oils_obj:validate="^\w$" />
+ </fields>
+ <links>
+ <link field="issuance" reltype="has_a" key="id" map="" class="siss"/>
+ </links>
+ </class>
<class id="sbsum" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="serial::basic_summary" oils_persist:tablename="serial.basic_summary" reporter:label="Basic Issue Summary">
<fields oils_persist:primary="id" oils_persist:sequence="serial.basic_summary_id_seq">
<field reporter:label="ID" name="id" reporter:datatype="id" />
<desc xml:lang="en-US">Checkin attempted on item during minimum transit checkin interval.</desc>
</event>
+ <event code='11104' textcode='SERIAL_CORRUPT_PATTERN_CODE'>
+ <desc xml:lang="en-US">A serial pattern code has been configured
+ that fails to conform to MFHD standards for fields 853-855.</desc>
+ </event>
+
<!-- ================================================================ -->
</ils_events>
}
sub get_org_ancestors {
- my($self, $org_id) = @_;
+ my($self, $org_id, $use_cache) = @_;
+
+ my ($cache, $orgs);
+
+ if ($use_cache) {
+ $cache = OpenSRF::Utils::Cache->new("global", 0);
+ $orgs = $cache->get_cache("org.ancestors.$org_id");
+ return $orgs if $orgs;
+ }
my $org_list = OpenILS::Utils::CStoreEditor->new->json_query({
select => {
where => {id => $org_id}
});
- my @orgs;
- push(@orgs, $_->{id}) for @$org_list;
- return \@orgs;
+ $orgs = [ map { $_->{id} } @$org_list ];
+
+ $cache->put_cache("org.ancestors.$org_id", $orgs) if $use_cache;
+ return $orgs;
}
sub get_org_full_path {
return undef;
}
+# I hate to put this here exactly, but this code needs to be shared between
+# the TPAC's mod_perl module and open-ils.serial.
+#
+# There is a reason every part of the query *except* those parts dealing
+# with scope are moved here from the code's origin in TPAC. The serials
+# use case does *not* want the same scoping logic.
+#
+# Also, note that for the serials uses case, we may filter in OPAC visible
+# status and copy/call_number deletedness, but we don't filter on any
+# particular values for serial.item.status or serial.item.date_received.
+# Since we're only using this *after* winnowing down the set of issuances
+# that copies should be related to, I'm not sure we need any such serial.item
+# filters.
+
+sub basic_opac_copy_query {
+ ######################################################################
+ # Pass a defined value for either $rec_id OR ($iss_id AND $dist_id), #
+ # not both. #
+ ######################################################################
+ my ($self,$rec_id,$iss_id,$dist_id,$copy_limit,$copy_offset,$staff) = @_;
+
+ return {
+ select => {
+ acp => ['id', 'barcode', 'circ_lib', 'create_date',
+ 'age_protect', 'holdable'],
+ acpl => [
+ {column => 'name', alias => 'copy_location'},
+ {column => 'holdable', alias => 'location_holdable'}
+ ],
+ ccs => [
+ {column => 'name', alias => 'copy_status'},
+ {column => 'holdable', alias => 'status_holdable'}
+ ],
+ acn => [
+ {column => 'label', alias => 'call_number_label'},
+ {column => 'id', alias => 'call_number'}
+ ],
+ circ => ['due_date'],
+ acnp => [
+ {column => 'label', alias => 'call_number_prefix_label'},
+ {column => 'id', alias => 'call_number_prefix'}
+ ],
+ acns => [
+ {column => 'label', alias => 'call_number_suffix_label'},
+ {column => 'id', alias => 'call_number_suffix'}
+ ],
+ bmp => [
+ {column => 'label', alias => 'part_label'},
+ ],
+ ($iss_id ? (sitem => ["issuance"]) : ())
+ },
+
+ from => {
+ acp => {
+ ($iss_id ? (
+ sitem => {
+ fkey => 'id',
+ field => 'unit',
+ filter => {issuance => $iss_id},
+ join => {
+ sstr => { }
+ }
+ }
+ ) : ()),
+ acn => {
+ join => {
+ acnp => { fkey => 'prefix' },
+ acns => { fkey => 'suffix' }
+ },
+ filter => [
+ {deleted => 'f'},
+ ($rec_id ? {record => $rec_id} : ())
+ ],
+ },
+ circ => { # If the copy is circulating, retrieve the open circ
+ type => 'left',
+ filter => {checkin_time => undef}
+ },
+ acpl => {
+ ($staff ? () : (filter => { opac_visible => 't' }))
+ },
+ ccs => {
+ ($staff ? () : (filter => { opac_visible => 't' }))
+ },
+ aou => {},
+ acpm => {
+ type => 'left',
+ join => {
+ bmp => { type => 'left' }
+ }
+ }
+ }
+ },
+
+ where => {
+ '+acp' => {
+ deleted => 'f',
+ ($staff ? () : (opac_visible => 't'))
+ },
+ ($dist_id ? ( '+sstr' => { distribution => $dist_id } ) : ()),
+ ($staff ? () : ( '+aou' => { opac_visible => 't' } ))
+ },
+
+ order_by => [
+ {class => 'aou', field => 'name'},
+ {class => 'acn', field => 'label'}
+ ],
+
+ limit => $copy_limit,
+ offset => $copy_offset
+ };
+}
+
1;
use OpenILS::Utils::MFHD;
use DateTime::Format::ISO8601;
use MARC::File::XML (BinaryEncoding => 'utf8');
+
+use OpenILS::Application::Serial::OPAC;
+
my $U = 'OpenILS::Application::AppUtils';
my @MFHD_NAMES = ('basic','supplement','index');
my %MFHD_NAMES_BY_TAG = ( '853' => $MFHD_NAMES[0],
}
sub received_siss_by_bib {
+ # XXX this is somewhat wrong in implementation and should not be used in
+ # new places - senator
my $self = shift;
my $client = shift;
my $bib = shift;
sub scoped_bib_holdings_summary {
+ # XXX this is somewhat wrong in implementation and should not be used in
+ # new places - senator
my $self = shift;
my $client = shift;
my $bibid = shift;
api_level => 1,
argc => 1,
signature => {
- desc => 'Receives a Bib ID and other optional params and returns set of holdings statements',
+ desc => '** DEPRECATED and only used by JSPAC. Somewhat wrong in implementation. *** Receives a Bib ID and other optional params and returns set of holdings statements',
params => [
{ name => 'bibid',
desc => 'id of the bre to which the issuances belong',
--- /dev/null
+package OpenILS::Application::Serial::OPAC;
+
+# This package contains methods for open-ils.serial that present data suitable
+# for OPAC display.
+
+use base qw/OpenILS::Application/;
+use strict;
+use warnings;
+
+# All of the packages we might 'use' are already imported in
+# OpenILS::Application::Serial. Only those that export symbols
+# need to be mentioned explicitly here.
+
+use OpenSRF::Utils::Logger qw/:logger/;
+use OpenILS::Utils::CStoreEditor q/:funcs/;
+
+my $U = "OpenILS::Application::AppUtils";
+
+my %MFHD_SUMMARIZED_SUBFIELDS = (
+ enum => [ split //, "abcdef" ], # $g and $h intentionally omitted for now
+ chron => [ split //, "ijklm" ]
+);
+
+# This is a helper for scoped_holding_summary_tree_for_bib() a little further down
+
+sub _place_org_node {
+ my ($node, $tree, $org_tree) = @_;
+
+ my @ancestry = reverse @{ $U->get_org_ancestors($node->{org_unit}, 1) };
+ shift @ancestry; # discard current org_unit
+
+ foreach (@ancestry) { # in leaf-to-root order
+ my $graft_point = _find_ou_in_holdings_tree($tree, $_);
+
+ if ($graft_point) {
+ push @{$graft_point->{children}}, $node;
+ return;
+ } else {
+ $node = {
+ org_unit => $_,
+ holding_summaries => [],
+ children => [$node]
+ }
+ }
+ }
+
+ # If we reach this point, we got all the way to the top of the org tree
+ # without finding corresponding nodes in $tree (holdings tree), so the
+ # latter must be empty, and we need to make $tree just contain what $node
+ # contains.
+
+ %$tree = %$node;
+}
+
+# This is a helper for scoped_holding_summary_tree_for_bib() a little further down
+
+sub _find_ou_in_holdings_tree {
+ my ($tree, $id) = @_;
+
+ return $tree if $tree->{org_unit} eq $id;
+ if (ref $tree->{children}) {
+ foreach (@{$tree->{children}}) {
+ my $maybe = _find_ou_in_holdings_tree($_, $id);
+ return $maybe if $maybe;
+ }
+ }
+
+ return;
+}
+
+sub scoped_holding_summary_tree_for_bib {
+ my (
+ $self, $client, $bib, $org_unit, $depth, $limit, $offset, $ascending
+ ) = @_;
+
+ my $org_tree = $U->get_org_tree; # caches
+
+ $org_unit ||= $org_tree->id;
+ $depth ||= 0;
+ $limit ||= 10;
+ $offset ||= 0;
+
+ my $e = new_editor;
+
+ # What we want to know from this query is essentially the set of
+ # holdings related to a given bib and the org units that have said
+ # holdings.
+
+ # For this we would only need sasum, sdist and ssub, but
+ # because we also need to be able to page (and therefore must sort) the
+ # results we get, we need reasonable columns on which to do the sorting.
+ # So for that we join sitem (via sstr) so we can sort on the maximum
+ # date_expected (which is basically the issue pub date) for items that
+ # have been received. That maximum date_expected is actually the second
+ # sort key, however. The first is the holding lib's position in a
+ # depth-first representation of the org tree (if you think about it,
+ # paging through holdings held at diverse points in the tree only makes
+ # sense if you do it this way).
+
+ my $rows = $e->json_query({
+ select => {
+ sasum => [qw/summary_type id generated_coverage/],
+ sdist => ["holding_lib"],
+ sitem => [
+ {column => "date_expected", transform => "max", aggregate => 1}
+ ]
+ },
+ from => {
+ sasum => {
+ sdist => {
+ join => {
+ ssub => {},
+ sstr => {
+ join => {sitem => {}}
+ },
+ }
+ }
+ }
+ },
+ where => {
+ "+sdist" => {
+ holding_lib =>
+ $U->get_org_descendants(int($org_unit), int($depth))
+ },
+ "+ssub" => {record_entry => int($bib)},
+ "+sitem" => {date_received => {"!=" => undef}}
+ },
+ limit => int($limit) + 1, # see comment below on "limit trick"
+ offset => int($offset),
+ order_by => [
+ {
+ class => "sdist",
+ field => "holding_lib",
+ transform => "actor.org_unit_simple_path",
+ params => [$org_tree->id]
+ },
+ {
+ class => "sitem",
+ field => "date_expected",
+ transform => "max", # to match select clause
+ direction => ($ascending ? "ASC" : "DESC")
+ }
+ ],
+ }) or return $e->die_event;
+
+ $e->disconnect;
+
+ # Now we build a tree out of our result set.
+ my $result = {};
+
+ # Use our "limit trick" from above to cheaply determine whether there's
+ # another page of results, for the UI's benefit. Put $more into the
+ # result hash at the very end.
+ my $more = 0;
+ if (scalar(@$rows) > int($limit)) {
+ $more = 1;
+ pop @$rows;
+ }
+
+ foreach my $row (@$rows) {
+ my $org_node_needs_placed = 0;
+ my $org_node =
+ _find_ou_in_holdings_tree($result, $row->{holding_lib});
+
+ if (not $org_node) {
+ $org_node_needs_placed = 1;
+ $org_node = {
+ org_unit => $row->{holding_lib},
+ holding_summaries => [],
+ children => []
+ };
+ }
+
+ # Make a very simple object for a single holding summary.
+ # generated_coverage is stored as JSON, and here we can unpack it.
+ my $summary = {
+ id => $row->{id},
+ summary_type => $row->{summary_type},
+ generated_coverage =>
+ OpenSRF::Utils::JSON->JSON2perl($row->{generated_coverage})
+ };
+
+ push @{$org_node->{holding_summaries}}, $summary;
+
+ if ($org_node_needs_placed) {
+ _place_org_node($org_node, $result, $org_tree);
+ }
+ }
+
+ $result->{more} = $more;
+ return $result;
+}
+
+__PACKAGE__->register_method(
+ method => "scoped_holding_summary_tree_for_bib",
+ api_name => "open-ils.serial.holding_summary_tree.by_bib",
+ api_level => 1,
+ argc => 6,
+ signature => {
+ desc => 'Return a set of holding summaries organized into a tree
+ of nodes that look like:
+ {org_unit:<id>, holding_summaries:[], children:[]}
+
+ The root node has an extra key: "more". Its value is 1 if there
+ are more pages (in the limit/offset sense) of results that the caller
+ could potentially fetch.
+
+ All arguments except the first (bibid) are optional.
+ ',
+ params => [
+ { name => "bibid",
+ desc => "ID of the bre to which holdings belong",
+ type => "number"
+ },
+ { name => "org_unit", type => "number" },
+ { name => "depth (default 0)", type => "number" },
+ { name => "limit (default 10)", type => "number" },
+ { name => "offset (default 0)", type => "number" },
+ { name => "ascending (default false)", type => "boolean" },
+ ]
+ }
+);
+
+# This is a helper for grouped_holdings_for_summary() later.
+sub _label_holding_level {
+ my ($pattern_field, $subfield, $value, $mfhd_cache) = @_;
+
+ # This is naïve, in that a-f are sometimes chron fields and not enum.
+ # OpenILS::Utils::MFHD understands that, but so far I don't think our
+ # interfaces do.
+
+ my $cache_key = $subfield . $value;
+
+ if (not exists $mfhd_cache->{$cache_key}) {
+ my $link_id = (split(/\./, $pattern_field->subfield('8')))[0];
+ my $fake_holding = new MFHD::Holding(
+ 1,
+ new MARC::Field('863', '4', '1', '8', "$link_id.1"),
+ new MFHD::Caption($pattern_field->clone)
+ );
+
+ if ($subfield ge 'i') { # chron
+ $mfhd_cache->{$cache_key} = $fake_holding->format_single_chron(
+ {$subfield => $value}, $subfield, 1, 1
+ );
+ } else { # enum
+ $mfhd_cache->{$cache_key} = $fake_holding->format_single_enum(
+ {$subfield => $value}, $subfield, 1
+ );
+ }
+ }
+
+ return $mfhd_cache->{$cache_key};
+}
+
+# This is a helper for grouped_holdings_for_summary() later.
+sub _get_deepest_holding_level {
+ my ($display_grouping, $pattern_field) = @_;
+
+ my @present = grep { $pattern_field->subfield($_) } @{
+ $MFHD_SUMMARIZED_SUBFIELDS{$display_grouping}
+ };
+
+ return pop @present;
+}
+
+# This is a helper for grouped_holdings_for_summary() later.
+sub _opac_visible_unit_data {
+ my ($issuance_id_list, $dist_id, $staff, $e) = @_;
+
+ return {} unless @$issuance_id_list;
+
+ my $rows = $e->json_query(
+ $U->basic_opac_copy_query(
+ undef, $issuance_id_list, $dist_id,
+ 1000, 0, # XXX no mechanism for users to page at this level yet
+ $staff
+ )
+ ) or return $e->die_event;
+
+ my $results = {};
+
+ # Take the list of rows returned from json_query() and sort results into
+ # several smaller lists stored in a hash keyed by issuance ID.
+ foreach my $row (@$rows) {
+ $results->{$row->{issuance}} = [] unless
+ exists $results->{$row->{issuance}};
+ push @{ $results->{$row->{issuance}} }, $row;
+ }
+
+ return $results;
+}
+
+# This is a helper for grouped_holdings_for_summary() later.
+sub _make_grouped_holding_node {
+ my (
+ $row, $subfield, $deepest_level, $pattern_field,
+ $unit_data, $mfhd_cache
+ ) = @_;
+
+ return {
+ $subfield eq $deepest_level ? (
+ label => $row->{label},
+ holding => $row->{id},
+ ($unit_data ? (units => ($unit_data->{$row->{id}} || [])) : ())
+ ) : (
+ value => $row->{value},
+ label => _label_holding_level(
+ $pattern_field, $subfield, $row->{value}, $mfhd_cache
+ )
+ )
+ };
+}
+
+# This is a helper for grouped_holdings_for_summary() later.
+sub _make_single_level_grouped_holding_query {
+ my (
+ $subfield, $deepest_level, $summary_hint, $summary_id,
+ $subfield_joins, $subfield_where_clauses,
+ $limit, $offsets
+ ) = @_;
+
+ return {
+ select => {
+ sstr => ["distribution"],
+ "smhc_$subfield" => ["value"], (
+ $subfield eq $deepest_level ?
+ (siss => [qw/id label date_published/]) : ()
+ )
+ },
+ from => {
+ $summary_hint => {
+ sdist => {
+ join => {
+ sstr => {
+ join => {
+ sitem => {
+ join => {
+ siss => {
+ join => {%$subfield_joins}
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ where => {
+ "+$summary_hint" => {id => $summary_id},
+ "+sitem" => {date_received => {"!=" => undef}},
+ %$subfield_where_clauses
+ },
+ distinct => 1, # sic, this goes here in json_query
+ limit => int($limit) + 1,
+ offset => int(shift(@$offsets)),
+ order_by => {
+ "smhc_$subfield" => {
+ "value" => {
+ direction => ($subfield eq $deepest_level ? "asc" : "desc")
+ }
+ }
+ }
+ };
+}
+
+sub grouped_holdings_for_summary {
+ my (
+ $self, $client, $summary_type, $summary_id,
+ $expand_path, $limit, $offsets, $auto_expand_first, $with_units
+ ) = @_;
+
+ # Validate input or set defaults.
+ ($summary_type .= "") =~ s/[^\w]//g;
+ $summary_id = int($summary_id);
+ $expand_path ||= [];
+ $limit ||= 10;
+ $limit = 10 if $limit < 1;
+ $offsets ||= [0];
+
+ foreach ($expand_path, $offsets) {
+ if (ref $_ ne 'ARRAY') {
+ return new OpenILS::Event(
+ "BAD_PARAMS", note =>
+ "'expand_path' and 'offsets' arguments must be arrays"
+ );
+ }
+ }
+
+ if (scalar(@$offsets) != scalar(@$expand_path) + 1) {
+ return new OpenILS::Event(
+ "BAD_PARAMS", note =>
+ "'offsets' array must be one element longer than 'expand_path'"
+ );
+ }
+
+ # Get the class hint for whichever type of summary we're expanding.
+ my $fmclass = "Fieldmapper::serial::${summary_type}_summary";
+ my $summary_hint = $Fieldmapper::fieldmap->{$fmclass}{hint} or
+ return new OpenILS::Event("BAD_PARAMS", note => "summary_type");
+
+ my $e = new_editor;
+
+ # First, get display grouping for requested summary (either chron or enum)
+ # and the pattern code. Even though we have to JOIN through sitem to get
+ # pattern_code from scap, we don't actually care about specific items yet.
+ my $row = $e->json_query({
+ select => {sdist => ["display_grouping"], scap => ["pattern_code"]},
+ from => {
+ $summary_hint => {
+ sdist => {
+ join => {
+ sstr => {
+ join => {
+ sitem => {
+ join => {
+ siss => {
+ join => {scap => {}}
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ where => {
+ "+$summary_hint" => {id => $summary_id},
+ "+sitem" => {date_received => {"!=" => undef}}
+ },
+ limit => 1
+ }) or return $e->die_event;
+
+ # Summaries without attached holdings constitute bad data, not benign
+ # empty result sets.
+ return new OpenILS::Event(
+ "BAD_PARAMS",
+ note => "Summary #$summary_id not found, or no holdings attached"
+ ) unless @$row;
+
+ # Unless data has been disarranged, all holdings grouped together under
+ # the same summary should have the same pattern code, so we can take any
+ # result from the set we just got.
+ my $pattern_field;
+ eval {
+ $pattern_field = new MARC::Field(
+ "853", # irrelevant for our purposes
+ @{ OpenSRF::Utils::JSON->JSON2perl($row->[0]->{pattern_code}) }
+ );
+ };
+ if ($@) {
+ return new OpenILS::Event("SERIAL_CORRUPT_PATTERN_CODE", note => $@);
+ }
+
+ # And now we know which subfields we will care about from
+ # serial.materialized_holding_code.
+ my $display_grouping = $row->[0]->{display_grouping};
+
+ # This will tell us when to stop grouping and start showing actual
+ # holdings.
+ my $deepest_level =
+ _get_deepest_holding_level($display_grouping, $pattern_field);
+ if (not defined $deepest_level) {
+ # corrupt pattern code
+ my $msg = "couldn't determine deepest holding level for " .
+ "$summary_type summary #$summary_id";
+ $logger->warn($msg);
+ return new OpenILS::Event("SERIAL_CORRUPT_PATTERN_CODE", note => $msg);
+ }
+
+ my @subfields = @{ $MFHD_SUMMARIZED_SUBFIELDS{$display_grouping} };
+
+ # We look for holdings grouped at the top level once no matter what,
+ # then we'll look deeper with additional queries for every element of
+ # $expand_path later.
+ # Below we define parts of the SELECT and JOIN clauses that we'll
+ # potentially reuse if $expand_path has elements.
+
+ my $subfield = shift @subfields;
+ my %subfield_joins = ("smhc_$subfield" => {class => "smhc"});
+ my %subfield_where_clauses = ("+smhc_$subfield" => {subfield => $subfield});
+
+ # Now get the top level of holdings.
+ my $top = $e->json_query(
+ _make_single_level_grouped_holding_query(
+ $subfield, $deepest_level, $summary_hint, $summary_id,
+ \%subfield_joins, \%subfield_where_clauses,
+ $limit, $offsets
+ )
+ ) or return $e->die_event;
+
+ # Deal with the extra row, if present, that tells are there are more pages
+ # of results.
+ my $top_more = 0;
+ if (scalar(@$top) > int($limit)) {
+ $top_more = 1;
+ pop @$top;
+ }
+
+ # Distribution is the same for all rows anyway, but we may need it for a
+ # copy query later.
+ my $dist_id = @$top ? $top->[0]->{distribution} : undef;
+
+ # This will help us avoid certain repetitive calculations. Examine
+ # _label_holding_level() to see what I mean.
+ my $mfhd_cache = {};
+
+ # Prepare related unit data if appropriate.
+ my $unit_data;
+
+ if ($with_units and $subfield eq $deepest_level) {
+ $unit_data = _opac_visible_unit_data(
+ [map { $_->{id} } @$top], $dist_id, $with_units > 1, $e
+ );
+ return $unit_data if defined $U->event_code($unit_data);
+ }
+
+ # Make the tree we have so far.
+ my $tree = [
+ map(
+ _make_grouped_holding_node(
+ $_, $subfield, $deepest_level, $pattern_field,
+ $unit_data, $mfhd_cache
+ ),
+ @$top
+ ), ($top_more ? undef : ())
+ ];
+
+ # We'll need a parent reference at each level as we descend.
+ my $parent = $tree;
+
+ # Will we be trying magic auto-expansion of the first top-level grouping?
+ if ($auto_expand_first and @$tree and not @$expand_path) {
+ $expand_path = [$tree->[0]->{value}];
+ $offsets = [0];
+ }
+
+ # Ok, that got us the top level, with nothing expanded. Now we loop through
+ # the elements of @$expand_path, issuing similar queries to get us deeper
+ # groupings and even actual specific holdings.
+ foreach my $value (@$expand_path) {
+ my $prev_subfield = $subfield;
+ $subfield = shift @subfields;
+
+ # This wad of JOINs is additive over each iteration.
+ $subfield_joins{"smhc_$subfield"} = {class => "smhc"};
+
+ # The WHERE clauses also change and grow each time.
+ $subfield_where_clauses{"+smhc_$prev_subfield"}->{value} = $value;
+ $subfield_where_clauses{"+smhc_$subfield"}->{subfield} = $subfield;
+
+ my $level = $e->json_query(
+ _make_single_level_grouped_holding_query(
+ $subfield, $deepest_level, $summary_hint, $summary_id,
+ \%subfield_joins, \%subfield_where_clauses,
+ $limit, $offsets
+ )
+ ) or return $e->die_event;
+
+ return $tree unless @$level;
+
+ # Deal with the extra row, if present, that tells are there are more
+ # pages of results.
+ my $level_more = 0;
+ if (scalar(@$level) > int($limit)) {
+ $level_more = 1;
+ pop @$level;
+ }
+
+ # Find attachment point for our results.
+ my ($point) = grep { ref $_ and $_->{value} eq $value } @$parent;
+
+ # Prepare related unit data if appropriate.
+ if ($with_units and $subfield eq $deepest_level) {
+ $unit_data = _opac_visible_unit_data(
+ [map { $_->{id} } @$level], $dist_id, $with_units > 1, $e
+ );
+ return $unit_data if defined $U->event_code($unit_data);
+ }
+
+ # Set parent for the next iteration.
+ $parent = $point->{children} = [
+ map(
+ _make_grouped_holding_node(
+ $_, $subfield, $deepest_level, $pattern_field,
+ $unit_data, $mfhd_cache
+ ),
+ @$level
+ ), ($level_more ? undef : ())
+ ];
+
+ last if $subfield eq $deepest_level;
+ }
+
+ return $tree;
+}
+
+__PACKAGE__->register_method(
+ method => "grouped_holdings_for_summary",
+ api_name => "open-ils.serial.holdings.grouped_by_summary",
+ api_level => 1,
+ argc => 7,
+ signature => {
+ desc => q/Return a tree of holdings associated with a given summary
+ grouped by all but the last of either chron or enum units./,
+ params => [
+ { name => "summary_type", type => "string" },
+ { name => "summary_id", type => "number" },
+ { name => "expand_path", type => "array",
+ desc => "In root-to-leaf order, the values of the nodes along the axis you want to expand" },
+ { name => "limit (default 10)", type => "number" },
+ { name => "offsets", type => "array", desc =>
+ "This must be exactly one element longer than expand_path" },
+ { name => "auto_expand_first", type => "boolean", desc =>
+ "Only if expand_path is empty, automatically expand first top-level grouping" },
+ { name => "with_units", type => "number", desc => q/
+ If true at all, for each holding, if there are associated units,
+ add some information about them to the result tree. These units
+ will be filtered by OPAC visibility unless you provide a value
+ greater than 1.
+
+ IOW:
+ 0 = no units,
+ 1 = opac visible units,
+ 2 = all units (i.e. staff view)
+ / }
+ ]
+ }
+);
+
+1;
}
return @subfields;
}
+my %__mfhd_month_labels = (
+ '01' => ['Jan.', 'January'],
+ '02' => ['Feb.', 'February'],
+ '03' => ['Mar.', 'March'],
+ '04' => ['Apr.', 'April'],
+ '05' => ['May ', 'May'],
+ '06' => ['Jun.', 'June'],
+ '07' => ['Jul.', 'July'],
+ '08' => ['Aug.', 'August'],
+ '09' => ['Sep.', 'September'],
+ '10' => ['Oct.', 'October'],
+ '11' => ['Nov.', 'November'],
+ '12' => ['Dec.', 'December'],
+ '21' => 'Spring',
+ '22' => 'Summer',
+ '23' => 'Autumn',
+ '24' => 'Winter'
+);
+
+sub _get_mfhd_month_label {
+ my ($month, $long) = @_;
+ $long ||= 0;
+
+ my $o = $__mfhd_month_labels{$month};
+ return (ref $o) ? $o->[$long] : $o;
+}
+
+# Called by method 'format_chron'
+#
+sub format_single_chron {
+ my $self = shift;
+ my $holdings = shift;
+ my $key = shift;
+ my $skip_sep = shift;
+ my $long = shift;
+ my $capstr;
+ my $chron;
+ my $sep = ':';
+
+ return if !defined $self->caption->capstr($key);
+
+ $capstr = $self->caption->capstr($key);
+ if (substr($capstr, 0, 1) eq '(') {
+ # a caption enclosed in parentheses is not displayed
+ $capstr = '';
+ }
+
+ # If this is the second level of chronology, then it's
+ # likely to be a month or season, so we should use the
+ # string name rather than the number given.
+ if ($key eq 'b' or $key eq 'j') {
+ # account for possible combined issue chronology
+ my @chron_parts = split('/', $holdings->{$key});
+ for (my $i = 0; $i < @chron_parts; $i++) {
+ my $month_label = _get_mfhd_month_label($chron_parts[$i], $long);
+ $chron_parts[$i] = $month_label if defined $month_label;
+ }
+ $chron = join('/', @chron_parts);
+ } else {
+ $chron = $holdings->{$key};
+ }
+
+ $skip_sep ||= ($key eq 'a' || $key eq 'i');
+
+ return ($skip_sep ? '' : $sep) . $capstr . $chron;
+}
#
# Called by method 'format_part' for formatting the chronology portion of
sub format_chron {
my $self = shift;
my $holdings = shift;
- my $caption = $self->caption;
my @keys = @_;
my $str = '';
- my %month = (
- '01' => 'Jan.',
- '02' => 'Feb.',
- '03' => 'Mar.',
- '04' => 'Apr.',
- '05' => 'May ',
- '06' => 'Jun.',
- '07' => 'Jul.',
- '08' => 'Aug.',
- '09' => 'Sep.',
- '10' => 'Oct.',
- '11' => 'Nov.',
- '12' => 'Dec.',
- '21' => 'Spring',
- '22' => 'Summer',
- '23' => 'Autumn',
- '24' => 'Winter'
- );
-
- foreach my $i (0..@keys) {
- my $key = $keys[$i];
- my $capstr;
- my $chron;
- my $sep;
-
- last if !defined $caption->capstr($key);
-
- $capstr = $caption->capstr($key);
- if (substr($capstr, 0, 1) eq '(') {
- # a caption enclosed in parentheses is not displayed
- $capstr = '';
- }
-
- # If this is the second level of chronology, then it's
- # likely to be a month or season, so we should use the
- # string name rather than the number given.
- if (($i == 1)) {
- # account for possible combined issue chronology
- my @chron_parts = split('/', $holdings->{$key});
- for (my $i = 0; $i < @chron_parts; $i++) {
- $chron_parts[$i] = $month{$chron_parts[$i]} if exists $month{$chron_parts[$i]};
- }
- $chron = join('/', @chron_parts);
- } else {
- $chron = $holdings->{$key};
- }
- $str .= (($i == 0 || $str =~ /[. ]$/) ? '' : ':') . $capstr . $chron;
+ foreach my $key (@keys) {
+ my $skip_sep = ($str =~ /[. ]$/);
+ my $new_part = $self->format_single_chron($holdings, $key, $skip_sep);
+ last unless defined $new_part;
+ $str .= $new_part;
}
return $str;
}
#
+# Called by method 'format_part' for each enum subfield
+#
+sub format_single_enum {
+ my $self = shift;
+ my $holding_values = shift;
+ my $key = shift;
+ my $skip_sep = shift;
+ my $capstr;
+ my $chron;
+ my $sep = ':';
+
+ return if !defined $self->caption->capstr($key);
+
+ $capstr = $self->caption->capstr($key);
+ if (substr($capstr, 0, 1) eq '(') {
+ # a caption enclosed in parentheses is not displayed
+ $capstr = '';
+ } elsif ($skip_sep) {
+ # We'll let a $skip_sep parameter of true mean what it means down by
+ # the return statement AND to pad the caption itself here.
+ $capstr .= ' ';
+ }
+
+
+ $skip_sep ||= ($key eq 'a');
+ return ($skip_sep ? '' : $sep) . $capstr . $holding_values->{$key};
+}
+
+#
# Called by method 'format' for each member of a possibly compressed holding
#
sub format_part {
# Enumerations
foreach my $key ('a'..'f') {
- my $capstr;
- my $chron;
- my $sep;
-
- last if !defined $caption->capstr($key);
-
- $capstr = $caption->capstr($key);
- if (substr($capstr, 0, 1) eq '(') {
- # a caption enclosed in parentheses is not displayed
- $capstr = '';
- }
- $str .=
- ($key eq 'a' ? '' : ':') . $capstr . $holding_values->{$key};
+ my $new_part = $self->format_single_enum($holding_values, $key);
+ last unless defined $new_part;
+ $str .= $new_part;
}
# Chronology
$ctx->{get_org_setting}->
($org, "opac.fully_compressed_serial_holdings")
) {
- $ctx->{holding_summaries} =
- $self->get_holding_summaries($rec_id, $org, $copy_depth);
-
- $ctx->{have_holdings_to_show} =
- scalar(@{$ctx->{holding_summaries}->{basic}}) ||
- scalar(@{$ctx->{holding_summaries}->{index}}) ||
- scalar(@{$ctx->{holding_summaries}->{supplement}});
+ # We're loading this data here? Are we therefore assuming that we
+ # *are* going to display something in the "issues" expandy?
+ $self->load_serial_holding_summaries($rec_id, $org, $copy_depth);
} else {
$ctx->{mfhd_summaries} =
$self->get_mfhd_summaries($rec_id, $org, $copy_depth);
$ctx->{marchtml} = $self->mk_marc_html($rec_id);
},
issues => sub {
- $ctx->{expanded_holdings} =
- $self->get_expanded_holdings($rec_id, $org, $copy_depth)
- if $ctx->{have_holdings_to_show};
+ return;
+ # XXX this needed?
},
cnbrowse => sub {
$self->prepare_browse_call_numbers();
my $copy_offset = shift;
my $pref_ou = shift;
- my $query = {
- select => {
- acp => ['id', 'barcode', 'circ_lib', 'create_date', 'age_protect', 'holdable'],
- acpl => [
- {column => 'name', alias => 'copy_location'},
- {column => 'holdable', alias => 'location_holdable'}
- ],
- ccs => [
- {column => 'name', alias => 'copy_status'},
- {column => 'holdable', alias => 'status_holdable'}
- ],
- acn => [
- {column => 'label', alias => 'call_number_label'},
- {column => 'id', alias => 'call_number'}
- ],
- circ => ['due_date'],
- acnp => [
- {column => 'label', alias => 'call_number_prefix_label'},
- {column => 'id', alias => 'call_number_prefix'}
- ],
- acns => [
- {column => 'label', alias => 'call_number_suffix_label'},
- {column => 'id', alias => 'call_number_suffix'}
- ],
- bmp => [
- {column => 'label', alias => 'part_label'},
- ]
- },
-
- from => {
- acp => {
- acn => {
- join => {
- acnp => { fkey => 'prefix' },
- acns => { fkey => 'suffix' }
- },
- filter => [{deleted => 'f'}, {record => $rec_id}],
- },
- circ => { # If the copy is circulating, retrieve the open circ
- type => 'left',
- filter => {checkin_time => undef}
- },
- acpl => {},
- ccs => {},
- aou => {},
- acpm => {
- type => 'left',
- join => {
- bmp => { type => 'left' }
- }
- }
- }
- },
-
- where => {
- '+acp' => {deleted => 'f' }
- },
-
- order_by => [
- { class => "aou", field => 'id',
- transform => 'evergreen.rank_ou', params => [$org, $pref_ou]
- },
- {class => 'aou', field => 'name'},
- {class => 'acn', field => 'label'},
- { class => "acp", field => 'status',
- transform => 'evergreen.rank_cp_status'
- }
- ],
-
- limit => $copy_limit,
- offset => $copy_offset
- };
+ my $query = $U->basic_opac_copy_query(
+ $rec_id, undef, undef, $copy_limit, $copy_offset, $self->ctx->{is_staff}
+ );
if($org != $self->ctx->{aou_tree}->()->id) {
# no need to add the org join filter if we're not actually filtering
};
};
- # Filter hidden items if this is the public catalog
- unless($self->ctx->{is_staff}) {
- $query->{where}->{'+acp'}->{opac_visible} = 't';
- $query->{from}->{'acp'}->{'acpl'}->{filter} = {opac_visible => 't'};
- $query->{from}->{'acp'}->{'ccs'}->{filter} = {opac_visible => 't'};
- $query->{where}->{'+aou'}->{opac_visible} = 't';
- }
+ # Unsure if we want these in the shared function, leaving here for now
+ unshift(@{$query->{order_by}},
+ { class => "aou", field => 'id',
+ transform => 'evergreen.rank_ou', params => [$org, $pref_ou]
+ }
+ );
+ push(@{$query->{order_by}},
+ { class => "acp", field => 'status',
+ transform => 'evergreen.rank_cp_status'
+ }
+ );
return $query;
}
'open-ils.search.biblio.record.html', $rec_id, 1);
}
-sub get_holding_summaries {
+sub load_serial_holding_summaries {
my ($self, $rec_id, $org, $depth) = @_;
+ my $limit = $self->cgi->param("slimit") || 10;
+ my $offset = $self->cgi->param("soffset") || 0;
+
my $serial = create OpenSRF::AppSession("open-ils.serial");
- my $result = $serial->request(
- "open-ils.serial.bib.summary_statements",
- $rec_id, {"org_id" => $org, "depth" => $depth}
+
+ # First, get the tree of /summaries/ of holdings.
+ my $tree = $serial->request(
+ "open-ils.serial.holding_summary_tree.by_bib",
+ $rec_id, $org, $depth, $limit, $offset
)->gather(1);
+ return if $self->apache_log_if_event(
+ $tree, "getting holding summary tree for record $rec_id"
+ );
+
+ # Next, if requested, get a list of individual holdings under a
+ # particular summary.
+ my $holdings;
+ my $summary_id = int($self->cgi->param("sid") || 0);
+ my $summary_type = $self->cgi->param("stype");
+
+ if ($summary_id and $summary_type) {
+ my $expand_path = [ $self->cgi->param("sepath") ],
+ my $expand_limit = $self->cgi->param("selimit");
+ my $expand_offsets = [ $self->cgi->param("seoffset") ];
+ my $auto_expand_first = 0;
+
+ if (not @$expand_offsets) {
+ $expand_offsets = undef;
+ $auto_expand_first = 1;
+ }
+
+ $holdings = $serial->request(
+ "open-ils.serial.holdings.grouped_by_summary",
+ $summary_type, $summary_id,
+ $expand_path, $expand_limit, $expand_offsets,
+ $auto_expand_first,
+ 1 + ($self->ctx->{is_staff} ? 1 : 0)
+ )->gather(1);
+
+ if ($holdings and ref $holdings eq "ARRAY") {
+ $self->place_holdings_with_summary(
+ $tree, $holdings, $summary_id, $summary_type
+ ) or $self->apache->log->warn(
+ "could not place holdings within summary tree"
+ );
+ } else {
+ $self->apache_log_if_event(
+ $holdings, "getting holdings grouped by summary $summary_id"
+ );
+ }
+ }
+
$serial->kill_me;
- return $result;
+
+ # The presence of any keys in the tree hash other than 'more' means that we
+ # must have /something/ we could show.
+ $self->ctx->{have_holdings_to_show} = grep { $_ ne 'more' } (keys %$tree);
+
+ $self->ctx->{holding_summary_tree} = $tree;
}
-sub get_mfhd_summaries {
- my ($self, $rec_id, $org, $depth) = @_;
+# This helper to load_serial_holding_summaries() recursively searches in
+# $tree for a holding summary matching $sid and $stype, and places $holdings
+# within the node for that summary. IOW, this is about showing expanded
+# holdings under their "parent" summary.
+sub place_holdings_with_summary {
+ my ($self, $tree, $holdings, $sid, $stype) = @_;
+
+ foreach my $sum (@{$tree->{holding_summaries}}) {
+ if ($sum->{id} == $sid and $sum->{summary_type} eq $stype) {
+ $sum->{holdings} = $holdings;
+ return 1;
+ }
+ }
- my $serial = create OpenSRF::AppSession("open-ils.search");
- my $result = $serial->request(
- "open-ils.search.serial.record.bib.retrieve",
- $rec_id, $org, $depth
- )->gather(1);
+ foreach my $child (@{$tree->{children}}) {
+ return 1 if $self->place_holdings_with_summary(
+ $child, $holdings, $sid, $stype
+ );
+ }
- $serial->kill_me;
- return $result;
+ return;
}
-sub get_expanded_holdings {
+sub get_mfhd_summaries {
my ($self, $rec_id, $org, $depth) = @_;
- my $holding_limit = int($self->cgi->param("holding_limit") || 10);
- my $holding_offset = int($self->cgi->param("holding_offset") || 0);
- my $type = $self->cgi->param("expand_holding_type");
-
- my $serial = create OpenSRF::AppSession("open-ils.serial");
+ my $serial = create OpenSRF::AppSession("open-ils.search");
my $result = $serial->request(
- "open-ils.serial.received_siss.retrieve.by_bib.atomic",
- $rec_id, {
- "ou" => $org, "depth" => $depth,
- "limit" => $holding_limit, "offset" => $holding_offset,
- "type" => $type
- }
+ "open-ils.search.serial.record.bib.retrieve",
+ $rec_id, $org, $depth
)->gather(1);
$serial->kill_me;
return Apache2::Const::OK;
}
+sub apache_log_if_event {
+ my ($self, $event, $prefix_text, $success_ok, $level) = @_;
+
+ $prefix_text ||= "Evergreen returned event";
+ $success_ok ||= 0;
+ $level ||= "warn";
+
+ chomp $prefix_text;
+ $prefix_text .= ": ";
+
+ my $code = $U->event_code($event);
+ if (defined $code and ($code or not $success_ok)) {
+ $self->apache->log->$level(
+ $prefix_text .
+ ($event->{textcode} || "") . " ($code)" .
+ ($event->{note} ? (": " . $event->{note}) : "")
+ );
+ return 1;
+ }
+
+ return;
+}
+
1;
INSERT INTO config.internal_flag (name) VALUES ('ingest.disable_metabib_rec_descriptor');
INSERT INTO config.internal_flag (name) VALUES ('ingest.disable_metabib_field_entry');
INSERT INTO config.internal_flag (name) VALUES ('ingest.assume_inserts_only');
+INSERT INTO config.internal_flag (name) VALUES ('serial.rematerialize_on_same_holding_code');
CREATE TABLE config.global_flag (
label TEXT NOT NULL
FROM actor.org_unit_ancestors($2);
$$ LANGUAGE SQL STABLE ROWS 1;
+-- Given the IDs of two rows in actor.org_unit, *the second being an ancestor
+-- of the first*, return in array form the path from the ancestor to the
+-- descendant, with each point in the path being an org_unit ID. This is
+-- useful for sorting org_units by their position in a depth-first (display
+-- order) representation of the tree.
+--
+-- This breaks with the precedent set by actor.org_unit_full_path() and others,
+-- and gets the parameters "backwards," but otherwise this function would
+-- not be very usable within json_query.
+CREATE OR REPLACE FUNCTION actor.org_unit_simple_path(INT, INT)
+RETURNS INT[] AS $$
+ WITH RECURSIVE descendant_depth(id, path) AS (
+ SELECT aou.id,
+ ARRAY[aou.id]
+ FROM actor.org_unit aou
+ JOIN actor.org_unit_type aout ON (aout.id = aou.ou_type)
+ WHERE aou.id = $2
+ UNION ALL
+ SELECT aou.id,
+ dd.path || ARRAY[aou.id]
+ FROM actor.org_unit aou
+ JOIN actor.org_unit_type aout ON (aout.id = aou.ou_type)
+ JOIN descendant_depth dd ON (dd.id = aou.parent_ou)
+ ) SELECT dd.path
+ FROM actor.org_unit aou
+ JOIN descendant_depth dd USING (id)
+ WHERE aou.id = $1 ORDER BY dd.path;
+$$ LANGUAGE SQL STABLE ROWS 1;
+
CREATE OR REPLACE FUNCTION actor.org_unit_proximity ( INT, INT ) RETURNS INT AS $$
SELECT COUNT(id)::INT FROM (
SELECT id FROM actor.org_unit_combined_ancestors($1, $2)
REFERENCES actor.org_unit (id)
DEFERRABLE INITIALLY DEFERRED,
label TEXT NOT NULL,
+ display_grouping TEXT NOT NULL DEFAULT 'chron'
+ CHECK (display_grouping IN ('enum', 'chron')),
receive_call_number BIGINT REFERENCES asset.call_number (id)
DEFERRABLE INITIALLY DEFERRED,
receive_unit_template INT REFERENCES asset.copy_template (id)
);
CREATE INDEX serial_index_summary_dist_idx ON serial.index_summary (distribution);
+CREATE VIEW serial.any_summary AS
+ SELECT
+ 'basic' AS summary_type, id, distribution,
+ generated_coverage, textual_holdings, show_generated
+ FROM serial.basic_summary
+ UNION
+ SELECT
+ 'index' AS summary_type, id, distribution,
+ generated_coverage, textual_holdings, show_generated
+ FROM serial.index_summary
+ UNION
+ SELECT
+ 'supplement' AS summary_type, id, distribution,
+ generated_coverage, textual_holdings, show_generated
+ FROM serial.supplement_summary ;
+
+
+CREATE TABLE serial.materialized_holding_code (
+ id BIGSERIAL PRIMARY KEY,
+ issuance INTEGER NOT NULL REFERENCES serial.issuance (id) ON DELETE CASCADE,
+ holding_type TEXT NOT NULL,
+ ind1 TEXT,
+ ind2 TEXT,
+ subfield CHAR,
+ value TEXT
+);
+
+CREATE OR REPLACE FUNCTION serial.materialize_holding_code() RETURNS TRIGGER
+AS $func$
+use strict;
+
+use MARC::Field;
+use JSON::XS;
+
+# Do nothing if holding_code has not changed...
+
+if ($_TD->{new}{holding_code} eq $_TD->{old}{holding_code}) {
+ # ... unless the following internal flag is set.
+
+ my $flag_rv = spi_exec_query(q{
+ SELECT * FROM config.internal_flag
+ WHERE name = 'serial.rematerialize_on_same_holding_code' AND enabled
+ }, 1);
+ return unless $flag_rv->{processed};
+}
+
+
+my $holding_code = (new JSON::XS)->decode($_TD->{new}{holding_code});
+
+my $field = new MARC::Field('999', @$holding_code); # tag doesnt matter
+
+my $dstmt = spi_prepare(
+ 'DELETE FROM serial.materialized_holding_code WHERE issuance = $1',
+ 'INT'
+);
+spi_exec_prepared($dstmt, $_TD->{new}{id});
+
+my $istmt = spi_prepare(
+ q{
+ INSERT INTO serial.materialized_holding_code (
+ issuance, holding_type, ind1, ind2, subfield, value
+ ) VALUES ($1, $2, $3, $4, $5, $6)
+ }, qw{INT TEXT TEXT TEXT CHAR TEXT}
+);
+
+foreach ($field->subfields) {
+ spi_exec_prepared(
+ $istmt,
+ $_TD->{new}{id},
+ $_TD->{new}{holding_type},
+ $field->indicator(1),
+ $field->indicator(2),
+ $_->[0],
+ $_->[1]
+ );
+}
+
+return;
+
+$func$ LANGUAGE 'plperlu';
+
+CREATE INDEX assist_holdings_display
+ ON serial.materialized_holding_code (issuance, subfield);
+
+CREATE TRIGGER materialize_holding_code
+ AFTER INSERT OR UPDATE ON serial.issuance
+ FOR EACH ROW EXECUTE PROCEDURE serial.materialize_holding_code() ;
COMMIT;
'description'
),
'bool', null)
+,( 'serial.default_display_grouping', 'serial'
+ oils_i18n_gettext(
+ 'serial.default_display_grouping',
+ 'Default display grouping for serials distributions presented in the OPAC.',
+ 'coust',
+ 'label'
+ ),
+ oils_i18n_gettext(
+ 'serial.default_display_grouping',
+ 'Default display grouping for serials distributions presented in the OPAC. This can be "enum" or "chron".',
+ 'coust',
+ 'description'
+ ),
+ 'string', null)
+
;
UPDATE config.org_unit_setting_type
),
'string'
);
-
SELECT setval( 'config.sms_carrier_id_seq', 1000 );
INSERT INTO config.sms_carrier VALUES
--- /dev/null
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT INTO config.internal_flag (name, value, enabled) VALUES (
+ 'serial.rematerialize_on_same_holding_code', NULL, FALSE
+);
+
+INSERT INTO config.org_unit_setting_type (
+ name, label, grp, description, datatype
+) VALUES (
+ 'serial.default_display_grouping',
+ 'Default display grouping for serials distributions presented in the OPAC.',
+ 'serial',
+ 'Default display grouping for serials distributions presented in the OPAC. This can be "enum" or "chron".',
+ 'string'
+);
+
+ALTER TABLE serial.distribution
+ ADD COLUMN display_grouping TEXT NOT NULL DEFAULT 'chron'
+ CHECK (display_grouping IN ('enum', 'chron'));
+
+-- why didn't we just make one summary table in the first place?
+CREATE VIEW serial.any_summary AS
+ SELECT
+ 'basic' AS summary_type, id, distribution,
+ generated_coverage, textual_holdings, show_generated
+ FROM serial.basic_summary
+ UNION
+ SELECT
+ 'index' AS summary_type, id, distribution,
+ generated_coverage, textual_holdings, show_generated
+ FROM serial.index_summary
+ UNION
+ SELECT
+ 'supplement' AS summary_type, id, distribution,
+ generated_coverage, textual_holdings, show_generated
+ FROM serial.supplement_summary ;
+
+
+-- Given the IDs of two rows in actor.org_unit, *the second being an ancestor
+-- of the first*, return in array form the path from the ancestor to the
+-- descendant, with each point in the path being an org_unit ID. This is
+-- useful for sorting org_units by their position in a depth-first (display
+-- order) representation of the tree.
+--
+-- This breaks with the precedent set by actor.org_unit_full_path() and others,
+-- and gets the parameters "backwards," but otherwise this function would
+-- not be very usable within json_query.
+CREATE OR REPLACE FUNCTION actor.org_unit_simple_path(INT, INT)
+RETURNS INT[] AS $$
+ WITH RECURSIVE descendant_depth(id, path) AS (
+ SELECT aou.id,
+ ARRAY[aou.id]
+ FROM actor.org_unit aou
+ JOIN actor.org_unit_type aout ON (aout.id = aou.ou_type)
+ WHERE aou.id = $2
+ UNION ALL
+ SELECT aou.id,
+ dd.path || ARRAY[aou.id]
+ FROM actor.org_unit aou
+ JOIN actor.org_unit_type aout ON (aout.id = aou.ou_type)
+ JOIN descendant_depth dd ON (dd.id = aou.parent_ou)
+ ) SELECT dd.path
+ FROM actor.org_unit aou
+ JOIN descendant_depth dd USING (id)
+ WHERE aou.id = $1 ORDER BY dd.path;
+$$ LANGUAGE SQL STABLE ROWS 1;
+
+CREATE TABLE serial.materialized_holding_code (
+ id BIGSERIAL PRIMARY KEY,
+ issuance INTEGER NOT NULL REFERENCES serial.issuance (id) ON DELETE CASCADE,
+ holding_type TEXT NOT NULL,
+ ind1 TEXT,
+ ind2 TEXT,
+ subfield CHAR,
+ value TEXT
+);
+
+CREATE OR REPLACE FUNCTION serial.materialize_holding_code() RETURNS TRIGGER
+AS $func$
+use strict;
+
+use MARC::Field;
+use JSON::XS;
+
+# Do nothing if holding_code has not changed...
+
+if ($_TD->{new}{holding_code} eq $_TD->{old}{holding_code}) {
+ # ... unless the following internal flag is set.
+
+ my $flag_rv = spi_exec_query(q{
+ SELECT * FROM config.internal_flag
+ WHERE name = 'serial.rematerialize_on_same_holding_code' AND enabled
+ }, 1);
+ return unless $flag_rv->{processed};
+}
+
+
+my $holding_code = (new JSON::XS)->decode($_TD->{new}{holding_code});
+
+my $field = new MARC::Field('999', @$holding_code); # tag doesnt matter
+
+my $dstmt = spi_prepare(
+ 'DELETE FROM serial.materialized_holding_code WHERE issuance = $1',
+ 'INT'
+);
+spi_exec_prepared($dstmt, $_TD->{new}{id});
+
+my $istmt = spi_prepare(
+ q{
+ INSERT INTO serial.materialized_holding_code (
+ issuance, holding_type, ind1, ind2, subfield, value
+ ) VALUES ($1, $2, $3, $4, $5, $6)
+ }, qw{INT TEXT TEXT TEXT CHAR TEXT}
+);
+
+foreach ($field->subfields) {
+ spi_exec_prepared(
+ $istmt,
+ $_TD->{new}{id},
+ $_TD->{new}{holding_type},
+ $field->indicator(1),
+ $field->indicator(2),
+ $_->[0],
+ $_->[1]
+ );
+}
+
+return;
+
+$func$ LANGUAGE 'plperlu';
+
+CREATE INDEX assist_holdings_display
+ ON serial.materialized_holding_code (issuance, subfield);
+
+CREATE TRIGGER materialize_holding_code
+ AFTER INSERT OR UPDATE ON serial.issuance
+ FOR EACH ROW EXECUTE PROCEDURE serial.materialize_holding_code() ;
+
+-- starting here, we materialize all existing holding codes.
+
+UPDATE config.internal_flag
+ SET enabled = TRUE
+ WHERE name = 'serial.rematerialize_on_same_holding_code';
+
+UPDATE serial.issuance SET holding_code = holding_code;
+
+UPDATE config.internal_flag
+ SET enabled = FALSE
+ WHERE name = 'serial.rematerialize_on_same_holding_code';
+
+-- finish holding code materialization process
+
+COMMIT;
#
# Current page, clear 'some_param' from the existing params:
# mkurl('', {foo => 'bar', boo => 'baz'}, ['some_param']);
- MACRO mkurl(page, params, clear_params) BLOCK;
+ #
+ # Current page to a named anchor 'copies'
+ # mkurl('', {}, [], 'copies');
+ MACRO mkurl(page, params, clear_params, named_anchor) BLOCK;
# clone the query string to avoid clobberation
cgi = CGI.new(CGI.query_string);
FOR k IN params.keys;
encoded = [];
max = params.$k.max;
- list = (params.$k.0 OR max == -1) ? params.$k : [params.$k];
+
+ # The following commented-out line can be fooled. Its replacement
+ # below is what you really mean.
+ # list = (params.$k.0 OR max == -1) ? params.$k : [params.$k];
+ list = params.$k.list;
+
IF list.size == 0; NEXT; END;
# CGI croaks on already-decoded strings. force-encode to be safe.
FOR p IN list; encoded.push(ctx.encode_utf8(p)); END;
END;
END;
+ final = named_anchor ? '#' _ named_anchor : '';
+
IF page;
IF cgi.query_string;
- page _ '?' _ cgi.query_string;
+ page _ '?' _ cgi.query_string _ final;
ELSE;
- page;
+ page _ final;
END;
ELSE;
# staying on the current page
- cgi.url("-path" => 1, "-query" => 1);
+ cgi.url("-path" => 1, "-query" => 1) _ final;
END;
END;
[%-
-FOREACH copy_info IN ctx.copies;
+
+# If being used in serial mode, flatten list of units so that they can be
+# used like one long list of copies without changing so much code below.
+IF serial_holdings;
+ copies = [];
+ FOREACH h IN serial_holdings;
+ units = h.units.slice(0); # copy
+ FOREACH unit IN units;
+ unit.holding_label = h.label;
+ END;
+ copies = copies.merge(units);
+ END;
+END;
+
+FOREACH copy_info IN copies;
IF copy_info.call_number_label != '##URI##';
has_copies = 'true';
END;
LAST;
END;
END;
-%]
+-%]
[%- IF has_copies; %]
<table cellpadding="0" cellspacing="0" border="0" width="100%" id="rdetails_status">
<thead>
<tr>
+ [% IF serial_holdings -%]
+ <th id='copy_header_holding_label'>[% l("Issue Label") %]</th>
+ [%- ELSE -%]
<th id='copy_header_library'>[% l("Location") %]</th>
+ [%- END %]
<th id='copy_header_callnmber'>[% l("Call Number") %]</th>
[%- IF has_parts == 'true' %]
<th id='copy_header_part'>[% l("Part") %]</th>
[%- IF ctx.is_staff %]
<th id='copy_header_age_hold'>[% l("Age Hold Protection") %]</th>
<th id='copy_header_create_date'>[% l("Create Date") %]</th>
+ [%- END %]
+ [%- IF ctx.is_staff OR serial_holdings %]
<th id='copy_header_holdable'>[% l("Holdable?") %]</th>
[%- END %]
<th id='copy_header_status'>[% l("Status") %]</th>
</thead>
<tbody class="copy_details_table">
[%- last_cn = 0;
- FOR copy_info IN ctx.copies;
+ FOR copy_info IN copies;
callnum = copy_info.call_number_label;
NEXT IF callnum == '##URI##';
END;
-%]
<tr>
- <td header='copy_header_library'>
+ [%- IF serial_holdings %]<td header='copy_header_holding_label' class='rdetail-issue-issue'>
+ [%- copy_info.holding_label | html; -%]
+ </td>
+ [%- ELSE %]<td header='copy_header_library'>
[%-
org_name = ctx.get_aou(copy_info.circ_lib).name;
org_name | html
-%]
- </td>
- <td header='copy_header_callnumber'>[% callnum | html %] [% IF ctx.get_org_setting(ctx.search_ou, 'sms.enable') == 1 %](<a href="[% mkurl(ctx.opac_root _ '/sms_cn', {copy_id => copy_info.id}) %]">Text</a>)[% END %]</td>
+ </td>[% END %]
+ <td header='copy_header_callnumber'>[% callnum | html %] [% IF ctx.get_org_setting(CGI.param('loc') OR ctx.aou_tree.id, 'sms.enable') == 1 %](<a href="[% mkurl(ctx.opac_root _ '/sms_cn', {copy_id => copy_info.id}) %]">Text</a>)[% END %]</td>
[%- IF has_parts == 'true' %]
<td header='copy_header_part'>[% copy_info.part_label | html %]</td>
[%- END %]
ctx.parse_datetime(copy_info.create_date),
DATE_FORMAT
) %]</td>
+ [% END # is_staff %]
+ [% IF ctx.is_staff OR serial_holdings %]
<td header='copy_header_holdable'>[% # Show copy/volume hold links to staff (without
# checking whether they have permissions to do those).
- overall_holdable = (copy_info.holdable == 't' AND
+ overall_holdable = (
+ copy_info.holdable == 't' AND
copy_info.location_holdable == 't' AND
copy_info.status_holdable == 't');
IF overall_holdable;
- l("Place on"); %]
+ l("Place on");
+ IF ctx.is_staff;
+ %]
<a href="[% mkurl(ctx.opac_root _ '/place_hold',
{hold_target => copy_info.id, hold_type => 'C', hold_source_page => mkurl()}) %]">[% l("copy") %]</a>
[%- IF copy_info.call_number != last_cn;
<a href="[% mkurl(ctx.opac_root _ '/place_hold',
{hold_target => copy_info.call_number, hold_type => 'V', hold_source_page => mkurl()}) %]">[% l("volume") %]</a>
[%- END;
+ IF serial_holdings;
+ l(" / ");
+ END;
+ END;
+ IF serial_holdings;
+ %]
+ <a class="rdetail-issue-place-hold"
+ href="[% mkurl(ctx.opac_root _ '/place_hold', {
+ hold_target => copy_info.issuance, hold_type => 'I',
+ hold_source_page => mkurl()
+ }) %]">[% l("issue") %]</a>[%-
+ END;
ELSE;
l("No");
END %]</td>
</tr>
[%- END %]
<tr>
- [%- IF ctx.copy_offset > 0;
+ [%- IF ctx.copy_offset > 0 AND NOT serial_holdings;
new_offset = ctx.copy_offset - ctx.copy_limit;
IF new_offset < 0; new_offset = 0; END %]
<td>
l('Previous [_1]', ctx.copy_offset - new_offset) %]</a>
</td>
[%- END %]
- [%- IF ctx.copies.size >= ctx.copy_limit %]
+ [%- IF copies.size >= ctx.copy_limit AND NOT serial_holdings %]
<td>
<a href="[% mkurl('', {copy_offset => ctx.copy_offset + ctx.copy_limit, copy_limit => ctx.copy_limit}) %]">[%
l('Next [_1]', ctx.copy_limit) %] »</a>
</td>
[%- END %]
</tr>
+ [% IF NOT serial_holdings -%]
<tr>
<td>
[%- more_copies_limit = 50 %] [%# TODO: config %]
- [%- IF ctx.copy_limit != more_copies_limit AND ctx.copies.size >= ctx.copy_limit %]
+ [%- IF ctx.copy_limit != more_copies_limit AND copies.size >= ctx.copy_limit %]
<div class="rdetail_show_copies">
<img src="[% ctx.media_prefix %]/images/plus_sign.png" />
<a href="[% mkurl('', {copy_limit => more_copies_limit, copy_offset => 0}) %]">[% l('Show more copies') %]</a>
[%- END %]
</td>
</tr>
+ [%- END %]
</tbody>
</table>
[% END; %]
--- /dev/null
+[%-
+expand_path = CGI.param('sepath') || [];
+expand_path = expand_path.list; # sic
+
+seoffset_list = CGI.param('seoffset') || [];
+seoffset_list = seoffset_list.list; # sic
+
+IF expand_path.size == 0 AND seoffset_list.size == 0;
+ seoffset_list = [0,0]; # compensate for $auto_expand_first; see ML
+END;
+
+selimit = CGI.param('selimit') || 10;
+ght_sepath = [];
+ght_depth = 0;
+
+VIEW grouped_holding_tree;
+ BLOCK list;
+ '<div class="rdetail-holding-group">';
+ prev_seoffset_list = seoffset_list.slice(0, ght_depth);
+ next_seoffset_list = seoffset_list.slice(0, ght_depth);
+
+ prev_seoffset_list.$ght_depth = prev_seoffset_list.$ght_depth - selimit;
+ IF prev_seoffset_list.$ght_depth < 0;
+ prev_seoffset_list.$ght_depth = 0;
+ END;
+
+ next_seoffset_list.$ght_depth = next_seoffset_list.$ght_depth + selimit;
+ IF item.0.units.size;
+ INCLUDE "opac/parts/record/copy_table.tt2" serial_holdings=item;
+ "<hr />";
+ "</div>";
+ ELSE;
+ FOREACH node IN item;
+ IF NOT node.label;
+ has_more = 1;
+ LAST;
+ END;
+
+ IF node.value;
+ ght_sepath.push(node.value);
+ new_seoffsets = seoffset_list.slice(0, ght_depth);
+ new_seoffsets.push(0);
+
+ expand_link = mkurl(
+ '', {'sepath' => ght_sepath, 'seoffset' => new_seoffsets},
+ 0, 'issues'
+ );
+
+ collapse_sepath = ght_sepath.slice(0, -2);
+ IF collapse_sepath.size == 0;
+ collapse_clear_params = ['sepath'];
+ ELSE;
+ collapse_clear_params = 0;
+ END;
+
+ collapse_link = mkurl(
+ '', {
+ 'sepath' => collapse_sepath,
+ 'seoffset' => new_seoffsets.slice(0, -2)
+ }, collapse_clear_params, 'issues'
+ );
+
+ "<div class='rdetail-holding-group'>";
+ IF node.children.size;
+ # TODO: make images or figure out a CSS trick or
+ # something. I doubt we can count on all OPAC clients
+ # having adequate fonts to cover these Unicode glyphs.
+ "▼ <a href='"; collapse_link;
+ ELSE;
+ "► <a href='"; expand_link;
+ END;
+ "'>"; node.label; "</a></div>";
+
+ IF node.children.size;
+ ght_depth = ght_depth + 1;
+ view.print(node.children);
+ ght_depth = ght_depth - 1;
+ END;
+
+ waste = ght_sepath.pop;
+ ELSE;
+ "<div class='rdetail-holding-group'>"; node.label; "</div>";
+ # XXX Hold placement link here? Maybe not if no units.
+ END;
+ END;
+
+ to_clear = 0;
+ new_sepath_end = ght_depth - 1;
+ IF new_sepath_end < 0;
+ to_clear = ['sepath'];
+ new_sepath = [];
+ ELSE;
+ new_sepath = expand_path.slice(0, ght_depth - 1);
+ END;
+
+ IF has_more;
+ '<a class="paging" href="';
+ mkurl('',{seoffset => next_seoffset_list, sepath => new_sepath},to_clear,'issues');
+ '">« '; l('Earlier holdings'); '</a>';
+ END;
+ IF seoffset_list.$ght_depth > 0;
+ '<a class="paging" href="';
+ mkurl('',{seoffset => prev_seoffset_list, sepath => new_sepath},to_clear,'issues');
+ '">'; l('Later holdings'); ' »</a> ';
+ END;
+ '</div>';
+ END;
+ END;
+END;
+
+VIEW holding_summary_tree;
+ BLOCK hash;
+ '<div class="rdetail-holding-group">';
+ ctx.get_aou(item.org_unit).name; "<br />";
+ FOREACH summary IN item.holding_summaries;
+ IF summary.holdings;
+ twisty = '▼ ';
+ link = mkurl(
+ '', {},
+ ['sid','stype','selimit','sepath','seoffset'], 'issues'
+ );
+ link_title = l('Collapse');
+ ELSE;
+ twisty = '► ';
+ link = mkurl(
+ '', {sid => summary.id, stype => summary.summary_type},
+ ['selimit','sepath','seoffset'], 'issues'
+ );
+ link_title = l('Expand');
+ END;
+ '<span>'; twisty;
+ '<a href="' _ link _ '" title="' _ link_title _ '">';
+ summary.generated_coverage.join(", ");
+ '</a></span><br />';
+ IF summary.holdings;
+ grouped_holding_tree.print(summary.holdings);
+ END;
+ END;
+ FOREACH child IN item.children;
+ view.print(child);
+ END;
+ '</div>';
+ END;
+END %]
+ <div class="holding-summary-tree">
+ [% holding_summary_tree.print(ctx.holding_summary_tree) %]
+ </div>
+ <div class="holding-summary-tree-pager">
+ [% slimit = CGI.param('slimit') || 10;
+ soffset = CGI.param('soffset') || 0;
+ soffset_prev = soffset - slimit;
+ IF soffset_prev < 0; soffset_prev = 0; END;
+ soffset_next = soffset + slimit;
+ %]
+ [% IF soffset > 0 %]
+ <a href="[% mkurl('', {soffset => soffset_prev}, ['sid','stype','sepath','selimit','seoffset'], 'issues') %]>[% l('Previous') %]</a>
+ [% END %]
+ [% IF ctx.holding_summary_tree.more %]
+ <a href="[% mkurl('', {soffset => soffset_next}, ['sid','stype','sepath','selimit','seoffset'], 'issues') %]">[% l('Next') %]</a>
+ [% END %]
+ </div>
--- /dev/null
+[% IF ctx.mfhd_summaries.size; %]
+ <div class="rdetail-mfhd-holdings">
+ <table><tbody>
+[%
+ mfhd = {
+ basic_holdings = l('Volumes'),
+ basic_holdings_add = l('Additional Volume Information'),
+ supplement_holdings = l('Supplements'),
+ supplement_holdings_add = l('Additional Supplement Information'),
+ index_holdings = l('Indexes'),
+ index_holdings_add = l('Additional Index Information'),
+ online = l('Online'),
+ missing = l('Missing'),
+ incomplete = l('Incomplete'),
+ };
+
+ FOREACH serial IN ctx.mfhd_summaries;
+%]
+ <tr>
+ <td class="rdetail-mfhd-head" colspan="2">[% l('Holdings summary ([_1])', serial.location) %]</td>
+ </tr>
+[%
+ FOREACH type IN mfhd.keys;
+ NEXT UNLESS serial.$type.size;
+%]
+ <tr>
+ <td class="rdetail-mfhd-type">[% mfhd.$type %]</td>
+ <td class="rdetail-mfhd-contents">[%
+ FOR thing IN serial.$type;
+ thing.join(", ");
+ END %]</td>
+ </tr>
+ [% END %]
+ <tr>
+ <td class="rdetail-mfhd-foot" colspan="2"> </td>
+ </tr>
+ [% END %]
+ </tbody></table>
+ </div>
+[% END %]
<div class='rdetail_extras_div'>
-[%
-base_expando = ctx.full_path _ "?expand=issues";
-FOREACH type IN ctx.holding_summaries.keys;
- NEXT UNLESS ctx.holding_summaries.$type.size;
- expanded = CGI.param('expand_holding_type') == type; %]
- <div class="rdetail-issue-type">
- <a href="[% base_expando; expanded ? '' : '&expand_holding_type=' _ type; %]#issues">[[% expanded ? '-' : '+' %]]</a>
- [% ctx.holding_summaries.$type.join(", ") %]
- [% IF expanded %]
- <table>
- [% FOR blob IN ctx.expanded_holdings %]
- <tr>
- <td class="rdetail-issue-issue">[% blob.issuance.label | html %]</td>
- [% IF blob.has_units %]
- <td class="rdetail-issue-place-hold">
- <a href="[% mkurl(ctx.opac_root _ '/place_hold',
- {hold_target => blob.issuance.id, hold_type => 'I', hold_source_page => mkurl()}) %]">[% l("Place Hold") %]</a>
- </td>
- [% END %]
- </tr>
- [% END %]
- </table>
- [% END %]
- </div>
-[% END %]
-[% IF ctx.mfhd_summaries.size; %]
- <div class="rdetail-mfhd-holdings">
- <table><tbody>
-[%
- mfhd = {
- basic_holdings = l('Volumes'),
- basic_holdings_add = l('Additional Volume Information'),
- supplement_holdings = l('Supplements'),
- supplement_holdings_add = l('Additional Supplement Information'),
- index_holdings = l('Indexes'),
- index_holdings_add = l('Additional Index Information'),
- online = l('Online'),
- missing = l('Missing'),
- incomplete = l('Incomplete'),
- };
-
- FOREACH serial IN ctx.mfhd_summaries;
-%]
- <tr>
- <td class="rdetail-mfhd-head" colspan="2">[% l('Holdings summary ([_1])', serial.location) %]</td>
- </tr>
-[%
- FOREACH type IN mfhd.keys;
- NEXT UNLESS serial.$type.size;
-%]
- <tr>
- <td class="rdetail-mfhd-type">[% mfhd.$type %]</td>
- <td class="rdetail-mfhd-contents">[%
- FOR thing IN serial.$type;
- thing.join(", ");
- END %]</td>
- </tr>
- [% END %]
- <tr>
- <td class="rdetail-mfhd-foot" colspan="2"> </td>
- </tr>
- [% END %]
- </tbody></table>
- </div>
-[% END %]
+ [% INCLUDE 'opac/parts/record/issues-db.tt2' # "new" serials holdings %]
+ [% INCLUDE 'opac/parts/record/issues-mfhd.tt2' # mfhd-based "classic" serials %]
</div>
ctx.record_hold_count, ctx.copy_summary.0.count) %]
</p>
</span>
-[%- INCLUDE "opac/parts/record/copy_table.tt2" %]
+[%- INCLUDE "opac/parts/record/copy_table.tt2" copies=ctx.copies %]
</div>
[%- END %]
new dijit.form.TextBox({
"disabled": true, "value": sub_id
});
+
+ var _display_grouping_store = new dojo.data.ItemFileReadStore({
+ "data": {
+ "identifier": "display_grouping",
+ "label": "label",
+ "items": [
+ {"display_grouping": "chron",
+ "label": "Chronology"},
+ {"display_grouping": "enum",
+ "label": "Enumeration"}
+ ]
+ }
+ });
+
+ var settings = fieldmapper.aou.fetchOrgSettingBatch(
+ openils.User.user.ws_ou(),
+ ["serial.default_display_grouping"]
+ );
+ var default_display_grouping = "chron";
+
+ if (settings && settings["serial.default_display_grouping"]) {
+ default_display_grouping =
+ settings["serial.default_display_grouping"].value;
+ }
+
+ dist_grid.overrideEditWidgets.display_grouping =
+ new dijit.form.FilteringSelect({
+ "store": _display_grouping_store,
+ "searchAttr": "label",
+ "name": "display_grouping"
+ });
+ dist_grid.overrideEditWidgets.display_grouping.shove = {
+ "create": default_display_grouping
+ };
}
</script>
[% INCLUDE "serial/subscription/distribution.tt2" %]
line-height: normal;
text-decoration: none;
}
+.rdetail-holding-group { margin-left: 1.5em; }
+.rdetail-holding-group span { margin-left: 1.5em; }
+.rdetail-holding-group .paging { margin-left: 1.5em; }