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

pulibrary / orangelight / 33aaef4d-e299-4c51-8412-b156165947dc

06 Dec 2023 11:03PM UTC coverage: 95.419% (+0.03%) from 95.394%
33aaef4d-e299-4c51-8412-b156165947dc

Pull #3891

circleci

sandbergja
bundle update
Pull Request #3891: bundle update

5707 of 5981 relevant lines covered (95.42%)

1416.71 hits per line

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

99.02
/app/services/physical_holdings_markup_builder.rb
1
# frozen_string_literal: false
2

3
class PhysicalHoldingsMarkupBuilder < HoldingRequestsBuilder
3✔
4
  include ApplicationHelper
3✔
5

6
  # Generate <span> markup used in links for browsing by call numbers
7
  # @return [String] the markup
8
  def call_number_span
3✔
9
    %(<span class="link-text">#{I18n.t('blacklight.holdings.browse')}</span>\
539✔
10
      <span class="icon-bookslibrary"></span>)
11
  end
12

13
  ##
14
  # Add call number link
15
  # @param [Hash] holding
16
  # @param [String] cn_value - a call number
17
  def call_number_link(holding, cn_value)
3✔
18
    cn = ''
550✔
19
    unless cn_value.nil?
550✔
20
      children = call_number_span
539✔
21
      cn_browse_link = link_to(children.html_safe,
539✔
22
                               "/browse/call_numbers?q=#{CGI.escape(cn_value)}",
23
                               class: 'browse-cn',
24
                               title: "Browse: #{cn_value}",
25
                               'data-toggle' => 'tooltip',
26
                               'data-original-title' => "Browse: #{cn_value}")
27
      cn = "#{holding['call_number']} #{cn_browse_link}"
539✔
28
    end
29
    content_tag(:td, cn.html_safe, class: 'holding-call-number')
550✔
30
  end
31

32
  def holding_location_repository
3✔
33
    children = content_tag(:span,
41✔
34
                           'On-site access',
35
                           class: 'availability-icon badge badge-success',
36
                           title: 'Availability: On-site by request',
37
                           'data-toggle' => 'tooltip')
38
    content_tag(:td, children.html_safe)
41✔
39
  end
40

41
  def holding_location_scsb_span
3✔
42
    markup = content_tag(:span, '',
39✔
43
                         title: '',
44
                         class: 'availability-icon badge',
45
                         data: { toggle: 'tooltip' })
46
    markup
39✔
47
  end
48

49
  def holding_location_scsb(holding, doc_id, holding_id)
3✔
50
    content_tag(:td, holding_location_scsb_span.html_safe,
39✔
51
                class: 'holding-status',
52
                data: {
53
                  'availability_record' => true,
54
                  'record_id' => doc_id,
55
                  'holding_id' => holding_id,
56
                  'scsb-barcode' => holding['items'].first['barcode'],
57
                  'aeon' => scsb_supervised_items?(holding)
58
                })
59
  end
60

61
  def holding_location_default(doc_id, holding_id, location_rules, temp_location_code)
3✔
62
    children = content_tag(:span, '', class: 'availability-icon')
467✔
63

64
    data = {
65
      'availability_record' => true,
467✔
66
      'record_id' => doc_id,
67
      'holding_id' => holding_id,
68
      aeon: self.class.aeon_location?(location_rules)
69
    }
70

71
    data['temp_location_code'] = temp_location_code unless temp_location_code.nil?
467✔
72

73
    content_tag(:td,
467✔
74
                 children.html_safe,
75
                 class: 'holding-status',
76
                 data:)
77
  end
78

79
  # Holding record with "dspace": false
80
  def holding_location_unavailable
3✔
81
    children = content_tag(:span,
3✔
82
                           'Unavailable',
83
                           class: 'availability-icon badge badge-danger',
84
                           title: 'Availability: Embargoed',
85
                           'data-toggle' => 'tooltip')
86
    content_tag(:td, children.html_safe, class: 'holding-status')
3✔
87
  end
88

89
  def self.holding_label(label)
3✔
90
    content_tag(:li, label, class: 'holding-label')
324✔
91
  end
92

93
  def self.shelving_titles_list(holding)
3✔
94
    children = "#{holding_label('Shelving title')} #{listify_array(holding['shelving_title'])}"
1✔
95
    content_tag(:ul, children.html_safe, class: 'shelving-title')
1✔
96
  end
97

98
  def self.location_notes_list(holding)
3✔
99
    children = "#{holding_label('Location note')} #{listify_array(holding['location_note'])}"
3✔
100
    content_tag(:ul, children.html_safe, class: 'location-note')
3✔
101
  end
102

103
  def self.location_has_list(holding)
3✔
104
    children = "#{holding_label('Location has')} #{listify_array(holding['location_has'])}"
306✔
105
    content_tag(:ul, children.html_safe, class: 'location-has')
306✔
106
  end
107

108
  def self.multi_item_availability(doc_id, holding_id)
3✔
109
    content_tag(:ul, '',
551✔
110
                class: 'item-status',
111
                data: {
112
                  'record_id' => doc_id,
113
                  'holding_id' => holding_id
114
                })
115
  end
116

117
  def self.supplements_list(holding)
3✔
118
    children = "#{holding_label('Supplements')} #{listify_array(holding['supplements'])}"
1✔
119
    content_tag(:ul, children.html_safe, class: 'holding-supplements')
1✔
120
  end
121

122
  def self.indexes_list(holding)
3✔
123
    children = "#{holding_label('Indexes')} #{listify_array(holding['indexes'])}"
13✔
124
    content_tag(:ul, children.html_safe, class: 'holding-indexes')
13✔
125
  end
126

127
  def self.journal_issues_list(holding_id)
3✔
128
    content_tag(:ul, '',
246✔
129
                class: 'journal-current-issues',
130
                data: { journal: true, holding_id: })
131
  end
132

133
  def self.scsb_use_label(restriction)
3✔
134
    "#{restriction} Only"
11✔
135
  end
136

137
  def self.scsb_use_toolip(restriction)
3✔
138
    if restriction == 'In Library Use'
11✔
139
      I18n.t('blacklight.scsb.in_library_use')
7✔
140
    else
141
      I18n.t('blacklight.scsb.supervised_use')
4✔
142
    end
143
  end
144

145
  # Generate the markup for record restrictions
146
  # @param holding [Hash] the restrictions for all holdings
147
  # @return [String] the markup
148
  def self.restrictions_markup(restrictions)
3✔
149
    restricted_items = restrictions.map do |value|
10✔
150
      content_tag(:td, scsb_use_label(value),
11✔
151
                  class: 'icon-warning icon-request-reading-room',
152
                  title: scsb_use_toolip(value),
153
                  'data-toggle' => 'tooltip')
154
    end
155
    if restricted_items.length > 1
10✔
156
      list = restricted_items.map { |value| content_tag(:li, value) }
3✔
157
      content_tag(:ul, list.join.html_safe, class: 'restrictions-list item-list')
1✔
158
    else
159
      restricted_items.join.html_safe
9✔
160
    end
161
  end
162

163
  def self.open_location?(location)
3✔
164
    location.nil? ? false : location[:open]
560✔
165
  end
166

167
  def self.requestable_location?(location, adapter, holding)
3✔
168
    return false if adapter.sc_location_with_suppressed_button?(holding)
558✔
169
    if location.nil?
554✔
170
      false
16✔
171
    elsif adapter.unavailable_holding?(holding)
538✔
172
      false
×
173
    else
174
      location[:requestable]
538✔
175
    end
176
  end
177

178
  def self.aeon_location?(location)
3✔
179
    location.nil? ? false : location[:aeon_location]
1,741✔
180
  end
181

182
  def aeon_location?(location)
3✔
183
    self.class.aeon_location?(location)
241✔
184
  end
185

186
  def self.scsb_location?(location)
3✔
187
    location.nil? ? false : /^scsb.+/ =~ location['code']
496✔
188
  end
189

190
  def self.requestable?(adapter, holding_id, location)
3✔
191
    !adapter.alma_holding?(holding_id) || aeon_location?(location) || scsb_location?(location)
564✔
192
  end
193

194
  def self.thesis?(adapter, holding_id)
3✔
195
    holding_id == 'thesis' && adapter.pub_date > 2012
215✔
196
  end
197

198
  def self.numismatics?(holding_id)
3✔
199
    holding_id == 'numismatics'
896✔
200
  end
201

202
  # Generate the CSS class for holding based upon its location and ID
203
  # @param adapter [HoldingRequestsAdapter] adapter for the Solr Document and Bibdata
204
  # @param location [Hash] location information
205
  # @param holding_id [String]
206
  # @return [String] the CSS class
207
  def self.show_request(adapter, location, holding_id)
3✔
208
    if requestable?(adapter, holding_id, location) && !thesis?(adapter, holding_id) || numismatics?(holding_id)
564✔
209
      'service-always-requestable'
199✔
210
    else
211
      'service-conditional'
365✔
212
    end
213
  end
214

215
  # Generate the location services markup for a holding
216
  # @param adapter [HoldingRequestsAdapter] adapter for the Solr Document and Bibdata
217
  # @param holding_id [String]
218
  # @param location_rules [Hash]
219
  # @param link [String] link markup
220
  # @return [String] block markup
221
  def self.location_services_block(adapter, holding_id, location_rules, link, holding)
3✔
222
    content_tag(:td, link,
558✔
223
                class: "location-services #{show_request(adapter, location_rules, holding_id)}",
224
                data: {
225
                  open: open_location?(location_rules),
226
                  requestable: requestable_location?(location_rules, adapter, holding),
227
                  aeon: aeon_location?(location_rules),
228
                  holding_id:
229
                })
230
  end
231

232
  def self.scsb_supervised_items?(holding)
3✔
233
    if holding.key? 'items'
39✔
234
      restricted_items = holding['items'].select do |item|
39✔
235
        item['use_statement'] == 'Supervised Use'
363✔
236
      end
237
      restricted_items.count == holding['items'].count
39✔
238
    else
239
      false
×
240
    end
241
  end
242

243
  def scsb_supervised_items?(holding)
3✔
244
    self.class.scsb_supervised_items?(holding)
39✔
245
  end
246

247
  ##
248
  def self.listify_array(arr)
3✔
249
    arr = arr.map do |e|
324✔
250
      content_tag(:li, e)
556✔
251
    end
252
    arr.join
324✔
253
  end
254

255
  def doc_id(holding)
3✔
256
    holding.dig("mms_id") || adapter.doc_id
1,969✔
257
  end
258

259
  # Example of a temporary holding, in this case holding_id is : firestone$res3hr
260
  # {\"firestone$res3hr\":{\"location_code\":\"firestone$res3hr\",
261
  # \"current_location\":\"Circulation Desk (3 Hour Reserve)\",\"current_library\":\"Firestone Library\",
262
  # \"call_number\":\"HT1077 .M87\",\"call_number_browse\":\"HT1077 .M87\",
263
  # \"items\":[{\"holding_id\":\"22740601020006421\",\"id\":\"23740600990006421\",
264
  # \"status_at_load\":\"1\",\"barcode\":\"32101005621469\",\"copy_number\":\"1\"}]}}
265
  def self.temporary_holding_id?(holding_id)
3✔
266
    /[a-zA-Z]\$[a-zA-Z]/.match?(holding_id)
121✔
267
  end
268

269
  # When it is a temporary location and is requestable, use the first holding_id of this temporary location items.
270
  def self.temporary_location_holding_id_first(holding)
3✔
271
    holding["items"][0]["holding_id"]
1✔
272
  end
273

274
  # Generate the links for a given holding
275
  # TODO: Come back and remove class method calls
276
  def request_placeholder(adapter, holding_id, location_rules, holding)
3✔
277
    doc_id = doc_id(holding)
557✔
278
    view_base = ActionView::Base.new(ActionView::LookupContext.new([]), {}, nil)
557✔
279
    link = request_link_component(adapter:, holding_id:, doc_id:, holding:, location_rules:).render_in(view_base)
557✔
280
    markup = self.class.location_services_block(adapter, holding_id, location_rules, link, holding)
557✔
281
    markup
557✔
282
  end
283

284
  def request_link_component(adapter:, holding_id:, doc_id:, holding:, location_rules:)
3✔
285
    if holding_id == 'thesis' || self.class.numismatics?(holding_id)
557✔
286
      AeonRequestButtonComponent.new(document: adapter.document, holding: { doc_id: holding }, url_class: Requests::NonAlmaAeonUrl)
48✔
287
    elsif holding['items'] && holding['items'].length > 1
509✔
288
      RequestButtonComponent.new(doc_id:, holding_id:, location: location_rules)
268✔
289
    elsif aeon_location?(location_rules)
241✔
290
      AeonRequestButtonComponent.new(document: adapter.document, holding: { doc_id: holding })
97✔
291
    elsif self.class.scsb_location?(location_rules)
144✔
292
      RequestButtonComponent.new(doc_id:, location: location_rules, holding:)
23✔
293
    elsif self.class.temporary_holding_id?(holding_id)
121✔
294
      holding_identifier = self.class.temporary_location_holding_id_first(holding)
1✔
295
      RequestButtonComponent.new(doc_id:, holding_id: holding_identifier, location: location_rules)
1✔
296
    else
297
      RequestButtonComponent.new(doc_id:, holding_id:, location: location_rules)
120✔
298
    end
299
  end
300

301
  attr_reader :adapter
3✔
302
  delegate :content_tag, :link_to, to: :class
3✔
303

304
  # Constructor
305
  # @param adapter [HoldingRequestsAdapter] adapter for the SolrDocument and Bibdata API
306
  def initialize(adapter)
3✔
307
    @adapter = adapter
319✔
308
  end
309

310
  # Builds the markup for online and physical holdings for a given record
311
  # @return [String] the markup for the online and physical holdings
312
  def build
3✔
313
    physical_holdings_block
306✔
314
  end
315

316
  # Generate a <span> element for a holding location
317
  # @param location [String] the location value
318
  # @param holding_id [String] the ID for the holding
319
  # @return [String] <span> markup
320
  def holding_location_span(location, holding_id)
3✔
321
    content_tag(:span, location,
556✔
322
                class: 'location-text',
323
                data: { location: true, holding_id: })
324
  end
325

326
  # Generate the link for a specific holding
327
  # @param holding [Hash] the information for the holding
328
  # @param location [Hash] the location information for the holding
329
  # @param holding_id [String] the ID for the holding
330
  # @param call_number [String] the call number
331
  # @param library [String] the library in which the holding resides
332
  # @param [String] the markup
333
  def locate_link(location, call_number, library, holding)
3✔
334
    locator = StackmapLocationFactory.new(resolver_service: ::StackmapService::Url)
555✔
335
    return '' if locator.exclude?(call_number:, library:)
555✔
336

337
    markup = ''
404✔
338
    markup = stackmap_markup(location, library, holding, call_number) if find_it_location?(location)
404✔
339
    ' ' + markup
404✔
340
  end
341

342
  def stackmap_markup(location, library, holding, call_number)
3✔
343
    if Flipflop.firestone_locator?
316✔
344
      stackmap_url_markup(location, library, holding, call_number)
312✔
345
    else
346
      stackmap_span_markup(location, library, holding)
4✔
347
    end
348
  end
349

350
  def stackmap_url_markup(location, library, holding, call_number)
3✔
351
    doc_id = doc_id(holding)
312✔
352

353
    stackmap_url = "/catalog/#{doc_id}/stackmap?loc=#{location}"
312✔
354
    stackmap_url << "&cn=#{call_number}" if call_number
312✔
355

356
    child = %(<span class="link-text">#{I18n.t('blacklight.holdings.stackmap')}</span>\
312✔
357
    <span class="fa fa-map-marker" aria-hidden="true"></span>)
358
    link_to(child.html_safe, stackmap_url,
312✔
359
              title: I18n.t('blacklight.holdings.stackmap'),
360
              class: 'find-it',
361
              data: {
362
                'map-location' => location.to_s,
363
                'location-library' => library,
364
                'location-name' => holding['location'],
365
                'blacklight-modal' => 'trigger',
366
                'call-number' => call_number,
367
                'library' => library
368
              })
369
  end
370

371
  def stackmap_span_markup(location, library, holding)
3✔
372
    content_tag(:span, '',
4✔
373
                data: {
374
                  'map-location': location.to_s,
375
                  'location-library': library,
376
                  'location-name': holding['location']
377
                })
378
  end
379

380
  # Generate the links for a specific holding
381
  # @param holding [Hash] the information for the holding
382
  # @param location [Hash] the location information for the holding
383
  # @param holding_id [String] the ID for the holding
384
  # @param call_number [String] the call number
385
  # @param [String] the markup
386
  def holding_location_container(holding, location, holding_id, call_number)
3✔
387
    markup = holding_location_span(location, holding_id)
555✔
388
    link_markup = locate_link(holding['location_code'], call_number, holding['library'], holding)
555✔
389
    markup << link_markup.html_safe
555✔
390
    markup
555✔
391
  end
392

393
  # Generate the markup block for a specific holding
394
  # @param holding [Hash] the information for the holding
395
  # @param location [Hash] the location information for the holding
396
  # @param holding_id [String] the ID for the holding
397
  # @param call_number [String] the call number
398
  # @param [String] the markup
399
  def holding_location(holding, location, holding_id, call_number)
3✔
400
    location = holding_location_container(holding, location, holding_id, call_number)
555✔
401
    markup = ''
555✔
402
    markup << content_tag(:td, location.html_safe,
555✔
403
                          class: 'library-location',
404
                          data: { holding_id: })
405
    markup
555✔
406
  end
407

408
  private
3✔
409

410
    # Generate the markup for a physical holding record
411
    # @param holding [Hash] holding information from a Solr Document
412
    # @param holding_id [String] the ID for the holding record
413
    # @return [String] the markup
414
    def process_physical_holding(holding, holding_id)
3✔
415
      markup = ''
550✔
416
      doc_id = doc_id(holding)
550✔
417
      temp_location_code = @adapter.temp_location_code(holding)
550✔
418

419
      location_rules = @adapter.holding_location_rules(holding)
550✔
420
      cn_value = @adapter.call_number(holding)
550✔
421

422
      holding_loc = @adapter.holding_location_label(holding)
550✔
423
      if holding_loc.present?
550✔
424
        markup = holding_location(
550✔
425
          holding,
426
          holding_loc,
427
          holding_id,
428
          cn_value
429
        )
430
      end
431
      markup << call_number_link(holding, cn_value)
550✔
432
      markup << if @adapter.repository_holding?(holding)
550✔
433
                  holding_location_repository
41✔
434
                elsif @adapter.scsb_holding?(holding) && !@adapter.empty_holding?(holding)
509✔
435
                  holding_location_scsb(holding, doc_id, holding_id)
39✔
436
                elsif @adapter.unavailable_holding?(holding)
470✔
437
                  holding_location_unavailable
3✔
438
                else
439
                  holding_location_default(doc_id,
467✔
440
                                           holding_id,
441
                                           location_rules,
442
                                           temp_location_code)
443
                end
444

445
      request_placeholder_markup = request_placeholder(@adapter, holding_id, location_rules, holding)
550✔
446
      markup << request_placeholder_markup.html_safe
550✔
447

448
      markup << build_holding_notes(holding, holding_id)
550✔
449

450
      markup = self.class.holding_block(markup) unless markup.empty?
550✔
451
      markup
550✔
452
    end
453

454
    def build_holding_notes(holding, holding_id)
3✔
455
      holding_notes = ''
550✔
456

457
      holding_notes << self.class.shelving_titles_list(holding) if @adapter.shelving_title?(holding)
550✔
458
      holding_notes << self.class.location_notes_list(holding) if @adapter.location_note?(holding)
550✔
459
      holding_notes << self.class.location_has_list(holding) if @adapter.location_has?(holding)
550✔
460
      holding_notes << self.class.multi_item_availability(doc_id(holding), holding_id)
550✔
461
      holding_notes << self.class.supplements_list(holding) if @adapter.supplements?(holding)
550✔
462
      holding_notes << self.class.indexes_list(holding) if @adapter.indexes?(holding)
550✔
463
      holding_notes << self.class.journal_issues_list(holding_id) if @adapter.journal?
550✔
464

465
      self.class.holding_details(holding_notes) unless holding_notes.empty?
550✔
466
    end
467

468
    # Generate the markup for physical holdings
469
    # @return [String] the markup
470
    def physical_holdings
3✔
471
      markup = ''
306✔
472
      @adapter.sorted_physical_holdings.each do |holding_id, holding|
306✔
473
        markup << process_physical_holding(holding, holding_id)
550✔
474
      end
475
      markup
306✔
476
    end
477

478
    # Generate the markup block for physical holdings
479
    # @return [String] the markup
480
    def physical_holdings_block
3✔
481
      markup = ''
306✔
482
      children = physical_holdings
306✔
483
      markup = self.class.content_tag(:tbody, children.html_safe) unless children.empty?
306✔
484
      markup
306✔
485
    end
486
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