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
4 changes: 3 additions & 1 deletion app/controllers/alma_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ class AlmaController < ApplicationController
def sru
return unless AlmaSru.enabled? && expected_params?

@availability = AlmaSru.lookup(params[:doc_id])
result = AlmaSru.lookup(params[:doc_id])
@availability = result[:availability]
@alma_e = result[:alma_e]
end

private
Expand Down
10 changes: 10 additions & 0 deletions app/javascript/controllers/content_loader_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ export default class extends Controller {
// Replace the entire element with the fetched HTML, or remove if empty
if (html.trim()) {
this.element.outerHTML = html

// Keep Alma availability in `.result-content` but place “Full-text options”
// with fulfillment links in the descendant `.result-get` container.
const resultContent = parentElement.closest('.result-content') || parentElement
const almaFulltextOptions = resultContent.querySelector('.alma-fulltext-options')
const resultGet = resultContent.querySelector('.result-get')
if (almaFulltextOptions && resultGet) {
resultGet.appendChild(almaFulltextOptions)
}

// Hide primo links if libkey link is present
if (parentElement.querySelector('.libkey-link')) {
const resultGet = parentElement.closest('.result-get')
Expand Down
50 changes: 40 additions & 10 deletions app/models/alma_sru.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@ class InvalidAlmaId < StandardError; end

# lookup is the primary method of interacting with this model.
#
# It will receive an Alma ID, validate it, look it up in the Alma SRU, and return a formatted result.
# It will receive an Alma ID, validate it, look it up in the Alma SRU, and return availability info and Alma-E status.
#
# It accepts an "alma_client" argument for use when testing, but this is not used in normal operations.
#
# Returns a hash with:
# - :availability => formatted availability statements array
# - :alma_e => boolean indicating if record is Alma-E
def self.lookup(raw_identifier, alma_client: nil)
return [] unless enabled?
return { availability: [], alma_e: false } unless enabled?

# Validate the raw identifier received. This will raise an InvalidAlmaId if validation fails.
raise InvalidAlmaId unless valid_alma_id?(raw_identifier)
Expand All @@ -41,24 +45,22 @@ def self.lookup(raw_identifier, alma_client: nil)
parse_response(alma_http.timeout(6).get(url), identifier)
rescue InvalidAlmaId
Rails.logger.debug("Invalid Alma ID: #{raw_identifier}")

[]
{ availability: [], alma_e: false }
rescue LookupFailure => e
Rails.logger.debug("Alma lookup failure: #{e}")

[]
{ availability: [], alma_e: false }
rescue HTTP::Error
Sentry.capture_message('Alma SRU connection failure')
Rails.logger.error('Alma SRU connection error')

[]
{ availability: [], alma_e: false }

Copy link
Copy Markdown

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. [16/10] [rubocop:Metrics/MethodLength]

end

# parse_response receives the raw response from the Alma SRU endpoint.
#
# For any non-200 response, it raises a LookupFailure.
#
# Other responses (in XML format) are parsed by Nokogiri, and we pluck content with an `AVA` tag.
# Other responses (in XML format) are parsed by Nokogiri to extract both AVA (holdings) and AVE (electronic) data.
# Returns a hash with :availability and :alma_e keys.
def self.parse_response(raw_response, reference_identifier)
raise LookupFailure, raw_response.status unless raw_response.status == 200

Expand All @@ -76,7 +78,15 @@ def self.parse_response(raw_response, reference_identifier)

# Reduce list to a single item if multiples exist
results[0] += ' and other locations' if results.length > 1
results.first(1)
availability = results.first(1)

# Check for AVE tags to determine if record is Alma-E
alma_e = alma_e?(parsed)

{
availability: availability,
alma_e: alma_e
}
Comment thread
qltysh[bot] marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 2 issues:

1. Assignment Branch Condition size for parse_response is too high. [<7, 15, 7> 17.97/17] [rubocop:Metrics/AbcSize]


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

end

# ava_to_hash takes an XML element that represents a single availability record
Expand Down Expand Up @@ -112,6 +122,26 @@ def self.fetch_controlfield(parsed_xml)
parsed_xml.xpath("//holding:controlfield[@tag='001']", NAMESPACE)&.text
end

# alma_e? receives a parsed XML document (Nokogiri::XML::Document) and returns true if the record
# contains AVE (electronic) datafields. We fall back on the 959 subfield b for legacy records.
def self.alma_e?(parsed_xml)
return true if parsed_xml.xpath("//holding:datafield[@tag='AVE']", NAMESPACE).any?

legacy_net_access?(parsed_xml)
end

# Some records omit AVE but still indicate electronic access in local 959$b=NET. After consulting
# with Metadata and Enterprise Systems, we learned that this a deprecated practice from before the
# Alma migration.
#
# It is still unclear whether Primo is determining electronic access from this subfield, but it's
# the only electronic access indicator we can find in the record other than AVE. We can revisit
# this approach if it proves ineffective.
def self.legacy_net_access?(parsed_xml)
parsed_xml.xpath("//holding:datafield[@tag='959']/holding:subfield[@code='b']", NAMESPACE)
.any? { |node| node.text.to_s.strip.casecmp('NET').zero? }
end

# format_availability receives a hash representing a single availability
# statement, and formats it for human readability. Values for "e" and "q" are
# required, while "c" and "d" are optional.
Expand Down
8 changes: 0 additions & 8 deletions app/models/normalize_primo_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,6 @@ def links
end
end

# Add Full-text options if pnx['links'] is nil and record has Alma-E (electronic availability)
full_record_link = record_link
if @record.dig('pnx', 'links').nil? &&
@record.dig('delivery', 'deliveryCategory')&.include?('Alma-E') &&
full_record_link.present?
links << { 'url' => "#{full_record_link}#nui.getit.service_viewit", 'kind' => 'Full-text options' }
end

# Return links if we found any
links.any? ? links : []
end
Expand Down
22 changes: 14 additions & 8 deletions app/views/alma/sru.html.erb
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
<% if AlmaSru.enabled? && @availability.present? %>
<div class="availability">
<% @availability.each do |statement| %>
<p><%= link_to(sanitize(statement, tags: %w[i strong], attributes: %w[class aria-hidden]),
"#{PrimoLinkBuilder.new(record_id: params[:doc_id], context: 'L').full_record_link}#getit_link1_0",
data: {content_piece: 'Availability Link' }) %></p>
<% end %>
</div>
<% if AlmaSru.enabled? %>
<% if @alma_e %>
<% fulltext_url = PrimoLinkBuilder.new(record_id: params[:doc_id], context: 'L').full_record_link + '#nui.getit.service_viewit' %>
<%= link_to 'Full-text options', fulltext_url, class: 'button alma-fulltext-options', data: { content_piece: 'Full-text Options' } %>
<% end %>
<% if @availability.present? %>
<div class="availability">
<% @availability.each do |statement| %>
<p><%= link_to(sanitize(statement, tags: %w[i strong], attributes: %w[class aria-hidden]),
"#{PrimoLinkBuilder.new(record_id: params[:doc_id], context: 'L').full_record_link}#getit_link1_0",
data: {content_piece: 'Availability Link' }) %></p>
<% end %>
</div>
<% end %>
<% end %>
6 changes: 4 additions & 2 deletions app/views/search/_result_primo.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,13 @@
<% end %>
</div>

<% if AlmaSru.enabled? && AlmaSru.valid_alma_id?(result[:identifier]) %>
<% should_render_almasru = AlmaSru.enabled? && AlmaSru.valid_alma_id?(result[:identifier]) %>

<% if should_render_almasru %>
<%= render(partial: 'trigger_almasru', locals: { doc_id: result[:identifier] }) %>
<% end %>

<% if result_get?(result) %>
<% if result_get?(result) || should_render_almasru %>
<div class="result-get">
<% if result[:links].present? %>
<% result[:links].each do |link| %>
Expand Down
14 changes: 12 additions & 2 deletions test/controllers/alma_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,27 @@ class AlmaControllerTest < ActionDispatch::IntegrationTest
assert response.body.blank?
end

test 'alma sru route returns nothing if lookup returns content with no AVA' do
test 'alma sru route returns full-text options when Alma-E is true' do
VCR.use_cassette('alma sru no availability') do
needle = 'alma9935053423706761'
get almasru_path(doc_id: needle)

assert_response :success
assert_select 'a.button', { count: 1, text: 'Full-text options' }
end
end

test 'alma sru route returns nothing when lookup has no AVA and no Alma-E' do
VCR.use_cassette('alma sru nonexistent record') do
needle = 'alma9900000000006761'
get almasru_path(doc_id: needle)

assert_response :success
assert response.body.blank?
end
end

test 'alma sru route returns HTML for successful lookup' do
test 'alma sru route returns availability for successful lookup' do
VCR.use_cassette('alma sru single record') do
needle = 'alma990014651640106761'
get almasru_path(doc_id: needle)
Expand Down
Loading