• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pulibrary / orangelight / d468d197-475b-45e5-b002-efe7b039dc42

14 Jul 2025 03:56PM UTC coverage: 95.35% (-0.02%) from 95.366%
d468d197-475b-45e5-b002-efe7b039dc42

Pull #4962

circleci

web-flow
Add a new bookmark button component (#4969)

* Add a new bookmark button component

This bookmark button replaces the bookmark checkbox on the search
results page.

The first time a non-logged in user presses the button, they are
shown a dialog that encourages them to sign in.  The bookmark is
added, whether or not they choose to sign in.  We then record
the timestamp in localStorage, and do not show them the dialog
again as long as that localStorage is present.

Advances #4910
Advances #4927

* set js: true in spec/features/bookmarks_spec.rb

* Update mocking in component test

Co-authored-by: Christina Chortaria <christinach@users.noreply.github.com>

---------

Co-authored-by: Christina Chortaria <actspatial@gmail.com>
Co-authored-by: Christina Chortaria <christinach@users.noreply.github.com>
Pull Request #4962: Orangelight pos workcycle 07-07-2025

80 of 84 new or added lines in 14 files covered. (95.24%)

1 existing line in 1 file now uncovered.

6029 of 6323 relevant lines covered (95.35%)

1515.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.45
/app/helpers/application_helper.rb
1
# frozen_string_literal: false
2

3
module ApplicationHelper
3✔
4
  require './lib/orangelight/string_functions'
3✔
5

6
  # Check the Rails Environment. Currently used for Matomo to support production.
7
  def rails_env?
3✔
8
    Rails.env.production?
482✔
9
  end
10

11
  def show_regular_search?
3✔
12
    !((%w[generate numismatics advanced_search].include? params[:action]) || (%w[advanced].include? params[:controller]))
964✔
13
  end
14

15
  # Generate the markup for the block containing links for requests to item holdings
16
  # holding record fields: 'location', 'library', 'location_code', 'call_number', 'call_number_browse',
17
  # 'shelving_title', 'location_note', 'electronic_access_1display', 'location_has', 'location_has_current',
18
  # 'indexes', 'supplements'
19
  # process online and physical holding information at the same time
20
  # @param [SolrDocument] document - record display fields
21
  # @return [String] online - online holding info html
22
  # @return [String] physical - physical holding info html
23
  def holding_request_block(document)
3✔
24
    adapter = HoldingRequestsAdapter.new(document, Bibdata)
108✔
25
    markup_builder = HoldingRequestsBuilder.new(adapter:,
108✔
26
                                                online_markup_builder: OnlineHoldingsMarkupBuilder,
27
                                                physical_markup_builder: PhysicalHoldingsMarkupBuilder)
28
    online_markup, physical_markup = markup_builder.build
108✔
29
    [online_markup, physical_markup]
108✔
30
  end
31

32
  # Determine whether or not a ReCAP holding has items restricted to supervised use
33
  # @param holding [Hash] holding values
34
  # @return [TrueClass, FalseClass]
35
  def scsb_supervised_items?(holding)
3✔
36
    if holding.key? 'items'
12✔
37
      restricted_items = holding['items'].select { |item| item['use_statement'] == 'Supervised Use' }
63✔
38
      restricted_items.count == holding['items'].count
12✔
39
    else
40
      false
×
41
    end
42
  end
43

44
  # Blacklight index field helper for the facet "series_display"
45
  # @param args [Hash]
46
  def series_results(args)
3✔
47
    series_display =
48
      if params[:f1] == 'in_series'
3✔
49
        same_series_result(params[:q1], args[:document][args[:field]])
×
50
      else
51
        args[:document][args[:field]]
3✔
52
      end
53
    series_display.join(', ')
3✔
54
  end
55

56
  # Retrieve the same series for that one being displayed
57
  # @param series [String] series name
58
  # @param series_display [Array<String>] series being displayed
59
  # @param [Array<String>] similarly named series
60
  def same_series_result(series, series_display)
3✔
61
    series_display.select { |t| t.start_with?(series) }
8✔
62
  end
63

64
  # Determines whether or not this is an aeon location (for an item holding)
65
  # @param location [Hash] location values
66
  # @return [TrueClass, FalseClass]
67
  def aeon_location?(location)
3✔
68
    location.nil? ? false : location[:aeon_location]
750✔
69
  end
70

71
  # Retrieve the location information for a given item holding
72
  # @param [Hash] holding values
73
  def holding_location(holding)
3✔
74
    location_code = holding.fetch('location_code', '').to_sym
1,492✔
75
    resolved_location = Bibdata.holding_locations[location_code]
1,492✔
76
    resolved_location ? resolved_location : {}
1,492✔
77
  end
78

79
  # Location display in the search results page
80
  def search_location_display(holding)
3✔
81
    location = holding_location_label(holding)
746✔
82
    render_arrow = (location.present? && holding['call_number'].present?)
746✔
83
    arrow = render_arrow ? ' &raquo; ' : ''
746✔
84
    location_display = content_tag(:span, location, class: 'results_location') + arrow.html_safe +
746✔
85
                       content_tag(:span, holding['call_number'], class: 'call-number')
86
    location_display.html_safe
746✔
87
  end
88

89
  def title_hierarchy(args)
3✔
90
    titles = JSON.parse(args[:document][args[:field]])
6✔
91
    all_links = []
6✔
92
    dirtags = []
6✔
93

94
    titles.each do |title|
6✔
95
      title_links = []
6✔
96
      title.each_with_index do |part, index|
6✔
97
        link_accum = StringFunctions.trim_punctuation(title[0..index].join(' '))
21✔
98
        title_links << link_to(part, "/?search_field=left_anchor&q=#{CGI.escape link_accum}", class: 'search-title', 'data-original-title' => "Search: #{link_accum}", title: "Search: #{link_accum}")
21✔
99
      end
100
      full_title = title.join(' ')
6✔
101
      dirtags << StringFunctions.trim_punctuation(full_title.dir.to_s)
6✔
102
      all_links << title_links.join('<span> </span>').html_safe
6✔
103
    end
104

105
    if all_links.length == 1
6✔
106
      all_links = content_tag(:div, all_links[0], dir: dirtags[0])
6✔
107
    else
108
      all_links = all_links.map.with_index { |l, i| content_tag(:li, l, dir: dirtags[i]) }
×
109
      all_links = content_tag(:ul, all_links.join.html_safe)
×
110
    end
111
    all_links
6✔
112
  end
113

114
  def action_notes_display(args)
3✔
115
    action_notes = JSON.parse(args[:document][args[:field]])
3✔
116
    lines = action_notes.map do |note|
3✔
117
      if note["uri"].present?
4✔
118
        link_to(note["description"], note["uri"])
1✔
119
      else
120
        note["description"]
3✔
121
      end
122
    end
123

124
    if lines.length == 1
3✔
125
      lines = content_tag(:div, lines[0])
2✔
126
    else
127
      lines = lines.map.with_index { |l| content_tag(:li, l) }
3✔
128
      lines = content_tag(:ul, lines.join.html_safe)
1✔
129
    end
130
    lines
3✔
131
  end
132

133
  def name_title_hierarchy(args)
3✔
134
    name_titles = JSON.parse(args[:document][args[:field]])
23✔
135
    all_links = []
23✔
136
    dirtags = []
23✔
137
    name_titles.each do |name_t|
23✔
138
      name_title_links = []
116✔
139
      name_t.each_with_index do |part, i|
116✔
140
        link_accum = StringFunctions.trim_punctuation(name_t[0..i].join(' '))
344✔
141
        if i.zero?
344✔
142
          next if args[:field] == 'name_uniform_title_1display'
116✔
143
          name_title_links << link_to(part, "/?f[author_s][]=#{CGI.escape link_accum}", class: 'search-name-title', 'data-original-title' => "Search: #{link_accum}")
103✔
144
        else
145
          name_title_links << link_to(part, "/?f[name_title_browse_s][]=#{CGI.escape link_accum}", class: 'search-name-title', 'data-original-title' => "Search: #{link_accum}")
228✔
146
        end
147
      end
148
      full_name_title = name_t.join(' ')
116✔
149
      dirtags << StringFunctions.trim_punctuation(full_name_title.dir.to_s)
116✔
150
      name_title_links << link_to('[Browse]', "/browse/name_titles?q=#{CGI.escape full_name_title}", class: 'browse-name-title', 'data-original-title' => "Browse: #{full_name_title}", dir: full_name_title.dir.to_s)
116✔
151
      all_links << name_title_links.join('<span> </span>').html_safe
116✔
152
    end
153

154
    if all_links.length == 1
23✔
155
      all_links = content_tag(:div, all_links[0], dir: dirtags[0])
8✔
156
    else
157
      all_links = all_links.map.with_index { |l, i| content_tag(:li, l, dir: dirtags[i]) }
123✔
158
      all_links = content_tag(:ul, all_links.join.html_safe)
15✔
159
    end
160
    all_links
23✔
161
  end
162

163
  def format_render(args)
3✔
164
    args[:document][args[:field]].join(', ')
107✔
165
  end
166

167
  def location_has(args)
3✔
168
    location_notes = JSON.parse(args[:document][:holdings_1display]).collect { |_k, v| v['location_has'] }.flatten
4✔
169
    if location_notes.length > 1
2✔
170
      content_tag(:ul) do
1✔
171
        location_notes.map { |note| content_tag(:li, note) }.join.html_safe
3✔
172
      end
173
    else
174
      location_notes
1✔
175
    end
176
  end
177

178
  def bibdata_location_code_to_sym(value)
3✔
179
    Bibdata.holding_locations[value.to_sym]
768✔
180
  end
181

182
  def render_location_code(value)
3✔
183
    values = normalize_location_code(value).map do |loc|
4,662✔
184
      location = Bibdata.holding_locations[loc.to_sym]
4,665✔
185
      location.nil? ? loc : "#{loc}: #{location_full_display(location)}"
4,665✔
186
    end
187
    values.one? ? values.first : values
4,662✔
188
  end
189

190
  # Depending on the url, we sometimes get strings, arrays, or hashes
191
  # Returns Array of locations
192
  def normalize_location_code(value)
3✔
193
    case value
4,662✔
194
    when String
195
      Array(value)
4,658✔
196
    when Array
197
      value
2✔
198
    when Hash, ActiveSupport::HashWithIndifferentAccess
199
      value.values
2✔
200
    else
NEW
201
      value
×
202
    end
203
  end
204

205
  def holding_location_label(holding)
3✔
206
    loc_code = holding['location_code']
770✔
207
    location = bibdata_location_code_to_sym(loc_code) unless loc_code.nil?
770✔
208
    # If the Bibdata location is nil, use the location value from the solr document.
209
    alma_location_display(holding, location) unless location.blank? && holding.blank?
770✔
210
  end
211

212
  # Alma location display on search results
213
  def alma_location_display(holding, location)
3✔
214
    if location.nil?
769✔
215
      [holding['library'], holding['location']].select(&:present?).join(' - ')
8✔
216
    else
217
      [location['library']['label'], location['label']].select(&:present?).join(' - ')
761✔
218
    end
219
  end
220

221
  # location = Bibdata.holding_locations[value.to_sym]
222
  def location_full_display(loc)
3✔
223
    loc['label'] == '' ? loc['library']['label'] : loc['library']['label'] + ' - ' + loc['label']
3,837✔
224
  end
225

226
  def html_safe(args)
3✔
227
    args[:document][args[:field]].each_with_index { |v, i| args[:document][args[:field]][i] = v.html_safe }
×
228
  end
229

230
  def current_year
3✔
231
    DateTime.now.year
1✔
232
  end
233

234
  # Construct an adapter for Solr Documents and the bib. data service
235
  # @return [HoldingRequestsAdapter]
236
  def holding_requests_adapter
3✔
237
    HoldingRequestsAdapter.new(@document, Bibdata)
121✔
238
  end
239

240
  # Returns true for locations with remote storage.
241
  # Remote storage locations have a value of 'recap_rmt' in Alma.
242
  def remote_storage?(location_code)
3✔
243
    Bibdata.holding_locations[location_code]["remote_storage"] == 'recap_rmt'
133✔
244
  end
245

246
  # Returns true for locations where the user can walk and fetch an item.
247
  # Currently this logic is duplicated in Javascript code in availability.es6
248
  def find_it_location?(location_code)
3✔
249
    return false if remote_storage?(location_code)
133✔
250
    return false if (location_code || "").start_with?("plasma$", "marquand$")
107✔
251

252
    return false if StackmapService::Url.missing_stackmap_reserves.include?(location_code)
103✔
253

254
    true
103✔
255
  end
256

257
  # Testing this feature with Voice Over - reading the Web content
258
  # If language defaults to english 'en' when no language_iana_primary_s exists then:
259
  # for cyrilic: for example russian, voice over will read each character as: cyrilic <character1>, cyrilic <character2>
260
  # for japanese it announces <character> ideograph
261
  # If there is no lang attribute it announces the same as having lang='en'
262
  def language_iana
3✔
263
    @document[:language_iana_s].present? ? @document[:language_iana_s].first : 'en'
129✔
264
  end
265

266
  def should_show_viewer?
3✔
267
    request.human? && controller.action_name != "librarian_view"
110✔
268
  end
269
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc