use Digest::MD5 qw/md5_hex/;
use Apache2::Const -compile => qw/OK/;
use MARC::Record;
+use List::Util qw/first/;
#use Data::Dumper;
#$Data::Dumper::Indent = 0;
no warnings 'uninitialized';
- # XXX TODO add config.global_flag rows for browse limit-limit and
- # browse offset-limit?
-
- my $limit = int($self->cgi->param('blimit') || 10);
- my $offset = int($self->cgi->param('boffset') || 0);
- my $force_backward = scalar($self->cgi->param('bback'));
+ # XXX TODO add config.global_flag rows for browse limit-limit ?
my @params = (
scalar($self->cgi->param('qtype')),
$self->ctx->{copy_location_group},
$self->ctx->{is_staff} ? 't' : 'f',
scalar($self->cgi->param('bpivot')),
- $force_backward ? 't' : 'f'
+ int(
+ $self->cgi->param('blimit') ||
+ $self->ctx->{opac_hits_per_page} || 10
+ )
);
- # We do need $limit, $offset, and $force_backward as part of the
- # cache key, but we also need to keep them separate from other
- # parameters for purposes of paging link generation.
return (
- "oils_browse_" . md5_hex(
- OpenSRF::Utils::JSON->perl2JSON(
- [@params, $limit, $offset, $force_backward]
- )
- ),
- $limit, $offset, $force_backward, @params
+ "oils_browse_" . md5_hex(OpenSRF::Utils::JSON->perl2JSON(\@params)),
+ @params
);
}
-# Break out any Public General Notes (field 680) for display and
-# hyperlinking. These are sometimes (erroneously?) called "scope notes."
-# I say erroneously, tentatively, because LoC doesn't seem to document
-# a "scope notes" field for authority records, while it does so for
-# classification records, which are something else. But I am not a
-# librarian.
+# Break out any Public General Notes (field 680) for display. These are
+# sometimes (erroneously?) called "scope notes." I say erroneously,
+# tentatively, because LoC doesn't seem to document a "scope notes"
+# field for authority records, while it does so for classification
+# records, which are something else. But I am not a librarian.
sub extract_public_general_notes {
my ($self, $record, $row) = @_;
- my @notes;
- foreach my $note ($record->field('680')) {
- my @note;
- my $last_heading;
-
- foreach my $subfield ($note->subfields) {
- my ($code, $value) = @$subfield;
-
- if ($code eq 'i') {
- push @note, $value;
- } elsif ($code eq '5') {
- if ($last_heading) {
- my $org = $self->ctx->{get_aou_by_shortname}->($value);
- $last_heading->{org_id} = $org->id if $org;
- }
- push @note, { institution => $value };
- } elsif ($code eq 'a') {
- $last_heading = {
- heading => $value, bterm => search_normalize($value)
- };
- push @note, $last_heading;
- }
- }
-
- push @notes, \@note if @note;
- }
-
- $row->{notes} = \@notes;
+ # Make a list of strings, each string being a concatentation of any
+ # subfields 'i', '5', or 'a' from one field 680, in order of appearance.
+ $row->{notes} = [
+ map {
+ join(
+ " ",
+ map { $_->[1] } grep { $_->[0] =~ /[i5a]/ } $_->subfields
+ )
+ } $record->field('680')
+ ];
}
sub find_authority_headings_and_notes {
}
sub load_browse_impl {
- my ($self, $limit, $offset, $force_backward, @params) = @_;
-
- my $inner_limit = ($offset >= 0 and not $force_backward) ?
- $limit + 1 : $limit;
+ my ($self, @params) = @_;
my $results = $self->editor->json_query({
- from => [
- "metabib.browse", (@params, $inner_limit, $offset)
- ]
+ from => [ "metabib.browse", @params ]
});
if (not $results) { # DB error, not empty result set.
return $results;
}
-# $results can be modified by this function. This would be simpler
-# but for the moving pivot concept that helps us avoid paging with
-# large offsets (slow).
+# Find paging information, put it into $self->ctx, and return "real"
+# rows from $results, excluding those that contain only paging
+# information.
sub infer_browse_paging {
- my ($self, $results, $limit, $offset, $force_backward) = @_;
-
- # (All these comments assume a default limit of 10). For typical
- # not-backwards requests not at the end of the result set, we
- # should have an eleventh result that tells us what's next.
- while (scalar @$results > $limit) {
- $self->ctx->{forward_pivot} = (pop @$results)->{browse_entry};
- $self->ctx->{more_forward} = 1;
- }
-
- # If we're going backwards by pivot id, we don't have an eleventh
- # result to tell us we can page forward, but we can assume we can
- # go forward because duh, we followed a link backward to get here.
- if ($force_backward and $self->cgi->param('bpivot')) {
- $self->ctx->{forward_pivot} = scalar($self->cgi->param('bpivot'));
- $self->ctx->{more_forward} = 1;
- }
-
- # The pivot that the user can use for going backwards is the first
- # of the result set.
- if (@$results) {
- $self->ctx->{back_pivot} = $results->[0]->{browse_entry};
- }
-
- # The result of these tests relate to basic limit/offset paging.
+ my ($self, $results) = @_;
- # This comparison for setting more_forward does not fold into
- # those for setting more_back.
- if ($offset < 0 || $force_backward) {
- $self->ctx->{more_forward} = 1;
+ foreach (@$results) {
+ if ($_->{pivot_point}) {
+ if ($_->{row_number} < 0) { # sic
+ $self->ctx->{forward_pivot} = $_->{pivot_point};
+ } else {
+ $self->ctx->{back_pivot} = $_->{pivot_point};
+ }
+ }
}
- if ($offset > 0 or (!$force_backward and $self->cgi->param('bpivot'))) {
- $self->ctx->{more_back} = 1;
- } elsif (scalar @$results < $limit) {
- $self->ctx->{more_back} = 0;
- } elsif (not($self->cgi->param('bterm') eq '0' and
- not defined $self->cgi->param('bpivot'))) { # paging links
- $self->ctx->{more_back} = 1;
- }
+ return [ grep { not defined $_->{pivot_point} } @$results ];
}
sub leading_article_test {
if ($map->{$qtype}) {
if ($bterm =~ qr/$map->{$qtype}/i) {
$self->ctx->{browse_leading_article_warning} = 1;
+ ($self->ctx->{browse_leading_article_alternative} = $bterm) =~
+ s/$map->{$qtype}//;
}
}
};
_init_browse_cache();
- $self->ctx->{more_forward} = 0;
- $self->ctx->{more_back} = 0;
+ # If there's a user logged in, flesh extended user info so we can get
+ # her opac.hits_per_page setting, if any.
+ if ($self->ctx->{user}) {
+ $self->prepare_extended_user_info('settings');
+ if (my $setting = first { $_->name eq 'opac.hits_per_page' }
+ @{$self->ctx->{user}->settings}) {
+
+ $self->ctx->{opac_hits_per_page} =
+ int(OpenSRF::Utils::JSON->JSON2perl($setting->value));
+ }
+ }
if ($self->cgi->param('qtype') and defined $self->cgi->param('bterm')) {
$self->cgi->param('bterm')
);
- my ($cache_key, $limit, $offset, $force_backward, @params) =
- $self->prepare_browse_parameters;
+ my ($cache_key, @params) = $self->prepare_browse_parameters;
my $results = $browse_cache->get_cache($cache_key);
if (not $results) {
- $results = $self->load_browse_impl(
- $limit, $offset, $force_backward, @params
- );
+ $results = $self->load_browse_impl(@params);
if ($results) {
$browse_cache->put_cache($cache_key, $results, $browse_timeout);
}
}
if ($results) {
- $self->infer_browse_paging(
- $results, $limit, $offset, $force_backward
- );
- $self->ctx->{browse_results} = $results;
+ $self->ctx->{browse_results} = $self->infer_browse_paging($results);
}
# We don't need an else clause to send the user a 5XX error or
authorities TEXT,
sources INT, -- visible ones, that is
row_number INT, -- internal use, sort of
- accurate BOOL -- Count in sources field is accurate? Not
+ accurate BOOL, -- Count in sources field is accurate? Not
-- if we had more than a browse superpage
-- of records to look at.
+ pivot_point BIGINT
);
$p$ LANGUAGE PLPGSQL;
CREATE OR REPLACE FUNCTION metabib.staged_browse(
- core_query TEXT,
+ query TEXT,
context_org INT,
context_locations INT[],
staff BOOL,
browse_superpage_size INT,
+ count_up_from_zero BOOL, -- if false, count down from -1
result_limit INT,
- use_offset INT
+ next_pivot_pos INT
) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
DECLARE
- core_cursor REFCURSOR;
- core_record RECORD;
+ curs REFCURSOR;
+ rec RECORD;
qpfts_query TEXT;
result_row metabib.flat_browse_entry_appearance%ROWTYPE;
results_skipped INT := 0;
- results_returned INT := 0;
+ row_counter INT := 0;
+ row_number INT;
slice_start INT;
slice_end INT;
full_end INT;
superpage_of_records BIGINT[];
superpage_size INT;
BEGIN
- OPEN core_cursor FOR EXECUTE core_query;
+ IF count_up_from_zero THEN
+ row_number := 0;
+ ELSE
+ row_number := -1;
+ END IF;
+
+ OPEN curs FOR EXECUTE query;
LOOP
- FETCH core_cursor INTO core_record;
- EXIT WHEN NOT FOUND;
+ FETCH curs INTO rec;
+ IF NOT FOUND THEN
+ IF result_row.pivot_point IS NOT NULL THEN
+ RETURN NEXT result_row;
+ END IF;
+ RETURN;
+ END IF;
result_row.sources := 0;
- full_end := ARRAY_LENGTH(core_record.records, 1);
+ full_end := ARRAY_LENGTH(rec.records, 1);
superpage_size := COALESCE(browse_superpage_size, full_end);
slice_start := 1;
slice_end := superpage_size;
WHILE result_row.sources = 0 AND slice_start <= full_end LOOP
- superpage_of_records := core_record.records[slice_start:slice_end];
+ superpage_of_records := rec.records[slice_start:slice_end];
qpfts_query :=
'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
'1::INT AS rel FROM (SELECT UNNEST(' ||
browse_superpage_size >= full_end;
IF result_row.sources > 0 THEN
- IF results_skipped < use_offset THEN
- results_skipped := results_skipped + 1;
- CONTINUE;
- END IF;
+ -- We've got a browse entry with visible holdings. Yay.
+
- result_row.browse_entry := core_record.id;
- result_row.authorities := core_record.authorities;
- result_row.fields := core_record.fields;
- result_row.value := core_record.value;
+ -- The function that calls this function needs row_number in order
+ -- to correctly order results from two different runs of this
+ -- functions.
+ result_row.row_number := row_number;
- -- This is needed so our caller can flip it and reverse it.
- result_row.row_number := results_returned;
+ -- Now, if row_counter is still less than limit, return a row. If
+ -- not, but it is less than next_pivot_pos, continue on without
+ -- returning actual result rows until we find
+ -- that next pivot, and return it.
- RETURN NEXT result_row;
+ IF row_counter < result_limit THEN
+ result_row.browse_entry := rec.id;
+ result_row.authorities := rec.authorities;
+ result_row.fields := rec.fields;
+ result_row.value := rec.value;
- results_returned := results_returned + 1;
+ RETURN NEXT result_row;
+ ELSE
+ result_row.browse_entry := NULL;
+ result_row.authorities := NULL;
+ result_row.fields := NULL;
+ result_row.value := NULL;
+ result_row.sources := NULL;
+ result_row.accurate := NULL;
+ result_row.pivot_point := rec.id;
+
+ IF row_counter >= next_pivot_pos THEN
+ RETURN NEXT result_row;
+ RETURN;
+ END IF;
+ END IF;
- EXIT WHEN results_returned >= result_limit;
+ IF count_up_from_zero THEN
+ row_number := row_number + 1;
+ ELSE
+ row_number := row_number - 1;
+ END IF;
+
+ -- row_counter is different from row_number.
+ -- It simply counts up from zero so that we know when
+ -- we've reached our limit.
+ row_counter := row_counter + 1;
END IF;
END LOOP;
END;
$p$ LANGUAGE PLPGSQL;
--- This is optimized to be fast for values of result_offset near zero.
+
CREATE OR REPLACE FUNCTION metabib.browse(
search_field INT[],
browse_term TEXT,
context_loc_group INT DEFAULT NULL,
staff BOOL DEFAULT FALSE,
pivot_id BIGINT DEFAULT NULL,
- force_backward BOOL DEFAULT FALSE,
- result_limit INT DEFAULT 10,
- result_offset INT DEFAULT 0 -- Can be negative!
+ result_limit INT DEFAULT 10
) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
DECLARE
core_query TEXT;
- whole_query TEXT;
+ back_query TEXT;
+ forward_query TEXT;
pivot_sort_value TEXT;
pivot_sort_fallback TEXT;
context_locations INT[];
- use_offset INT;
browse_superpage_size INT;
results_skipped INT := 0;
+ back_limit INT;
+ back_to_pivot INT;
+ forward_limit INT;
+ forward_to_pivot INT;
BEGIN
+ -- First, find the pivot if we were given a browse term but not a pivot.
IF pivot_id IS NULL THEN
pivot_id := metabib.browse_pivot(search_field, browse_term);
END IF;
SELECT INTO pivot_sort_value, pivot_sort_fallback
sort_value, value FROM metabib.browse_entry WHERE id = pivot_id;
+ -- Bail if we couldn't find a pivot.
IF pivot_sort_value IS NULL THEN
RETURN;
END IF;
+ -- Transform the context_loc_group argument (if any) (logc at the
+ -- TPAC layer) into a form we'll be able to use.
IF context_loc_group IS NOT NULL THEN
SELECT INTO context_locations ARRAY_AGG(location)
FROM asset.copy_location_group_map
WHERE lgroup = context_loc_group;
END IF;
+ -- Get the configured size of browse superpages.
SELECT INTO browse_superpage_size value -- NULL ok
FROM config.global_flag
WHERE enabled AND name = 'opac.browse.holdings_visibility_test_limit';
+ -- First we're going to search backward from the pivot, then we're going
+ -- to search forward. In each direction, we need two limits. At the
+ -- lesser of the two limits, we delineate the edge of the result set
+ -- we're going to return. At the greater of the two limits, we find the
+ -- pivot value that would represent an offset from the current pivot
+ -- at a distance of one "page" in either direction, where a "page" is a
+ -- result set of the size specified in the "result_limit" argument.
+ --
+ -- The two limits in each direction make four derived values in total,
+ -- and we calculate them now.
+ back_limit := CEIL(result_limit::FLOAT / 2);
+ back_to_pivot := result_limit;
+ forward_limit := result_limit / 2;
+ forward_to_pivot := result_limit - 1;
+
+ -- This is the meat of the SQL query that finds browse entries. We'll
+ -- pass this to a function which uses it with a cursor, so that individual
+ -- rows may be fetched in a loop until some condition is satisfied, without
+ -- waiting for a result set of fixed size to be collected all at once.
core_query := '
SELECT
mbe.id,
)
WHERE ';
- -- PostgreSQL is not magic. We can't actually pass a negative offset.
- IF result_offset >= 0 AND NOT force_backward THEN
- use_offset := result_offset;
- core_query := core_query ||
- ' mbe.sort_value >= ' || quote_literal(pivot_sort_value) ||
- ' GROUP BY 1,2,3 ORDER BY mbe.sort_value, mbe.value ';
-
- RETURN QUERY SELECT * FROM metabib.staged_browse(
- core_query, context_org, context_locations,
- staff, browse_superpage_size, result_limit, use_offset
- );
- ELSE
- -- Part 1 of 2 to deliver what the user wants with a negative offset:
- core_query := core_query ||
- ' mbe.sort_value < ' || quote_literal(pivot_sort_value) ||
- ' GROUP BY 1,2,3 ORDER BY mbe.sort_value DESC, mbe.value DESC ';
-
- -- Part 2 of 2 to deliver what the user wants with a negative offset:
- RETURN QUERY SELECT * FROM (SELECT * FROM metabib.staged_browse(
- core_query, context_org, context_locations,
- staff, browse_superpage_size, result_limit, use_offset
- )) sb ORDER BY row_number DESC;
+ -- This is the variant of the query for browsing backward.
+ back_query := core_query ||
+ ' mbe.sort_value <= ' || quote_literal(pivot_sort_value) ||
+ ' GROUP BY 1,2,3 ORDER BY mbe.sort_value DESC, mbe.value DESC ';
+
+ -- This variant browses forward.
+ forward_query := core_query ||
+ ' mbe.sort_value > ' || quote_literal(pivot_sort_value) ||
+ ' GROUP BY 1,2,3 ORDER BY mbe.sort_value, mbe.value ';
+
+ -- We now call the function which applies a cursor to the provided
+ -- queries, stopping at the appropriate limits and also giving us
+ -- the next page's pivot.
+ RETURN QUERY
+ SELECT * FROM metabib.staged_browse(
+ back_query, context_org, context_locations,
+ staff, browse_superpage_size, TRUE, back_limit, back_to_pivot
+ ) UNION
+ SELECT * FROM metabib.staged_browse(
+ forward_query, context_org, context_locations,
+ staff, browse_superpage_size, FALSE, forward_limit, forward_to_pivot
+ ) ORDER BY row_number DESC;
- END IF;
END;
$p$ LANGUAGE PLPGSQL;
context_loc_group INT DEFAULT NULL,
staff BOOL DEFAULT FALSE,
pivot_id BIGINT DEFAULT NULL,
- force_backward BOOL DEFAULT FALSE,
- result_limit INT DEFAULT 10,
- result_offset INT DEFAULT 0 -- Can be negative, implying backward!
+ result_limit INT DEFAULT 10
) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
BEGIN
RETURN QUERY SELECT * FROM metabib.browse(
context_loc_group,
staff,
pivot_id,
- force_backward,
- result_limit,
- result_offset
+ result_limit
);
END;
$p$ LANGUAGE PLPGSQL;
-
COMMIT;
authorities TEXT,
sources INT, -- visible ones, that is
row_number INT, -- internal use, sort of
- accurate BOOL -- Count in sources field is accurate? Not
+ accurate BOOL, -- Count in sources field is accurate? Not
-- if we had more than a browse superpage
-- of records to look at.
+ pivot_point BIGINT
);
$p$ LANGUAGE PLPGSQL;
CREATE OR REPLACE FUNCTION metabib.staged_browse(
- core_query TEXT,
+ query TEXT,
context_org INT,
context_locations INT[],
staff BOOL,
browse_superpage_size INT,
+ count_up_from_zero BOOL, -- if false, count down from -1
result_limit INT,
- use_offset INT
+ next_pivot_pos INT
) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
DECLARE
- core_cursor REFCURSOR;
- core_record RECORD;
+ curs REFCURSOR;
+ rec RECORD;
qpfts_query TEXT;
result_row metabib.flat_browse_entry_appearance%ROWTYPE;
results_skipped INT := 0;
- results_returned INT := 0;
+ row_counter INT := 0;
+ row_number INT;
slice_start INT;
slice_end INT;
full_end INT;
superpage_of_records BIGINT[];
superpage_size INT;
BEGIN
- OPEN core_cursor FOR EXECUTE core_query;
+ IF count_up_from_zero THEN
+ row_number := 0;
+ ELSE
+ row_number := -1;
+ END IF;
+
+ OPEN curs FOR EXECUTE query;
LOOP
- FETCH core_cursor INTO core_record;
- EXIT WHEN NOT FOUND;
+ FETCH curs INTO rec;
+ IF NOT FOUND THEN
+ IF result_row.pivot_point IS NOT NULL THEN
+ RETURN NEXT result_row;
+ END IF;
+ RETURN;
+ END IF;
result_row.sources := 0;
- full_end := ARRAY_LENGTH(core_record.records, 1);
+ full_end := ARRAY_LENGTH(rec.records, 1);
superpage_size := COALESCE(browse_superpage_size, full_end);
slice_start := 1;
slice_end := superpage_size;
WHILE result_row.sources = 0 AND slice_start <= full_end LOOP
- superpage_of_records := core_record.records[slice_start:slice_end];
+ superpage_of_records := rec.records[slice_start:slice_end];
qpfts_query :=
'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
'1::INT AS rel FROM (SELECT UNNEST(' ||
browse_superpage_size >= full_end;
IF result_row.sources > 0 THEN
- IF results_skipped < use_offset THEN
- results_skipped := results_skipped + 1;
- CONTINUE;
- END IF;
+ -- We've got a browse entry with visible holdings. Yay.
- result_row.browse_entry := core_record.id;
- result_row.authorities := core_record.authorities;
- result_row.fields := core_record.fields;
- result_row.value := core_record.value;
- -- This is needed so our caller can flip it and reverse it.
- result_row.row_number := results_returned;
+ -- The function that calls this function needs row_number in order
+ -- to correctly order results from two different runs of this
+ -- functions.
+ result_row.row_number := row_number;
- RETURN NEXT result_row;
+ -- Now, if row_counter is still less than limit, return a row. If
+ -- not, but it is less than next_pivot_pos, continue on without
+ -- returning actual result rows until we find
+ -- that next pivot, and return it.
- results_returned := results_returned + 1;
+ IF row_counter < result_limit THEN
+ result_row.browse_entry := rec.id;
+ result_row.authorities := rec.authorities;
+ result_row.fields := rec.fields;
+ result_row.value := rec.value;
- EXIT WHEN results_returned >= result_limit;
+ RETURN NEXT result_row;
+ ELSE
+ result_row.browse_entry := NULL;
+ result_row.authorities := NULL;
+ result_row.fields := NULL;
+ result_row.value := NULL;
+ result_row.sources := NULL;
+ result_row.accurate := NULL;
+ result_row.pivot_point := rec.id;
+
+ IF row_counter >= next_pivot_pos THEN
+ RETURN NEXT result_row;
+ RETURN;
+ END IF;
+ END IF;
+
+ IF count_up_from_zero THEN
+ row_number := row_number + 1;
+ ELSE
+ row_number := row_number - 1;
+ END IF;
+
+ -- row_counter is different from row_number.
+ -- It simply counts up from zero so that we know when
+ -- we've reached our limit.
+ row_counter := row_counter + 1;
END IF;
END LOOP;
END;
$p$ LANGUAGE PLPGSQL;
--- This is optimized to be fast for values of result_offset near zero.
+
CREATE OR REPLACE FUNCTION metabib.browse(
search_field INT[],
browse_term TEXT,
context_loc_group INT DEFAULT NULL,
staff BOOL DEFAULT FALSE,
pivot_id BIGINT DEFAULT NULL,
- force_backward BOOL DEFAULT FALSE,
- result_limit INT DEFAULT 10,
- result_offset INT DEFAULT 0 -- Can be negative!
+ result_limit INT DEFAULT 10
) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
DECLARE
core_query TEXT;
- whole_query TEXT;
+ back_query TEXT;
+ forward_query TEXT;
pivot_sort_value TEXT;
pivot_sort_fallback TEXT;
context_locations INT[];
- use_offset INT;
browse_superpage_size INT;
results_skipped INT := 0;
+ back_limit INT;
+ back_to_pivot INT;
+ forward_limit INT;
+ forward_to_pivot INT;
BEGIN
+ -- First, find the pivot if we were given a browse term but not a pivot.
IF pivot_id IS NULL THEN
pivot_id := metabib.browse_pivot(search_field, browse_term);
END IF;
SELECT INTO pivot_sort_value, pivot_sort_fallback
sort_value, value FROM metabib.browse_entry WHERE id = pivot_id;
+ -- Bail if we couldn't find a pivot.
IF pivot_sort_value IS NULL THEN
RETURN;
END IF;
+ -- Transform the context_loc_group argument (if any) (logc at the
+ -- TPAC layer) into a form we'll be able to use.
IF context_loc_group IS NOT NULL THEN
SELECT INTO context_locations ARRAY_AGG(location)
FROM asset.copy_location_group_map
WHERE lgroup = context_loc_group;
END IF;
+ -- Get the configured size of browse superpages.
SELECT INTO browse_superpage_size value -- NULL ok
FROM config.global_flag
WHERE enabled AND name = 'opac.browse.holdings_visibility_test_limit';
+ -- First we're going to search backward from the pivot, then we're going
+ -- to search forward. In each direction, we need two limits. At the
+ -- lesser of the two limits, we delineate the edge of the result set
+ -- we're going to return. At the greater of the two limits, we find the
+ -- pivot value that would represent an offset from the current pivot
+ -- at a distance of one "page" in either direction, where a "page" is a
+ -- result set of the size specified in the "result_limit" argument.
+ --
+ -- The two limits in each direction make four derived values in total,
+ -- and we calculate them now.
+ back_limit := CEIL(result_limit::FLOAT / 2);
+ back_to_pivot := result_limit;
+ forward_limit := result_limit / 2;
+ forward_to_pivot := result_limit - 1;
+
+ -- This is the meat of the SQL query that finds browse entries. We'll
+ -- pass this to a function which uses it with a cursor, so that individual
+ -- rows may be fetched in a loop until some condition is satisfied, without
+ -- waiting for a result set of fixed size to be collected all at once.
core_query := '
SELECT
mbe.id,
)
WHERE ';
- -- PostgreSQL is not magic. We can't actually pass a negative offset.
- IF result_offset >= 0 AND NOT force_backward THEN
- use_offset := result_offset;
- core_query := core_query ||
- ' mbe.sort_value >= ' || quote_literal(pivot_sort_value) ||
- ' GROUP BY 1,2,3 ORDER BY mbe.sort_value, mbe.value ';
-
- RETURN QUERY SELECT * FROM metabib.staged_browse(
- core_query, context_org, context_locations,
- staff, browse_superpage_size, result_limit, use_offset
- );
- ELSE
- -- Part 1 of 2 to deliver what the user wants with a negative offset:
- core_query := core_query ||
- ' mbe.sort_value < ' || quote_literal(pivot_sort_value) ||
- ' GROUP BY 1,2,3 ORDER BY mbe.sort_value DESC, mbe.value DESC ';
-
- -- Part 2 of 2 to deliver what the user wants with a negative offset:
- RETURN QUERY SELECT * FROM (SELECT * FROM metabib.staged_browse(
- core_query, context_org, context_locations,
- staff, browse_superpage_size, result_limit, use_offset
- )) sb ORDER BY row_number DESC;
+ -- This is the variant of the query for browsing backward.
+ back_query := core_query ||
+ ' mbe.sort_value <= ' || quote_literal(pivot_sort_value) ||
+ ' GROUP BY 1,2,3 ORDER BY mbe.sort_value DESC, mbe.value DESC ';
+
+ -- This variant browses forward.
+ forward_query := core_query ||
+ ' mbe.sort_value > ' || quote_literal(pivot_sort_value) ||
+ ' GROUP BY 1,2,3 ORDER BY mbe.sort_value, mbe.value ';
+
+ -- We now call the function which applies a cursor to the provided
+ -- queries, stopping at the appropriate limits and also giving us
+ -- the next page's pivot.
+ RETURN QUERY
+ SELECT * FROM metabib.staged_browse(
+ back_query, context_org, context_locations,
+ staff, browse_superpage_size, TRUE, back_limit, back_to_pivot
+ ) UNION
+ SELECT * FROM metabib.staged_browse(
+ forward_query, context_org, context_locations,
+ staff, browse_superpage_size, FALSE, forward_limit, forward_to_pivot
+ ) ORDER BY row_number DESC;
- END IF;
END;
$p$ LANGUAGE PLPGSQL;
context_loc_group INT DEFAULT NULL,
staff BOOL DEFAULT FALSE,
pivot_id BIGINT DEFAULT NULL,
- force_backward BOOL DEFAULT FALSE,
- result_limit INT DEFAULT 10,
- result_offset INT DEFAULT 0 -- Can be negative, implying backward!
+ result_limit INT DEFAULT 10
) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
BEGIN
RETURN QUERY SELECT * FROM metabib.browse(
context_loc_group,
staff,
pivot_id,
- force_backward,
- result_limit,
- result_offset
+ result_limit
);
END;
$p$ LANGUAGE PLPGSQL;
INCLUDE "opac/parts/topnav.tt2";
ctx.page_title = l("Browse the Catalog");
- blimit = CGI.param('blimit') || 10;
- boffset = CGI.param('boffset') || 0;
+ blimit = CGI.param('blimit') || ctx.opac_hits_per_page || 10;
- depart_list = ['blimit', 'bterm', 'boffset', 'bpivot', 'bback'];
+ depart_list = ['blimit', 'bterm', 'bpivot'];
%]
<div id="search-wrapper">
<div id="main-content">
<div id="browse-the-catalog">
<div id="browse-controls">
- <form method="get">
+ <form method="get" onsubmit="$('browse-submit-spinner').className = ''; return true">
<input type="hidden" name="blimit"
value="[% blimit %]" />
[% control_qtype = INCLUDE "opac/parts/qtype_selector.tt2"
- id="browse-search-class" browse_only=1 %]
+ id="browse-search-class" browse_only=1 plural=1 %]
[% control_bterm = BLOCK %]<input type="text" name="bterm" id="browse-term"
value="[% CGI.param('bterm') | html %]" />[% END %]
[% control_locg = INCLUDE build_org_selector id='browse-context'
show_loc_groups=1
arialabel=l('Select holding library') %]
- [% l('Browse by [_1] for [_2] held under [_3]', control_qtype, control_bterm, control_locg) %]
+ [% l('Browse for [_1] starting with [_2] in [_3]', control_qtype, control_bterm, control_locg) %]
<input type="submit" value="[% l('Go') %]" />
+ <img id="browse-submit-spinner" src="[% ctx.media_prefix %]/opac/images/progressbar_green.gif" class="hidden" style="width: 16px; height: 16px;" alt="" />
</form>
</div>
[% BLOCK browse_pager %]
<div class="browse-pager">
- [% IF ctx.more_back %]
- <a class="opac-button" href="[% mkurl('', {bpivot => ctx.back_pivot, bback => 1}) %]">← [%l ('Back') %]</a>
+ [% IF ctx.back_pivot %]
+ <a class="opac-button" href="[% mkurl('', {bpivot => ctx.back_pivot}) %]" onclick="$('browse-pager-spinner-[% id %]').className = '';">← [%l ('Back') %]</a>
[% END %]
[% IF browse.english_pager; # XXX how to apply i18n here?
current_qtype = CGI.param('qtype') || 'title' %]
<span class="browse-shortcuts">
- <a href="[% mkurl('', {qtype => current_qtype, bterm => '0'}, ['boffset','bpivot','bback']) %]">0-9</a>
+ <a href="[% mkurl('', {qtype => current_qtype, bterm => '0'}, ['bpivot']) %]">0-9</a>
[% FOR letter IN ['A'..'Z'] %]
- <a href="[% mkurl('', {qtype => current_qtype, bterm => letter}, ['boffset','bpivot','bback']) %]">[% letter %]</a>
+ <a href="[% mkurl('', {qtype => current_qtype, bterm => letter}, ['bpivot']) %]">[% letter %]</a>
[% END %]
</span>
[% END %]
- [% IF ctx.more_forward %]
- <a class="opac-button" href="[% mkurl('', {bpivot => ctx.forward_pivot}, ['bback']) %]">[%l ('Forward') %] →</a>
+ [% IF ctx.forward_pivot %]
+ <a class="opac-button" href="[% mkurl('', {bpivot => ctx.forward_pivot}) %]" onclick="$('browse-pager-spinner-[% id %]').className = '';">[%l ('Next') %] →</a>
[% END %]
+
+ <img id="browse-pager-spinner-[% id %]" src="[% ctx.media_prefix %]/opac/images/progressbar_green.gif" class="hidden" style="width: 16px; height: 16px;" alt="" />
</div>
[% END %]
- [% PROCESS browse_pager %]
+ [% PROCESS browse_pager id=0 %]
<div id="browse-results">
[% IF ctx.browse_error %]
[% ELSE %]
[% IF ctx.browse_leading_article_warning %]
<div class="browse-leading-article-warning">
- [% l("Your browse term seems to begin with an article. You might get better results by omitting the article.") %]
+ [% l("Your browse term seems to begin with an article (a, an, the). You might get better results by omitting the article.") %]
+ [% IF ctx.browse_leading_article_alternative %]
+ <p>
+ [% alternative_link = BLOCK %]
+ <a href="[% mkurl('', {bterm => ctx.browse_leading_article_alternative}, ['bpivot']) %]">[% ctx.browse_leading_article_alternative | html %]</a>
+ [%- END; # alternative_link BLOCK
+ l("Did you mean [_1]?", alternative_link);
+ END # IF %]
+ </p>
</div>
[% END %]
- <ul class="browse-result-list">
+
+ <ol class="browse-result-list">
[% FOR result IN ctx.browse_results %]
<li class="browse-result">
- <span class="browse-result-value">
+ <span class="browse-result-value[% result.row_number == 0 && !CGI.param('bpivot') ? ' browse-result-best-match' : '' %]">
<a href="[% mkurl(
ctx.opac_root _ '/results', {
'fi:has_browse_entry' => (result.browse_entry _ ',' _ result.fields)
[% END %]
</div>
- [% PROCESS browse_pager %]
+ [% PROCESS browse_pager id=1 %]
</div>
<div class="common-full-pad"></div>
[% l("Note:") %]
</span>
<span class="browse-public-general-note-body">
- [% FOR piece IN note;
- IF piece.heading;
- mkurl_args = {bterm => piece.bterm};
- IF piece.org_id;
- mkurl_args.locg = piece.org_id;
- END;
- %]
- <a href="[% mkurl('', mkurl_args, ['boffset','bpivot','bback']) %]">[% piece.heading | html %]</a>
- [% ELSIF piece.institution %]
- <span class="browse-public-general-note-institution">
- [% piece.institution | html %]
- </span>
- [% ELSE %]
- [% piece | html %]
- [% END;
- END %]
+ [% FOR piece IN note; piece | html; END %]
</span>
</div>
[% END;
.browse-result-sources, .browse-result-authority-bib-links {
margin-left: 1em;
}
+.browse-result-best-match {
+ font-weight: bold;
+}
.browse-pager {
margin: 2ex 0;
}
.browse-result-list {
- list-style-type: square;
+ padding-bottom: 0.5ex;
}
.browse-shortcuts {
font-size: 120%;
[% query_types = [
{value => "keyword", label => l("Keyword")},
- {value => "title", label => l("Title"), browse => 1},
+ {value => "title", label => l("Title"), plural_label => l("Titles"), browse => 1},
{value => "jtitle", label => l("Journal Title")},
- {value => "author", label => l("Author"), browse => 1},
- {value => "subject", label => l("Subject"), browse => 1},
- {value => "series", label => l("Series"), browse => 1},
+ {value => "author", label => l("Author"), plural_label => l("Authors"), browse => 1},
+ {value => "subject", label => l("Subject"), plural_label => l("Subjects"), browse => 1},
+ {value => "series", label => l("Series"), plural_label => l("Series"), browse => 1},
{value => "id|bibcn", label => l("Bib Call Number")}
] %]
<select name="[% name || 'qtype' %]"[% IF id; ' id="'; id ; '"' ; END -%]
NEXT IF browse_only AND NOT qt.browse -%]
<option value='[% qt.value | html %]'[%
query_type == qt.value ? ' selected="selected"' : ''
- %]>[% qt.label | html %]</option>
+ %]>[% IF plural AND qt.plural_label;
+ qt.plural_label | html;
+ ELSE;
+ qt.label | html;
+ END %]</option>
[% END -%]
</select>