From b2d97627be924b57c53d39f777a40e432751b9da Mon Sep 17 00:00:00 2001
From: jazairi <16103405+jazairi@users.noreply.github.com>
Date: Fri, 3 Oct 2025 09:44:14 -0400
Subject: [PATCH] Initial tabbed Primo/TIMDEX interface
Why these changes are being introduced:
USE UI needs affordances to switch between results
from different sources. Primo results should be
the default view.
Relevant ticket(s):
* [USE-31](https://mitlibraries.atlassian.net/browse/USE-31)
* [TIMX-549](https://mitlibraries.atlassian.net/browse/TIMX-549)
How this addresses that need:
* Integrates Primo search into the controller and
view layers
* Adds Turbo frame 'tabs' to switch between Primo
and TIMDEX results.
Side effects of this change:
* Maintaining separate views for each result type
is probably not ideal, but I'm accepting it as a
risk until we normalize TIMDEX records similarly
to how we normalize Primo.
* Several tests have been skipped for features
that are no longer relevant to USE UI, but are
core to GDT. We will need to revise our overall
test strategy such that these features are tested
as part of GDT.
* FRBRized full record links don't always work. It
the method we use breaks FRBR links for CDI
records. Predictably, Ex Libris documentation on
FRBR does not address this issue. We might decide
to forgo FRBR links in general, or perhaps limit
them by content type (assuming book results always
come from Alma).
* Pagination is not yet implemented.
---
app/controllers/search_controller.rb | 127 +++--
app/views/search/_form.html.erb | 251 ++++-----
app/views/search/_result_primo.html.erb | 77 +++
app/views/search/_search_summary_use.html.erb | 7 +
app/views/search/results.html.erb | 89 +--
test/controllers/search_controller_test.rb | 525 +++++++++++-------
test/integration/error_resilience_test.rb | 2 +-
7 files changed, 675 insertions(+), 403 deletions(-)
create mode 100644 app/views/search/_result_primo.html.erb
create mode 100644 app/views/search/_search_summary_use.html.erb
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index f1efd795..02772968 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -15,59 +15,102 @@ def results
# inject session preference for boolean type if it is present
params[:booleanType] = cookies[:boolean_type] || 'AND'
- # hand off to Enhancer chain
+ # Determine which tab to load - default to primo unless gdt is enabled
+ @active_tab = if Flipflop.enabled?(:gdt)
+ 'gdt' # Keep existing GDT behavior unchanged
+ else
+ params[:tab] || 'primo' # Default to primo for new tabbed interface
+ end
@enhanced_query = Enhancer.new(params).enhanced_query
- # hand off enhanced query to builder
- query = QueryBuilder.new(@enhanced_query).query
+ # Route to appropriate search based on active tab
+ if Flipflop.enabled?(:gdt)
+ # Keep existing GDT behavior unchanged
+ load_gdt_results
+ else
+ case @active_tab
+ when 'primo'
+ load_primo_results
+ when 'timdex'
+ load_timdex_results
+ end
+ end
+ end
- # Create cache key for this query
- # Sorting query hash to ensure consistent key generation regardless of the parameter order
- sorted_query = query.sort_by { |k, v| k.to_sym }.to_h
- cache_key = Digest::MD5.hexdigest(sorted_query.to_s)
+ private
- # builder hands off to wrapper which returns raw results here
- # We are using two difference caches to allow for Geo and USE to be cached separately. This ensures we don't have
- # cache key collission for these two different query types. In practice, the likelihood of this happening is low,
- # as the query parameters are different for each type and they won't often be run with the same cache backend other
- # than locally, but this is a safeguard.
- # The response type is a GraphQL::Client::Response, which is not directly serializable, so we convert it to a hash.
- response = if Flipflop.enabled?(:gdt)
- Rails.cache.fetch("#{cache_key}/geo", expires_in: 12.hours) do
- raw = execute_geospatial_query(query)
- {
- data: raw.data.to_h,
- errors: raw.errors.details.to_h
- }
- end
- else
- Rails.cache.fetch("#{cache_key}/use", expires_in: 12.hours) do
- raw = TimdexBase::Client.query(TimdexSearch::BaseQuery, variables: query)
- {
- data: raw.data.to_h,
- errors: raw.errors.details.to_h
- }
- end
- end
+ def load_gdt_results
+ query = QueryBuilder.new(@enhanced_query).query
+
+ response = cache_timdex_query(query)
# Handle errors
@errors = extract_errors(response)
-
- # Analayze results
- # The @pagination instance variable includes info about next/previous pages (where they exist) to assist the UI.
@pagination = Analyzer.new(@enhanced_query, response).pagination if @errors.nil?
-
- # Display results
@results = extract_results(response)
@filters = extract_filters(response)
end
- private
+ def load_primo_results
+ begin
+ primo_search = PrimoSearch.new
+ per_page = params[:per_page] || 20
+ primo_response = primo_search.search(params[:q], per_page)
+
+ @results = NormalizePrimoResults.new(primo_response, params[:q]).normalize
+
+ # Basic pagination for now.
+ if @results.present?
+ @pagination = {
+ hits: @results.count,
+ start: 1,
+ end: @results.count
+ }
+ end
+
+ rescue StandardError => e
+ @errors = handle_primo_errors(e)
+ end
+ end
+
+ def load_timdex_results
+ query = QueryBuilder.new(@enhanced_query).query
+ response = cache_timdex_query(query)
+
+ @errors = extract_errors(response)
+ @pagination = Analyzer.new(@enhanced_query, response).pagination if @errors.nil?
+ @results = extract_results(response)
+ end
def active_filters
ENV.fetch('ACTIVE_FILTERS', '').split(',').map(&:strip)
end
+ def cache_timdex_query(query)
+ # Create cache key for this query
+ # Sorting query hash to ensure consistent key generation regardless of the parameter order
+ sorted_query = query.sort_by { |k, v| k.to_sym }.to_h
+ cache_key = Digest::MD5.hexdigest(sorted_query.to_s)
+
+ # builder hands off to wrapper which returns raw results here
+ # We are using two difference caches to allow for Geo and USE to be cached separately. This ensures we don't have
+ # cache key collision for these two different query types. In practice, the likelihood of this happening is low,
+ # as the query parameters are different for each type and they won't often be run with the same cache backend other
+ # than locally, but this is a safeguard.
+ # The response type is a GraphQL::Client::Response, which is not directly serializable, so we convert it to a hash.
+ Rails.cache.fetch("#{cache_key}/#{@active_tab}", expires_in: 12.hours) do
+ raw = if @active_tab == 'gdt'
+ execute_geospatial_query(query)
+ elsif @active_tab == 'timdex'
+ TimdexBase::Client.query(TimdexSearch::BaseQuery, variables: query)
+ end
+ {
+ data: raw.data.to_h,
+ errors: raw.errors.details.to_h
+ }
+ end
+ end
+
def execute_geospatial_query(query)
if query['geobox'] == 'true' && query[:geodistance] == 'true'
TimdexBase::Client.query(TimdexSearch::AllQuery, variables: query)
@@ -214,4 +257,16 @@ def validate_geobox_values!
flash[:error] = 'Maximum latitude cannot exceed minimum latitude.'
redirect_to root_url
end
+
+ def handle_primo_errors(error)
+ Rails.logger.error("Primo search error: #{error.message}")
+
+ if error.is_a?(ArgumentError)
+ [{ 'message' => 'Primo search is not properly configured.' }]
+ elsif error.is_a?(HTTP::TimeoutError)
+ [{ 'message' => 'The Primo service is currently slow to respond. Please try again.' }]
+ else
+ [{ 'message' => error.message }]
+ end
+ end
end
diff --git a/app/views/search/_form.html.erb b/app/views/search/_form.html.erb
index b0287460..81f02734 100644
--- a/app/views/search/_form.html.erb
+++ b/app/views/search/_form.html.erb
@@ -31,118 +31,117 @@ keyword_placeholder = search_required ? "Enter your search" : "Keyword anywhere"
<%= 'aria-describedby=site-desc' if Flipflop.enabled?(:gdt) %>>
<% if Flipflop.enabled?(:gdt) %>
- >
-
- <%= geobox_label %>
-
-
-
+ <% end %>
diff --git a/app/views/search/_result_primo.html.erb b/app/views/search/_result_primo.html.erb
new file mode 100644
index 00000000..318eb664
--- /dev/null
+++ b/app/views/search/_result_primo.html.erb
@@ -0,0 +1,77 @@
+