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

pulibrary / bibdata / 4a135bb0-d77d-42e0-8dcc-cb6152f6d386

06 Sep 2023 03:26PM UTC coverage: 90.955% (-0.09%) from 91.048%
4a135bb0-d77d-42e0-8dcc-cb6152f6d386

push

circleci

web-flow
Merge pull request #2240 from pulibrary/i2239-sru

i2239: Use SRU instead of Alma API for bib record retrieval

11 of 11 new or added lines in 2 files covered. (100.0%)

3429 of 3770 relevant lines covered (90.95%)

333.25 hits per line

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

71.77
/app/controllers/bibliographic_controller.rb
1
class BibliographicController < ApplicationController # rubocop:disable Metrics/ClassLength
1✔
2
  include FormattingConcern
1✔
3

4
  def adapter
1✔
5
    @adapter ||= AlmaAdapter.new
49✔
6
  end
7

8
  def index
1✔
9
    if params[:bib_id]
4✔
10
      if params.fetch(:holdings_only, '0') == '1'
3✔
11
        redirect_to action: :bib_holdings, bib_id: params[:bib_id], adapter: params[:adapter], status: :moved_permanently
1✔
12
      elsif params.fetch(:items_only, '0') == '1'
2✔
13
        redirect_to action: :bib_items, bib_id: params[:bib_id], adapter: params[:adapter], status: :moved_permanently
1✔
14
      else
15
        redirect_to action: :bib, bib_id: params[:bib_id], adapter: params[:adapter], status: :moved_permanently
1✔
16
      end
17
    else
18
      render plain: "Record please supply a bib id", status: :not_found
1✔
19
    end
20
  end
21

22
  # Returns availability for a single ID
23
  # Client: This endpoint is used by orangelight to render status on the catalog
24
  #   show page
25
  def availability
1✔
26
    id = params[:bib_id]
2✔
27
    availability = adapter.get_availability_one(id:, deep_check: (params[:deep] == "true"))
2✔
28
    respond_to do |wants|
1✔
29
      wants.json { render json: availability }
2✔
30
    end
31
  rescue => e
32
    handle_alma_exception(exception: e, message: "Failed to retrieve availability for ID: #{id}")
1✔
33
  end
34

35
  # Returns availability for multiple IDs
36
  # Client: This endpoint is used by orangelight to render status on the catalog
37
  #   search results page
38
  def availability_many
1✔
39
    ids = (params[:bib_ids] || "").split(",")
4✔
40
    availability = adapter.get_availability_many(ids:, deep_check: ActiveModel::Type::Boolean.new.cast(params[:deep]))
4✔
41
    respond_to do |wants|
2✔
42
      wants.json { render json: availability }
4✔
43
    end
44
  rescue => e
45
    handle_alma_exception(exception: e, message: "Failed to retrieve availability for IDs: #{ids}")
2✔
46
  end
47

48
  # Returns availability for a single holding in a bib record
49
  # Client: This endpoint is used by Requests to populate a request form and
50
  #   submit requests to the ILS
51
  def availability_holding
1✔
52
    if params[:bib_id] && params[:holding_id]
6✔
53
      availability = adapter.get_availability_holding(id: params[:bib_id], holding_id: params[:holding_id])
6✔
54
      respond_to do |wants|
5✔
55
        wants.json { render json: availability, status: availability.nil? ? 404 : 200 }
10✔
56
      end
57
    else
58
      render plain: "Please supply a bib id and a holding id", status: :not_found
×
59
    end
60
  rescue => e
61
    handle_alma_exception(exception: e, message: "Failed to retrieve holdings for: #{params[:bib_id]}/#{params[:holding_id]}")
1✔
62
  end
63

64
  # Client: This endpoint is used by orangelight to present the staff view
65
  #   and sometimes by individuals to pull records from the ILS
66
  def bib
1✔
67
    opts = {
68
      holdings: params.fetch('holdings', 'true') == 'true',
13✔
69
      holdings_in_bib: params.fetch('holdings_in_bib', 'true') == 'true'
70
    }
71

72
    begin
73
      records = adapter.get_bib_record(bib_id_param)
13✔
74
      records.strip_non_numeric! unless opts[:holdings]
2✔
75
    rescue => e
76
      return handle_alma_exception(exception: e, message: "Failed to retrieve the record using the bib. ID: #{bib_id_param}")
2✔
77
    end
78

79
    if records.nil?
2✔
80
      render plain: "Record #{params[:bib_id]} not found or suppressed", status: :not_found
×
81
      Rails.logger.error "Record #{params[:bib_id]} not found or suppressed"
×
82
    else
83
      respond_to do |wants|
2✔
84
        wants.json  do
2✔
85
          json = MultiJson.dump(pass_records_through_xml_parser(records))
×
86
          render json:
×
87
        end
88
        wants.xml do
2✔
89
          xml = records_to_xml_string(records)
2✔
90
          render xml:
2✔
91
        end
92
      end
93
    end
94
  end
95

96
  # Client: Used by firestone_locator to pull bibliographic data
97
  #   Also used to pull orangelight and pul_solr test fixtures
98
  def bib_solr
1✔
99
    opts = {
100
      holdings: params.fetch('holdings', 'true') == 'true',
4✔
101
      holdings_in_bib: params.fetch('holdings_in_bib', 'true') == 'true'
102
    }
103
    records = adapter.get_bib_record(bib_id_param)
4✔
104
    if records.nil?
×
105
      render plain: "Record #{params[:bib_id]} not found or suppressed", status: :not_found
×
106
    else
107
      solr_doc = indexer.map_record(records)
×
108
      render json: solr_doc
×
109
    end
110
  rescue => e
111
    handle_alma_exception(exception: e, message: "Failed to retrieve the holding records for the bib. ID: #{sanitize(params[:bib_id])}")
×
112
  end
113

114
  # Client: No known use cases
115
  def bib_holdings
1✔
116
    records = adapter.get_holding_records(sanitize(params[:bib_id]))
7✔
117
    if records.empty?
1✔
118
      render plain: "Record #{params[:bib_id]} not found or suppressed", status: :not_found
×
119
    else
120
      respond_to do |wants|
1✔
121
        wants.json  do
1✔
122
          json = MultiJson.dump(pass_records_through_xml_parser(records))
×
123
          render json:
×
124
        end
125
        wants.xml do
1✔
126
          xml = records_to_xml_string(records)
1✔
127
          render xml:
1✔
128
        end
129
      end
130
    end
131
  rescue => e
132
    handle_alma_exception(exception: e, message: "Failed to retrieve the holding records for the bib. ID: #{sanitize(params[:bib_id])}")
3✔
133
  end
134

135
  # bibliographic/:bib_id/items
136
  # Client: Used by figgy to check CDL status. Used by firestone_locator for
137
  #   call number and location data
138
  def bib_items
1✔
139
    item_keys = ["id", "pid", "perm_location", "temp_location", "cdl"]
13✔
140
    holding_summary = adapter.get_items_for_bib(bib_id_param).holding_summary(item_key_filter: item_keys)
13✔
141

142
    respond_to do |wants|
7✔
143
      wants.json  { render json: MultiJson.dump(add_locator_call_no(holding_summary)) }
14✔
144
      wants.xml { render xml: '<todo but="You probably want JSON anyway" />' }
7✔
145
    end
146
  rescue Alma::BibItemSet::ResponseError
147
    render_not_found(params[:bib_id])
2✔
148
  rescue => e
149
    handle_alma_exception(exception: e, message: "Failed to retrieve items for bib ID: #{bib_id_param}")
×
150
  end
151

152
  private
1✔
153

154
    def render_not_found(id)
1✔
155
      render plain: "Record #{id} not found or suppressed", status: :not_found
2✔
156
    end
157

158
    # Construct or access the indexing service
159
    # @return [IndexingService]
160
    def index_job_queue
1✔
161
      traject_config = Rails.application.config.traject
×
162
      solr_config = Rails.application.config.solr
×
163
      @index_job_queue ||= IndexJobQueue.new(config: traject_config['config'], url: solr_config['url'])
×
164
    end
165

166
    # Ensure that the client is authenticated and the user is a catalog administrator
167
    def protect
1✔
168
      if user_signed_in?
×
169
        render plain: "You are unauthorized", status: :forbidden if !current_user.catalog_admin?
×
170
      else
171
        redirect_to user_cas_omniauth_authorize_path
×
172
      end
173
    end
174

175
    # Retrieve and sanitize the Bib. ID from the request parameters
176
    # @return [String]
177
    def sanitized_id
1✔
178
      id = params[:bib_id]
×
179
      sanitize(id)
×
180
    end
181

182
    # Generate the options for retrieving bib. records from Voyager
183
    # @return [Hash]
184
    def voyager_opts
1✔
185
      {
186
        holdings: params.fetch('holdings', 'true') == 'true',
×
187
        holdings_in_bib: params.fetch('holdings_in_bib', 'true') == 'true'
188
      }
189
    end
190

191
    # Access the URL helpers for the application
192
    # @return [Array<ActionDispatch::Routing::RouteSet::NamedRouteCollection::UrlHelper>]
193
    def url_helpers
1✔
194
      Rails.application.routes.url_helpers
×
195
    end
196

197
    # Access the global Traject Object
198
    # @return [Traject::Indexer::MarcIndexer] the Traject indexer
199
    def indexer
1✔
200
      TRAJECT_INDEXER
×
201
    end
202

203
    # Generate the URL for the application root
204
    # @return [String] the root URL
205
    def root_url
1✔
206
      url_helpers.root_url(host: request.host_with_port)
×
207
    end
208

209
    # Generates the URL for the bibliographic record
210
    # @return [String] the URL
211
    def bib_id_url
1✔
212
      url_helpers.show_bib_url(params[:bib_id], host: request.host_with_port)
×
213
    end
214

215
    # Sanitizes the bib_id HTTP parameter
216
    # @return [String]
217
    def bib_id_param
1✔
218
      sanitize(params[:bib_id])
32✔
219
    end
220

221
    def add_locator_call_no(records)
1✔
222
      records.each do |location, holdings|
7✔
223
        next unless location == "firestone$stacks"
10✔
224
        holdings.each do |holding|
3✔
225
          holding["sortable_call_number"] = sortable_call_number(holding["call_number"])
3✔
226
        end
227
      end
228
    end
229

230
    def sortable_call_number(call_no)
1✔
231
      return call_no unless /^[A-Za-z]/.match?(call_no)
3✔
232
      call_no = make_sortable_call_number(call_no)
3✔
233
      lsort_result = Lcsort.normalize(call_no)
3✔
234
      return lsort_result.gsub('..', '.') unless lsort_result.nil?
3✔
235
      force_number_part_to_have_4_digits(call_no)
×
236
    rescue
237
      call_no
×
238
    end
239

240
    def make_sortable_call_number(call_no)
1✔
241
      tokens = call_no.split(" ")
3✔
242
      needs_adjustment = ["oversize", "folio"].include? tokens.first.downcase
3✔
243
      return call_no unless needs_adjustment
3✔
244
      # Move the first token (e.g. Oversize or Folio) to the end
245
      (tokens[1..] << tokens[0]).join(" ")
1✔
246
    end
247

248
    # This routine adjust something from "A53.blah" to "A0053.blah" for sorting purposes
249
    #
250
    def force_number_part_to_have_4_digits(call_no)
1✔
251
      dot_parts = call_no.tr(',', '.').split('.')
×
252
      return call_no if dot_parts.count <= 1
×
253

254
      parts = dot_parts[0].scan(/[A-Za-z]+|\d+/)
×
255
      parts[1] = parts[1].rjust(4, '0')
×
256
      dot_parts[0] = parts.join('.')
×
257
      dot_parts.join('.')
×
258
    end
259
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