From d9c26ca6732251ff252586b257d51cd052be0203 Mon Sep 17 00:00:00 2001 From: Jeremy Prevost Date: Tue, 2 Dec 2025 11:46:14 -0500 Subject: [PATCH 1/5] Adds availablility information to records Why are these changes being introduced: * This information is useful to users to understand if an item is available for checkout or needs to be requested. Relevant ticket(s): * https://mitlibraries.atlassian.net/browse/USE-246 How does this address that need: * Creates a helper method to format availability information * Updates the result partial to display availability information Document any side effects to this change: * Location information is now expected to be an array with three elements instead of a string to allow for more flexibility in formatting. --- app/helpers/results_helper.rb | 45 ++++++++++++++++++++++ app/models/normalize_primo_record.rb | 2 +- app/views/search/_result_primo.html.erb | 3 ++ test/helpers/results_helper_test.rb | 20 ++++++++++ test/models/normalize_primo_record_test.rb | 2 +- 5 files changed, 70 insertions(+), 2 deletions(-) diff --git a/app/helpers/results_helper.rb b/app/helpers/results_helper.rb index 85b4860d..59316797 100644 --- a/app/helpers/results_helper.rb +++ b/app/helpers/results_helper.rb @@ -2,4 +2,49 @@ module ResultsHelper def results_summary(hits) hits.to_i >= 10_000 ? '10,000+ results' : "#{number_with_delimiter(hits)} results" end + + # Formats availability information for display. + # Expects: + # - status: a string indicating availability status + # - location: an array with three elements: [library name, location name, call number] + # - other_availability: a boolean string indicating if there is availability at other locations + def availability(status, location, other_availability) + blurb = case status.downcase + # `available` is a common status used in Alma/Primo VE for items that are not checked out and should be + # on the shelf + when 'available' + "#{icon('check')} Available in #{location(location)}" + # `check_holdings`: unclear when (or if) this is used. Bento handled this so we did too assuming it was + # meaningful + when 'check_holdings' + "#{icon('question')} May be available in #{location(location)}" + # 'unavailable' is used for items that are checked out, missing, or otherwise not on the shelf + when 'unavailable' + "#{icon('times')} Not currently available in #{location(location)}" + # Unclear if there are other statuses we should handle here. For now we log and display a generic message. + else + Rails.logger.error("Unhandled availability status: #{status.inspect}") + "#{icon('question')} Uncertain availability in #{location(location)} #{status}" + end + + blurb += ' and other locations.' if other_availability.present? + + # We are generating HTML in this helper, so we need to mark it as safe or it will be escaped in the view. + blurb.html_safe + end + + # Fontawesome helper. Currently only takes the icon name and assumes solid sharp style. + # Could be extended later to default to these styles but allow overrides if appropriate. + # Also defaults to aria-hidden true, which is probably what we want for icons used + # purely for decoration. If an icon is used in a more meaningful way, we may want to extend this helper + # to allow passing in additional aria attributes. + def icon(fa) + "" + end + + # Formats location information for availability display. + # Expects an array with three elements: [library name, location name, call number] + def location(loc) + "#{loc[0]} #{loc[1]} (#{loc[2]})" + end end diff --git a/app/models/normalize_primo_record.rb b/app/models/normalize_primo_record.rb index 9ea61873..3389b278 100644 --- a/app/models/normalize_primo_record.rb +++ b/app/models/normalize_primo_record.rb @@ -280,7 +280,7 @@ def location return unless @record['delivery']['bestlocation'] loc = @record['delivery']['bestlocation'] - ["#{loc['mainLocation']} #{loc['subLocation']}", loc['callNumber']] + [loc['mainLocation'], loc['subLocation'], loc['callNumber']] end def subjects diff --git a/app/views/search/_result_primo.html.erb b/app/views/search/_result_primo.html.erb index cf696c47..2317eedf 100644 --- a/app/views/search/_result_primo.html.erb +++ b/app/views/search/_result_primo.html.erb @@ -38,6 +38,9 @@ <% if result[:links].present? %> <% result[:links].each do |link| %> <%= link_to link['kind'].titleize, link['url'], class: 'button' %> + <% if result[:availability].present? %> + <%= availability(result[:availability], result[:location], result[:other_availability]) %> + <% end %> <% end %> <% end %> diff --git a/test/helpers/results_helper_test.rb b/test/helpers/results_helper_test.rb index d3df676b..c30dfaaa 100644 --- a/test/helpers/results_helper_test.rb +++ b/test/helpers/results_helper_test.rb @@ -17,4 +17,24 @@ class ResultsHelperTest < ActionView::TestCase hits = 9000 assert_equal '9,000 results', results_summary(hits) end + + test 'availability helper handles known statuses correctly' do + location = ['Main Library', 'First Floor', 'QA76.73.R83 2023'] + + available_blurb = availability('available', location, false) + assert_includes available_blurb, 'Available in' + assert_includes available_blurb, location(location) + + check_holdings_blurb = availability('check_holdings', location, false) + assert_includes check_holdings_blurb, 'May be available in' + assert_includes check_holdings_blurb, location(location) + + unavailable_blurb = availability('unavailable', location, false) + assert_includes unavailable_blurb, 'Not currently available in' + assert_includes unavailable_blurb, location(location) + + unknown_blurb = availability('unknown_status', location, false) + assert_includes unknown_blurb, 'Uncertain availability in' + assert_includes unknown_blurb, location(location) + end end diff --git a/test/models/normalize_primo_record_test.rb b/test/models/normalize_primo_record_test.rb index bc6ebd55..e6e10524 100644 --- a/test/models/normalize_primo_record_test.rb +++ b/test/models/normalize_primo_record_test.rb @@ -174,7 +174,7 @@ def cdi_record test 'returns best location with call number' do normalized = NormalizePrimoRecord.new(full_record, 'test').normalize - expected_location = ['Hayden Library Stacks', 'QA76.73.R83 2023'] + expected_location = ['Hayden Library', 'Stacks', 'QA76.73.R83 2023'] assert_equal expected_location, normalized[:location] end From abc8c3f78ac25ecf8e487d43dee56f78651d6eb3 Mon Sep 17 00:00:00 2001 From: Jeremy Prevost Date: Tue, 2 Dec 2025 15:15:44 -0500 Subject: [PATCH 2/5] Fixup missing ' --- app/helpers/results_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/results_helper.rb b/app/helpers/results_helper.rb index 59316797..900ba7b0 100644 --- a/app/helpers/results_helper.rb +++ b/app/helpers/results_helper.rb @@ -39,7 +39,7 @@ def availability(status, location, other_availability) # purely for decoration. If an icon is used in a more meaningful way, we may want to extend this helper # to allow passing in additional aria attributes. def icon(fa) - "" + "" end # Formats location information for availability display. From 73d5caaad408d6a48b4be2499715d405e5dd4952 Mon Sep 17 00:00:00 2001 From: Jeremy Prevost Date: Tue, 2 Dec 2025 15:19:15 -0500 Subject: [PATCH 3/5] Move availability helper from results to record --- app/helpers/record_helper.rb | 45 +++++++++++++++++++++++++++++ app/helpers/results_helper.rb | 45 ----------------------------- test/helpers/record_helper_test.rb | 20 +++++++++++++ test/helpers/results_helper_test.rb | 20 ------------- 4 files changed, 65 insertions(+), 65 deletions(-) diff --git a/app/helpers/record_helper.rb b/app/helpers/record_helper.rb index 020777e3..0d7d7047 100644 --- a/app/helpers/record_helper.rb +++ b/app/helpers/record_helper.rb @@ -139,6 +139,51 @@ def deduplicate_subjects(subjects) subjects.map { |subject| subject['value'].uniq(&:downcase) }.uniq { |values| values.map(&:downcase) } end + # Formats availability information for display. + # Expects: + # - status: a string indicating availability status + # - location: an array with three elements: [library name, location name, call number] + # - other_availability: a boolean string indicating if there is availability at other locations + def availability(status, location, other_availability) + blurb = case status.downcase + # `available` is a common status used in Alma/Primo VE for items that are not checked out and should be + # on the shelf + when 'available' + "#{icon('check')} Available in #{location(location)}" + # `check_holdings`: unclear when (or if) this is used. Bento handled this so we did too assuming it was + # meaningful + when 'check_holdings' + "#{icon('question')} May be available in #{location(location)}" + # 'unavailable' is used for items that are checked out, missing, or otherwise not on the shelf + when 'unavailable' + "#{icon('times')} Not currently available in #{location(location)}" + # Unclear if there are other statuses we should handle here. For now we log and display a generic message. + else + Rails.logger.error("Unhandled availability status: #{status.inspect}") + "#{icon('question')} Uncertain availability in #{location(location)} #{status}" + end + + blurb += ' and other locations.' if other_availability.present? + + # We are generating HTML in this helper, so we need to mark it as safe or it will be escaped in the view. + blurb.html_safe + end + + # Fontawesome helper. Currently only takes the icon name and assumes solid sharp style. + # Could be extended later to default to these styles but allow overrides if appropriate. + # Also defaults to aria-hidden true, which is probably what we want for icons used + # purely for decoration. If an icon is used in a more meaningful way, we may want to extend this helper + # to allow passing in additional aria attributes. + def icon(fa) + "" + end + + # Formats location information for availability display. + # Expects an array with three elements: [library name, location name, call number] + def location(loc) + "#{loc[0]} #{loc[1]} (#{loc[2]})" + end + private def render_kind_value(list) diff --git a/app/helpers/results_helper.rb b/app/helpers/results_helper.rb index 900ba7b0..85b4860d 100644 --- a/app/helpers/results_helper.rb +++ b/app/helpers/results_helper.rb @@ -2,49 +2,4 @@ module ResultsHelper def results_summary(hits) hits.to_i >= 10_000 ? '10,000+ results' : "#{number_with_delimiter(hits)} results" end - - # Formats availability information for display. - # Expects: - # - status: a string indicating availability status - # - location: an array with three elements: [library name, location name, call number] - # - other_availability: a boolean string indicating if there is availability at other locations - def availability(status, location, other_availability) - blurb = case status.downcase - # `available` is a common status used in Alma/Primo VE for items that are not checked out and should be - # on the shelf - when 'available' - "#{icon('check')} Available in #{location(location)}" - # `check_holdings`: unclear when (or if) this is used. Bento handled this so we did too assuming it was - # meaningful - when 'check_holdings' - "#{icon('question')} May be available in #{location(location)}" - # 'unavailable' is used for items that are checked out, missing, or otherwise not on the shelf - when 'unavailable' - "#{icon('times')} Not currently available in #{location(location)}" - # Unclear if there are other statuses we should handle here. For now we log and display a generic message. - else - Rails.logger.error("Unhandled availability status: #{status.inspect}") - "#{icon('question')} Uncertain availability in #{location(location)} #{status}" - end - - blurb += ' and other locations.' if other_availability.present? - - # We are generating HTML in this helper, so we need to mark it as safe or it will be escaped in the view. - blurb.html_safe - end - - # Fontawesome helper. Currently only takes the icon name and assumes solid sharp style. - # Could be extended later to default to these styles but allow overrides if appropriate. - # Also defaults to aria-hidden true, which is probably what we want for icons used - # purely for decoration. If an icon is used in a more meaningful way, we may want to extend this helper - # to allow passing in additional aria attributes. - def icon(fa) - "" - end - - # Formats location information for availability display. - # Expects an array with three elements: [library name, location name, call number] - def location(loc) - "#{loc[0]} #{loc[1]} (#{loc[2]})" - end end diff --git a/test/helpers/record_helper_test.rb b/test/helpers/record_helper_test.rb index cbec11b6..dafbc755 100644 --- a/test/helpers/record_helper_test.rb +++ b/test/helpers/record_helper_test.rb @@ -341,4 +341,24 @@ class RecordHelperTest < ActionView::TestCase subjects = [] assert_nil deduplicate_subjects(subjects) end + + test 'availability helper handles known statuses correctly' do + location = ['Main Library', 'First Floor', 'QA76.73.R83 2023'] + + available_blurb = availability('available', location, false) + assert_includes available_blurb, 'Available in' + assert_includes available_blurb, location(location) + + check_holdings_blurb = availability('check_holdings', location, false) + assert_includes check_holdings_blurb, 'May be available in' + assert_includes check_holdings_blurb, location(location) + + unavailable_blurb = availability('unavailable', location, false) + assert_includes unavailable_blurb, 'Not currently available in' + assert_includes unavailable_blurb, location(location) + + unknown_blurb = availability('unknown_status', location, false) + assert_includes unknown_blurb, 'Uncertain availability in' + assert_includes unknown_blurb, location(location) + end end diff --git a/test/helpers/results_helper_test.rb b/test/helpers/results_helper_test.rb index c30dfaaa..d3df676b 100644 --- a/test/helpers/results_helper_test.rb +++ b/test/helpers/results_helper_test.rb @@ -17,24 +17,4 @@ class ResultsHelperTest < ActionView::TestCase hits = 9000 assert_equal '9,000 results', results_summary(hits) end - - test 'availability helper handles known statuses correctly' do - location = ['Main Library', 'First Floor', 'QA76.73.R83 2023'] - - available_blurb = availability('available', location, false) - assert_includes available_blurb, 'Available in' - assert_includes available_blurb, location(location) - - check_holdings_blurb = availability('check_holdings', location, false) - assert_includes check_holdings_blurb, 'May be available in' - assert_includes check_holdings_blurb, location(location) - - unavailable_blurb = availability('unavailable', location, false) - assert_includes unavailable_blurb, 'Not currently available in' - assert_includes unavailable_blurb, location(location) - - unknown_blurb = availability('unknown_status', location, false) - assert_includes unknown_blurb, 'Uncertain availability in' - assert_includes unknown_blurb, location(location) - end end From c481a3e1b3ce4b72d23a8cab6c2d21c22058039b Mon Sep 17 00:00:00 2001 From: Jeremy Prevost Date: Wed, 3 Dec 2025 12:00:33 -0500 Subject: [PATCH 4/5] Adjust normalized location in primo to use hash We should move this logic to `holdings` as TIMDEX and Primo are currently normalizing different types of information to a single field. TIMDEX has both `locations` and `holdings` information available whereas Primo only has `locations`. The information in TIMDEX `holdings` aligns well with what Primo `locations` is doing for us here. Since we are not using TIMDEX locations or holdings in search results that use the normalizer, I have opted to resolve that under a separate ticket rather than fixing as part of this work. The data structure I am using here for Primo locations should align well with data available in TIMDEX holdings when we move both to USE normalized holdings. --- app/helpers/record_helper.rb | 2 +- app/models/normalize_primo_record.rb | 8 +++++++- test/helpers/record_helper_test.rb | 6 +++++- test/models/normalize_primo_record_test.rb | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/helpers/record_helper.rb b/app/helpers/record_helper.rb index 0d7d7047..ba6a7274 100644 --- a/app/helpers/record_helper.rb +++ b/app/helpers/record_helper.rb @@ -181,7 +181,7 @@ def icon(fa) # Formats location information for availability display. # Expects an array with three elements: [library name, location name, call number] def location(loc) - "#{loc[0]} #{loc[1]} (#{loc[2]})" + "#{loc[:name]} #{loc[:collection]} (#{loc[:call_number]})" end private diff --git a/app/models/normalize_primo_record.rb b/app/models/normalize_primo_record.rb index 3389b278..f0b3ea0b 100644 --- a/app/models/normalize_primo_record.rb +++ b/app/models/normalize_primo_record.rb @@ -275,12 +275,18 @@ def publisher @record['pnx']['addata']['pub'].first end + # Current logic in this method should likely move to `holdings` field def location return unless @record['delivery'] return unless @record['delivery']['bestlocation'] loc = @record['delivery']['bestlocation'] - [loc['mainLocation'], loc['subLocation'], loc['callNumber']] + + { + name: loc['mainLocation'], + collection: loc['subLocation'], + call_number: loc['callNumber'] + } end def subjects diff --git a/test/helpers/record_helper_test.rb b/test/helpers/record_helper_test.rb index dafbc755..36c9d8ea 100644 --- a/test/helpers/record_helper_test.rb +++ b/test/helpers/record_helper_test.rb @@ -343,7 +343,11 @@ class RecordHelperTest < ActionView::TestCase end test 'availability helper handles known statuses correctly' do - location = ['Main Library', 'First Floor', 'QA76.73.R83 2023'] + location = { + name: 'Main Library', + collection: 'First Floor', + call_number: 'QA76.73.R83 2023' + } available_blurb = availability('available', location, false) assert_includes available_blurb, 'Available in' diff --git a/test/models/normalize_primo_record_test.rb b/test/models/normalize_primo_record_test.rb index e6e10524..4f23d610 100644 --- a/test/models/normalize_primo_record_test.rb +++ b/test/models/normalize_primo_record_test.rb @@ -174,7 +174,7 @@ def cdi_record test 'returns best location with call number' do normalized = NormalizePrimoRecord.new(full_record, 'test').normalize - expected_location = ['Hayden Library', 'Stacks', 'QA76.73.R83 2023'] + expected_location = { name: 'Hayden Library', collection: 'Stacks', call_number: 'QA76.73.R83 2023' } assert_equal expected_location, normalized[:location] end From d79005ed0602ecdcb7c5ee7c73ac252c5fcf31f5 Mon Sep 17 00:00:00 2001 From: Jeremy Prevost Date: Wed, 3 Dec 2025 13:36:20 -0500 Subject: [PATCH 5/5] Moves availability out of links loop This may need further adjustment when we add LibKey and any other links that are still not implemented --- app/views/search/_result_primo.html.erb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/search/_result_primo.html.erb b/app/views/search/_result_primo.html.erb index 2317eedf..5096b4ff 100644 --- a/app/views/search/_result_primo.html.erb +++ b/app/views/search/_result_primo.html.erb @@ -38,11 +38,11 @@ <% if result[:links].present? %> <% result[:links].each do |link| %> <%= link_to link['kind'].titleize, link['url'], class: 'button' %> - <% if result[:availability].present? %> - <%= availability(result[:availability], result[:location], result[:other_availability]) %> - <% end %> <% end %> <% end %> + <% if result[:availability].present? %> + <%= availability(result[:availability], result[:location], result[:other_availability]) %> + <% end %>