Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
ALMA_OPENURL=https://na06.alma.exlibrisgroup.com/view/uresolver/01MIT_INST/openurl?
FEATURE_TIMDEX_FULLTEXT=true
FEATURE_GEODATA=false
FEATURE_PRIMO_NDE_LINKS=false
MIT_PRIMO_URL=https://mit.primo.exlibrisgroup.com
OPENALEX_EMAIL=FAKE_OPENALEX_EMAIL
PRIMO_API_KEY=FAKE_PRIMO_API_KEY
PRIMO_API_URL=https://api-na.hosted.exlibrisgroup.com/primo/v1
PRIMO_SCOPE=cdi
PRIMO_TAB=all
PRIMO_VID=01MIT_INST:MIT
PRIMO_NDE_VID=01MIT_INST:NDE
SYNDETICS_PRIMO_URL=https://syndetics.com/index.php?client=primo
TACOS_HOST=FAKE_TACOS_HOST
TACOS_URL=http://FAKE_TACOS_HOST/graphql
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ may have unexpected consequences if applied to other TIMDEX UI apps.
- `FEATURE_TAB_TIMDEX_ALL`: Display a tab for displaying the combined TIMDEX data. `TIMDEX_INDEX` affects which data appears in this tab.
- `FEATURE_TAB_TIMDEX_ALMA`: Display a tab for displaying Alma data from TIMDEX. `TIMDEX_INDEX` must include `Alma` data or no results will return.
- `FEATURE_TIMDEX_FULLTEXT`: Activate fulltext searching for sources in TIMDEX that support it
- `FEATURE_PRIMO_NDE_LINKS`: Enables all Primo UI links to target the NDE version of Primo. When enabled, links will use `/nde/search` and `/nde/fulldisplay` paths along with the NDE view ID from `PRIMO_NDE_VID`.
- `FILTER_ACCESS_TO_FILES`: The name to use instead of "Access to files" for that filter / aggregation.
- `FILTER_CONTENT_TYPE`: The name to use instead of "Content type" for that filter / aggregation.
- `FILTER_CONTRIBUTOR`: The name to use instead of "Contributor" for that filter / aggregation.
Expand All @@ -125,6 +126,7 @@ may have unexpected consequences if applied to other TIMDEX UI apps.
- `OPENALEX_EMAIL`: required to enable OpenAlex OpenAccess lookups. In dev use your personal email. In production we'll use a Moira.
- `ORIGINS`: sets origins for CORS (currently used only for TACOS API calls).
- `PLATFORM_NAME`: The value set is added to the header after the MIT Libraries logo. The logic and CSS for this comes from our theme gem.
- `PRIMO_NDE_VID`: The Primo view ID for NDE Only required if `FEATURE_PRIMO_NDE_LINKS` is enabled. Ask Enterprise Systems for value.
- `PRIMO_TIMEOUT`: The number of seconds before a Primo request times out (default 6).
- `REQUESTS_PER_PERIOD` - number of requests that can be made for general throttles per `REQUEST_PERIOD`
- `REQUEST_PERIOD` - time in minutes used along with `REQUESTS_PER_PERIOD`
Expand Down
13 changes: 2 additions & 11 deletions app/helpers/results_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,9 @@ def tab_description
# Examples from UI we are targeting:
# - https://mit.primo.exlibrisgroup.com/discovery/search?query=any,contains,breakfast%20of%20champions&tab=all&search_scope=bento_catalog&vid=01MIT_INST:MIT
# - https://mit.primo.exlibrisgroup.com/discovery/search?query=any,contains,breakfast%20of%20champions&tab=all&search_scope=cdi&vid=01MIT_INST:MIT
# - https://mit.primo.exlibrisgroup.com/nde/search?query=breakfast%20of%20champions&tab=all&search_scope=all&vid=01MIT_INST:NDE
def search_primo_link
base_url = ENV.fetch('MIT_PRIMO_URL') + '/discovery/search?'
base_url + search_primo_params
end

def search_primo_params
URI.encode_www_form({
query: "any,contains,#{params[:q]}",
tab: 'all',
search_scope: 'all',
vid: ENV.fetch('PRIMO_VID')
})
PrimoLinkBuilder.new(query_term: params[:q]).search_link
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice use of the builder pattern. It does a great job consolidating this logic and making it more testable.

end

# Creates MIT ArchivesSpace links based on current search term
Expand Down
7 changes: 1 addition & 6 deletions app/helpers/search_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,6 @@ def extract_year(date, delimiter)
end

def primo_search_url(query_term)
base_url = 'https://mit.primo.exlibrisgroup.com/discovery/search'
params = {
vid: ENV.fetch('PRIMO_VID'),
query: "any,contains,#{query_term}"
}
"#{base_url}?#{params.to_query}"
PrimoLinkBuilder.new(query_term: query_term).search_link
end
end
2 changes: 1 addition & 1 deletion app/models/feature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
class Feature
# List of all valid features in the application
VALID_FEATURES = %i[geodata boolean_picker oa_always simulate_search_latency tab_primo_all tab_timdex_all
tab_timdex_alma record_link timdex_fulltext].freeze
tab_timdex_alma record_link timdex_fulltext primo_nde_links].freeze

# Check if a feature is enabled by name
#
Expand Down
14 changes: 4 additions & 10 deletions app/models/normalize_primo_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -237,16 +237,10 @@ def record_link
return unless @record['context']

record_id = @record['pnx']['control']['recordid'].join
base = [ENV.fetch('MIT_PRIMO_URL'), '/discovery/fulldisplay?'].join
query = {
docid: record_id,
vid: ENV.fetch('PRIMO_VID'),
context: @record['context'],
search_scope: 'all',
lang: 'en',
tab: ENV.fetch('PRIMO_TAB')
}.to_query
[base, query].join
PrimoLinkBuilder.new(
record_id: record_id,
context: @record['context']
).full_record_link
end

def numbering
Expand Down
89 changes: 89 additions & 0 deletions app/models/primo_link_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Builds Primo links with support for discovery and NDE UIs.
#
# @example Building a search link
# builder = PrimoLinkBuilder.new(query_term: "machine learning")
# builder.search_link
# # => "https://mit.primo.exlibrisgroup.com/discovery/search?query=any%2Ccontains%2Cmachine+learning&..."
#
# @example Building a full record link
# builder = PrimoLinkBuilder.new(record_id: "alma123", context: "L")
# builder.full_record_link
# # => "https://mit.primo.exlibrisgroup.com/discovery/fulldisplay?docid=alma123&..."
class PrimoLinkBuilder
# @param query_term [String, nil] The search query term (used for search_link)
# @param record_id [String, nil] The Primo record ID (used for full_record_link)
# @param context [String, nil] The Primo context code indicating record type:
# - 'L' for local catalog (Alma) records
# - 'PC' for CDI records
# - 'ALL' for all scopes
# See https://developers.exlibrisgroup.com/primo/apis/deep-links-new-ui/ and
# https://knowledge.exlibrisgroup.com/Primo/Product_Documentation/Primo/Back_Office_Guide/070Monitoring_and_Maintaining_Primo/Displaying_PNX_Records_from_Primo_Front_End
def initialize(query_term: nil, record_id: nil, context: nil)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be helpful to explain these input variables. While query_term and record_id are easily understandable, I'm genuinely not sure what context is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context is used for the display endpoint only and indicates whether it's Alma/local ("L") or CDI (uhh...something else). Will document.

@query_term = query_term
@record_id = record_id
@context = context
end

# Build a Primo search results link
# @param tab [String] Determines which results tab to display. (default: PRIMO_TAB env var, or 'all').
# For detailed documentation, see primo_search model.
# @param search_scope [String] Determines which Primo scope to search. (default: PRIMO_SCOPE env var, or 'all').
# For detailed documentation, see primo_search model.
# @return [String] The complete search URL
def search_link(tab: ENV.fetch('PRIMO_TAB', 'all'), search_scope: ENV.fetch('PRIMO_SCOPE', 'all'))
return nil unless @query_term

base_url = "#{ENV.fetch('MIT_PRIMO_URL')}#{search_path}?"
params = {
query: search_query,
tab: tab,
search_scope: search_scope,
vid: vid
}
base_url + URI.encode_www_form(params)
end

# Build a Primo full record link
# @param tab [String] Determines which results tab to display. (default: PRIMO_TAB env var, or 'all').
# For detailed documentation, see primo_search model.
# @param search_scope [String] Determines which Primo scope to search. (default: PRIMO_SCOPE env var, or 'all').
# For detailed documentation, see primo_search model.
# @param lang [String] The language (default: 'en')
# @return [String] The complete full record URL, or nil if record_id or context is missing
def full_record_link(tab: ENV.fetch('PRIMO_TAB', 'all'), search_scope: ENV.fetch('PRIMO_SCOPE', 'all'), lang: 'en')
return nil unless @record_id && @context

base_url = "#{ENV.fetch('MIT_PRIMO_URL')}#{full_record_path}?"
params = {
docid: @record_id,
vid: vid,
context: @context,
search_scope: search_scope,
lang: lang,
tab: tab
}
base_url + URI.encode_www_form(params)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method has too many lines. [11/10] [rubocop:Metrics/MethodLength]

end

private

def search_path
Feature.enabled?(:primo_nde_links) ? '/nde/search' : '/discovery/search'
end

def full_record_path
Feature.enabled?(:primo_nde_links) ? '/nde/fulldisplay' : '/discovery/fulldisplay'
end

def vid
Feature.enabled?(:primo_nde_links) ? ENV.fetch('PRIMO_NDE_VID') : ENV.fetch('PRIMO_VID')
end

def search_query
if Feature.enabled?(:primo_nde_links)
@query_term
else
"any,contains,#{@query_term}"
end
end
end
13 changes: 8 additions & 5 deletions test/helpers/results_helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,16 @@ class ResultsHelperTest < ActionView::TestCase
assert_equal description, tab_description
end

test 'search_primo_params includes encoded search query' do
test 'search_primo_link includes encoded search query and correct path' do
params[:q] = 'breakfast of champions'
result = search_primo_params
link = search_primo_link

assert_includes result, 'query=any%2Ccontains%2Cbreakfast+of+champions'
assert_includes result, 'tab=all'
assert_includes result, 'search_scope=all'
assert link.start_with?('https://')
assert_includes link, '/discovery/search?'
assert_includes link, 'query=any%2Ccontains%2Cbreakfast+of+champions'
assert_includes link, 'tab=all'
assert_includes link, 'search_scope=cdi'
assert_includes link, 'vid=01MIT_INST%3AMIT'
end

test 'search_primo_link returns a valid URL string' do
Expand Down
4 changes: 2 additions & 2 deletions test/helpers/search_helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,13 @@ class SearchHelperTest < ActionView::TestCase

test 'primo_search_url generates correct Primo URL' do
query = 'machine learning'
expected_url = 'https://mit.primo.exlibrisgroup.com/discovery/search?query=any%2Ccontains%2Cmachine+learning&vid=01MIT_INST%3AMIT'
expected_url = 'https://mit.primo.exlibrisgroup.com/discovery/search?query=any%2Ccontains%2Cmachine+learning&tab=all&search_scope=cdi&vid=01MIT_INST%3AMIT'
assert_equal expected_url, primo_search_url(query)
end

test 'primo_search_url handles special characters in query' do
query = 'data & analytics'
expected_url = 'https://mit.primo.exlibrisgroup.com/discovery/search?query=any%2Ccontains%2Cdata+%26+analytics&vid=01MIT_INST%3AMIT'
expected_url = 'https://mit.primo.exlibrisgroup.com/discovery/search?query=any%2Ccontains%2Cdata+%26+analytics&tab=all&search_scope=cdi&vid=01MIT_INST%3AMIT'
assert_equal expected_url, primo_search_url(query)
end

Expand Down
105 changes: 105 additions & 0 deletions test/models/primo_link_builder_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Test NDE link generation
require 'test_helper'

class PrimoLinkBuilderTest < ActiveSupport::TestCase
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect the intent of testing the way we are here is to avoid fragile tests, but it makes me nervous not to have any full link testing. I think most of these are fine as is, but maybe one more test for each type of link "search_link|full_record_link generates a full link as expected" or something?

test 'search_link generates Primo discovery URL by default' do
link = PrimoLinkBuilder.new(query_term: 'machine learning').search_link

assert_includes link, '/discovery/search?'
assert_includes link, 'query=any%2Ccontains%2Cmachine+learning'
assert_includes link, 'vid=01MIT_INST%3AMIT'
end

test 'search_link generates Primo NDE URL when feature flag us enabled' do
ClimateControl.modify(FEATURE_PRIMO_NDE_LINKS: 'true') do
link = PrimoLinkBuilder.new(query_term: 'machine learning').search_link

assert_includes link, '/nde/search?'
assert_includes link, 'query=machine+learning'
assert_includes link, 'vid=01MIT_INST%3ANDE'
assert_not_includes link, 'any%2Ccontains'
end
end

test 'full_record_link generates discovery URL by default' do
link = PrimoLinkBuilder.new(record_id: 'alma990003098710106761', context: 'L').full_record_link

assert_includes link, '/discovery/fulldisplay?'
assert_includes link, 'docid=alma990003098710106761'
assert_includes link, 'vid=01MIT_INST%3AMIT'
assert_includes link, 'context=L'
end

test 'full_record_link generates NDE URL when feature flag enabled' do
ClimateControl.modify(FEATURE_PRIMO_NDE_LINKS: 'true') do
link = PrimoLinkBuilder.new(record_id: 'alma990003098710106761', context: 'L').full_record_link

assert_includes link, '/nde/fulldisplay?'
assert_includes link, 'docid=alma990003098710106761'
assert_includes link, 'vid=01MIT_INST%3ANDE'
assert_includes link, 'context=L'
end
end

test 'search_link returns nil when query_term is nil' do
link = PrimoLinkBuilder.new(query_term: nil).search_link

assert_nil link
end

test 'full_record_link returns nil when record_id is missing' do
link = PrimoLinkBuilder.new(context: 'foo').full_record_link

assert_nil link
end

test 'full_record_link returns nil when context is missing' do
link = PrimoLinkBuilder.new(record_id: 'alma123').full_record_link

assert_nil link
end

test 'search_link generates complete URL for discovery' do
builder = PrimoLinkBuilder.new(query_term: 'database security')
link = builder.search_link

expected = 'https://mit.primo.exlibrisgroup.com/discovery/search?query=any%2Ccontains%2Cdatabase+security&tab=all&search_scope=cdi&vid=01MIT_INST%3AMIT'
assert_equal expected, link
end

test 'search_link generates complete URL for NDE' do
ClimateControl.modify(FEATURE_PRIMO_NDE_LINKS: 'true') do
builder = PrimoLinkBuilder.new(query_term: 'machine learning')
link = builder.search_link

expected = 'https://mit.primo.exlibrisgroup.com/nde/search?query=machine+learning&tab=all&search_scope=cdi&vid=01MIT_INST%3ANDE'
assert_equal expected, link
end
end

test 'full_record_link generates complete URL for discovery' do
builder = PrimoLinkBuilder.new(record_id: 'alma990003098710106761', context: 'L')
link = builder.full_record_link

assert link.start_with?('https://mit.primo.exlibrisgroup.com/discovery/fulldisplay?')
assert_includes link, 'docid=alma990003098710106761'
assert_includes link, 'context=L'
assert_includes link, 'vid=01MIT_INST%3AMIT'
assert_includes link, 'search_scope=cdi'
assert_includes link, 'lang=en'
end

test 'full_record_link generates complete URL for NDE' do
ClimateControl.modify(FEATURE_PRIMO_NDE_LINKS: 'true') do
builder = PrimoLinkBuilder.new(record_id: 'alma990003098710106761', context: 'P')
link = builder.full_record_link

assert link.start_with?('https://mit.primo.exlibrisgroup.com/nde/fulldisplay?')
assert_includes link, 'docid=alma990003098710106761'
assert_includes link, 'context=P'
assert_includes link, 'vid=01MIT_INST%3ANDE'
assert_includes link, 'search_scope=cdi'
assert_includes link, 'lang=en'
end
end
end