Skip to content

Commit 2a81cec

Browse files
authored
Merge pull request #257 from MITLibraries/use-95-pagination
Fix Turbo Frames pagination bug
2 parents 241ffc6 + 9da1f33 commit 2a81cec

File tree

17 files changed

+442
-61
lines changed

17 files changed

+442
-61
lines changed

app/assets/stylesheets/partials/_pagination.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
justify-content: center;
66
margin-top: 3em;
77

8+
.first {
9+
border-right: 1px solid black;
10+
margin-right: 0.5em;
11+
padding-right: 0.5em;
12+
}
13+
814
.previous {
915
border-right: 1px solid black;
1016
margin-right: 0.5em;

app/assets/stylesheets/partials/_search.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,13 @@
182182
.about {
183183
margin-top: 5rem;
184184
}
185+
186+
/* primo continuation partial */
187+
.primo-continuation {
188+
background-color: #f8f9fa;
189+
border: 1px solid #dee2e6;
190+
border-radius: 0.375rem;
191+
padding: 2rem;
192+
margin: 2rem 0;
193+
text-align: center;
194+
}

app/controllers/search_controller.rb

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ def results
1919
else
2020
params[:tab] || 'primo' # Default to primo for new tabbed interface
2121
end
22+
23+
# Include the active tab in the enhanced query so it's available for pagination and other uses.
24+
params[:tab] = @active_tab unless Feature.enabled?(:gdt)
2225
@enhanced_query = Enhancer.new(params).enhanced_query
2326

2427
# Route to appropriate search based on active tab
@@ -47,28 +50,41 @@ def load_gdt_results
4750
@errors = extract_errors(response)
4851
return unless @errors.nil?
4952

50-
@pagination = Analyzer.new(@enhanced_query, response).pagination
53+
@pagination = Analyzer.new(@enhanced_query, response, :timdex).pagination
5154
raw_results = extract_results(response)
5255
@results = NormalizeTimdexResults.new(raw_results, @enhanced_query[:q]).normalize
5356
@filters = extract_filters(response)
5457
end
5558

5659
def load_primo_results
60+
current_page = @enhanced_query[:page] || 1
61+
per_page = @enhanced_query[:per_page] || 20
62+
offset = (current_page - 1) * per_page
63+
64+
# Check if we're beyond Primo API limits before making the request.
65+
if offset >= Analyzer::PRIMO_MAX_OFFSET
66+
@show_primo_continuation = true
67+
return
68+
end
69+
5770
primo_response = query_primo
5871
@results = NormalizePrimoResults.new(primo_response, @enhanced_query[:q]).normalize
5972

60-
# Enhanced pagination using cached response
61-
if @results.present?
62-
total_hits = primo_response.dig('info', 'total') || @results.count
63-
per_page = @enhanced_query[:per_page] || 20
64-
current_page = @enhanced_query[:page] || 1
65-
66-
@pagination = {
67-
hits: total_hits,
68-
start: ((current_page - 1) * per_page) + 1,
69-
end: [current_page * per_page, total_hits].min
70-
}
73+
# Handle empty results from Primo API. Sometimes Primo will return no results at a given offset,
74+
# despite claiming in the initial query that more are available. This happens randomly and
75+
# seemingly for no reason (well below the recommended offset of 2,000). While the bug also
76+
# exists in Primo UI, sending users there seems like the best we can do.
77+
if @results.empty?
78+
docs = primo_response['docs'] if primo_response.is_a?(Hash)
79+
if docs.nil? || docs.empty?
80+
@show_primo_continuation = true
81+
else
82+
@errors = [{ 'message' => 'No more results available at this page number.' }]
83+
end
7184
end
85+
86+
# Use Analyzer for consistent pagination across all search types
87+
@pagination = Analyzer.new(@enhanced_query, primo_response, :primo).pagination
7288
rescue StandardError => e
7389
@errors = handle_primo_errors(e)
7490
end
@@ -80,7 +96,7 @@ def load_timdex_results
8096
@errors = extract_errors(response)
8197
return unless @errors.nil?
8298

83-
@pagination = Analyzer.new(@enhanced_query, response).pagination
99+
@pagination = Analyzer.new(@enhanced_query, response, :timdex).pagination
84100
raw_results = extract_results(response)
85101
@results = NormalizeTimdexResults.new(raw_results, @enhanced_query[:q]).normalize
86102
end
@@ -117,7 +133,10 @@ def query_primo
117133
Rails.cache.fetch("#{cache_key}/primo", expires_in: 12.hours) do
118134
primo_search = PrimoSearch.new
119135
per_page = @enhanced_query[:per_page] || 20
120-
primo_search.search(@enhanced_query[:q], per_page)
136+
current_page = @enhanced_query[:page] || 1
137+
offset = (current_page - 1) * per_page
138+
139+
primo_search.search(@enhanced_query[:q], per_page, offset)
121140
end
122141
end
123142

app/helpers/pagination_helper.rb

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,45 @@
11
module PaginationHelper
2+
def first_url(query_params)
3+
# Work with a copy to avoid mutating the original enhanced_query.
4+
params_copy = query_params.dup
5+
params_copy[:page] = 1
6+
7+
# Preserve the active tab in pagination URLs.
8+
params_copy[:tab] = @active_tab if @active_tab.present?
9+
10+
link_to results_path(params_copy), 'aria-label': 'First page',
11+
data: { turbo_frame: 'search-results', turbo_action: 'advance' },
12+
rel: 'nofollow' do
13+
'«« First'.html_safe
14+
end
15+
end
16+
217
def next_url(query_params)
3-
query_params[:page] = @pagination[:next]
4-
link_to results_path(query_params), 'aria-label': 'Next page' do
18+
# Work with a copy to avoid mutating the original enhanced_query.
19+
params_copy = query_params.dup
20+
params_copy[:page] = @pagination[:next]
21+
22+
# Preserve the active tab in pagination URLs.
23+
params_copy[:tab] = @active_tab if @active_tab.present?
24+
25+
link_to results_path(params_copy), 'aria-label': 'Next page',
26+
data: { turbo_frame: 'search-results', turbo_action: 'advance' },
27+
rel: 'nofollow' do
528
'Next »'.html_safe
629
end
730
end
831

932
def prev_url(query_params)
10-
query_params[:page] = @pagination[:prev]
11-
link_to results_path(query_params), 'aria-label': 'Previous page' do
33+
# Work with a copy to avoid mutating the original enhanced_query.
34+
params_copy = query_params.dup
35+
params_copy[:page] = @pagination[:prev]
36+
37+
# Preserve the active tab in pagination URLs.
38+
params_copy[:tab] = @active_tab if @active_tab.present?
39+
40+
link_to results_path(params_copy), 'aria-label': 'Previous page',
41+
data: { turbo_frame: 'search-results', turbo_action: 'advance' },
42+
rel: 'nofollow' do
1243
'« Previous'.html_safe
1344
end
1445
end

app/helpers/search_helper.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,13 @@ def extract_year(date, delimiter)
110110
date.split(delimiter).last
111111
end
112112
end
113+
114+
def primo_search_url(query_term)
115+
base_url = 'https://mit.primo.exlibrisgroup.com/discovery/search'
116+
params = {
117+
vid: ENV.fetch('PRIMO_VID'),
118+
query: "any,contains,#{query_term}"
119+
}
120+
"#{base_url}?#{params.to_query}"
121+
end
113122
end

app/models/analyzer.rb

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,44 @@ class Analyzer
33

44
RESULTS_PER_PAGE = 20
55

6-
def initialize(enhanced_query, response)
6+
# Primo API theoretical maximum recommended offset is 2000 records (per Ex Libris documentation)
7+
# but in practice, the API often can't deliver results beyond ~960 records for large result sets,
8+
# likely due to performance constraints.
9+
PRIMO_MAX_OFFSET = 960
10+
11+
def initialize(enhanced_query, response, source)
12+
@source = source
713
@pagination = {}
814
@pagination[:hits] = hits(response)
915
@pagination[:start] = ((enhanced_query[:page] - 1) * RESULTS_PER_PAGE) + 1
10-
@pagination[:end] = [enhanced_query[:page] * RESULTS_PER_PAGE, hits(response)].min
16+
@pagination[:end] = [enhanced_query[:page] * RESULTS_PER_PAGE, @pagination[:hits]].min
1117
@pagination[:prev] = enhanced_query[:page] - 1 if enhanced_query[:page] > 1
12-
@pagination[:next] = next_page(enhanced_query[:page], @pagination[:hits])
18+
19+
next_page_num = next_page(enhanced_query[:page], @pagination[:hits])
20+
@pagination[:next] = next_page_num if next_page_num
1321
end
1422

1523
private
1624

1725
def hits(response)
1826
return 0 if response.nil?
27+
28+
if @source == :primo
29+
primo_hits(response)
30+
elsif @source == :timdex
31+
timdex_hits(response)
32+
else
33+
0
34+
end
35+
end
36+
37+
def primo_hits(response)
38+
return 0 unless response.is_a?(Hash)
39+
40+
response.dig('info', 'total') || 0
41+
end
42+
43+
def timdex_hits(response)
1944
return 0 unless response.is_a?(Hash) && response.key?(:data) && response[:data].key?('search')
2045
return 0 unless response[:data]['search'].is_a?(Hash) && response[:data]['search'].key?('hits')
2146

app/models/enhancer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def initialize(params)
1313
@enhanced_query[:page] = calculate_page(params[:page].to_i)
1414
@enhanced_query[:advanced] = 'true' if params[:advanced].present?
1515
@enhanced_query[:booleanType] = params[:booleanType] || 'AND'
16+
@enhanced_query[:tab] = params[:tab] if params[:tab].present?
1617

1718
if Feature.enabled?(:geodata)
1819
@enhanced_query[:geobox] = 'true' if params[:geobox] == 'true'

app/models/primo_search.rb

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
11
# Searches Primo Search API and formats results
22
#
33
class PrimoSearch
4-
54
def initialize
65
validate_env
76
@primo_http = HTTP.persistent(primo_api_url)
87
@results = {}
98
end
109

11-
def search(term, per_page)
12-
url = search_url(term, per_page)
10+
def search(term, per_page, offset = 0)
11+
url = search_url(term, per_page, offset)
1312
result = @primo_http.timeout(http_timeout)
14-
.headers(
15-
accept: 'application/json',
16-
Authorization: "apikey #{primo_api_key}"
17-
)
18-
.get(url)
19-
13+
.headers(
14+
accept: 'application/json',
15+
Authorization: "apikey #{primo_api_key}"
16+
)
17+
.get(url)
18+
2019
raise "Primo Error Detected: #{result.status}" unless result.status == 200
2120

2221
JSON.parse(result)
@@ -26,15 +25,15 @@ def search(term, per_page)
2625

2726
def validate_env
2827
missing_vars = []
29-
28+
3029
missing_vars << 'PRIMO_API_URL' if primo_api_url.nil?
3130
missing_vars << 'PRIMO_API_KEY' if primo_api_key.nil?
3231
missing_vars << 'PRIMO_SCOPE' if primo_scope.nil?
3332
missing_vars << 'PRIMO_TAB' if primo_tab.nil?
3433
missing_vars << 'PRIMO_VID' if primo_vid.nil?
35-
34+
3635
return if missing_vars.empty?
37-
36+
3837
raise ArgumentError, "Required Primo environment variables are not set: #{missing_vars.join(', ')}"
3938
end
4039

@@ -65,8 +64,8 @@ def clean_term(term)
6564
end
6665

6766
# Constructs the search URL with required parameters for Primo API
68-
def search_url(term, per_page)
69-
[
67+
def search_url(term, per_page, offset = 0)
68+
url_parts = [
7069
primo_api_url,
7170
'/search?q=any,contains,',
7271
clean_term(term),
@@ -77,10 +76,18 @@ def search_url(term, per_page)
7776
'&scope=',
7877
primo_scope,
7978
'&limit=',
80-
per_page,
79+
per_page
80+
]
81+
82+
# Add offset parameter for pagination (only if > 0)
83+
url_parts += ['&offset=', offset] if offset > 0
84+
85+
url_parts += [
8186
'&apikey=',
8287
primo_api_key
83-
].join
88+
]
89+
90+
url_parts.join
8491
end
8592

8693
# Timeout configuration for HTTP requests

app/views/search/_pagination.html.erb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
<% return if @pagination.nil? %>
22

33
<nav class="pagination-container" aria-label="Pagination">
4+
<div class="first">
5+
<% current_page = @enhanced_query[:page] || 1 %>
6+
<% if current_page > 2 %>
7+
<%= first_url(@enhanced_query) %>
8+
<% else %>
9+
First
10+
<% end %>
11+
</div>
412
<div class="previous">
513
<% if @pagination[:prev] %>
614
<%= prev_url(@enhanced_query) %>
715
<% else %>
8-
First
16+
Previous
917
<% end %>
1018
</div>
11-
<div class="current"><%= @pagination[:start] %> - <%= @pagination[:end] %> of <%= @pagination[:hits] %></div>
19+
<div class="current"><%= @pagination[:start] %> - <%= @pagination[:end] %> of <%= number_with_delimiter(@pagination[:hits]) %></div>
1220
<div class="next">
1321
<% if @pagination[:next] %>
1422
<%= next_url(@enhanced_query) %>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<div class="primo-continuation">
2+
<h2 class="hd-3">Continue your search in Search Our Collections</h2>
3+
<p>
4+
You've reached the pagination limit for embedded search results. To see more results and access
5+
additional search features, continue your search in the full Search Our Collections interface.
6+
</p>
7+
<div>
8+
<%= link_to "Continue in Search Our Collections",
9+
primo_search_url(@enhanced_query[:q]),
10+
class: "button-primary",
11+
rel: "nofollow" %>
12+
</div>
13+
</div>

0 commit comments

Comments
 (0)