alma 0.2.8 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ ## Alma::Electronic
2
+
3
+ A wrapper for the Alma::Electronic API.
4
+
5
+ The main entry point is the get methods.
6
+
7
+ ### To get a list of all the collections.
8
+
9
+ ```
10
+ Alma::Electronic.get()
11
+ ```
12
+
13
+ Will also accept these params:
14
+
15
+ | Parameter | Type | Required | Description |
16
+ | --------- | ----------| --------- | ---------------------------------------------------------------------------------------------------------------|
17
+ | q | xs:string | Optional. | Search query. Optional. Searching for words in interface_name, keywords, name or po_line_id (see Brief Search) |
18
+ | limit | xs:int | Optional. | Default: 10Limits the number of results. Optional. Valid values are 0-100. Default value: 10. |
19
+ | offset | xs:int | Optional. | Default: 0Offset of the results returned. Optional. Default value: 0, which methodseans that the first results will be returned. |
20
+
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "alma/electronic"
5
+
6
+ # Contains batch processing utils for Alma Electronic APIs.
7
+ #
8
+ # This class and its methods are used to iterate over Alma Electronic IDs to
9
+ # process and fetch Alma electronic objects via the Alma Electronic APIs. The
10
+ # https calls are logged and can be used to rerun the batch process without
11
+ # making any further http calls or to just rerun parts of the full batch.
12
+ module Alma
13
+ class Electronic::BatchUtils
14
+ attr_reader :notes, :type
15
+
16
+ # @param [Hash] options The options to create a batch instance.
17
+ # @option [true, false] :chain true indicates a new instance of self returned.
18
+ # @option [Array<String>] :ids List of collection ids.
19
+ # @option ["collection", "service", "portfolio"] :type The Alma Electronic object type.
20
+ # @option [String] :tag A string used to tag the batch session.
21
+ # @option [Logger] :logger A logger used t
22
+ def initialize(options = {})
23
+ options ||= options {}
24
+ @chain = options.fetch(:chain, false)
25
+ @ids = options.fetch(:ids, [])
26
+ @type = options.fetch(:type, "collection")
27
+ @tag = options.fetch(:tag, Time.now.to_s)
28
+
29
+ @@logger = options[:logger] || Logger.new("log/electronic_batch_process.log")
30
+ end
31
+
32
+ def get_collection_notes(ids = nil, options = {})
33
+ ids ||= @ids
34
+ get_notes(options.merge(ids: make_collection_ids(ids), type: "collection"))
35
+ end
36
+
37
+ def get_service_notes(ids = nil, options = {})
38
+ ids ||= @ids
39
+ get_notes(options.merge(ids: get_service_ids(ids, options), type: "service"))
40
+ end
41
+
42
+ def get_portfolio_notes(ids = nil, options = {})
43
+ ids ||= @ids
44
+ get_notes(options.merge(ids: get_portfolio_ids(ids, options, type: "portfolio")))
45
+ end
46
+
47
+ def get_notes(options = {})
48
+ options ||= {}
49
+ chain = options.fetch(:chain, @chain)
50
+ ids = options[:ids] || (chain ? build_ids(options) : @ids)
51
+
52
+ type = options.fetch(:type, @type)
53
+ tag = options.fetch(:tag, @tag)
54
+ @notes = ids.inject({}) do |notes, params|
55
+ id = get_id(type, params)
56
+ start = Time.now
57
+
58
+ begin
59
+ item = Alma::Electronic.get(params)
60
+ rescue StandardError => e
61
+ item = { "error" => e.message }
62
+ end
63
+
64
+ data = ["error", "authentication_note", "public_note"].reduce({}) do |acc, field|
65
+ acc[field] = item[field] if item[field].present?
66
+ acc
67
+ end
68
+
69
+ unavailable = item.dig("service_temporarily_unavailable", "value")
70
+ if unavailable == "1" || unavailable == "true"
71
+ data.merge!(item.slice("service_temporarily_unavailable", "service_unavailable_date", "service_unavailable_reason"))
72
+ end
73
+
74
+ if data.present?
75
+ log(params.merge(data).merge(type: type, start: start, tag: tag))
76
+
77
+ notes[id] = data unless data["error"].present?
78
+ end
79
+
80
+ notes
81
+ end
82
+
83
+ self.class.new(options.merge(
84
+ chain: chain,
85
+ ids: ids,
86
+ type: type,
87
+ tag: tag,
88
+ notes: notes,
89
+ logger: @@logger,
90
+ ))
91
+ end
92
+
93
+ def get_service_ids(ids = @ids, options = {})
94
+ tag = options.fetch(:tag, @tag)
95
+ start = Time.now
96
+
97
+ make_collection_ids(ids)
98
+ .map { |id| id.merge(type: "services") }
99
+ .inject([]) do |service_ids, params|
100
+ params.merge!(tag: tag)
101
+
102
+ begin
103
+ item = Alma::Electronic.get(params)
104
+
105
+ if item["errorList"]
106
+ log params.merge(item["errorList"])
107
+ .merge(start: start)
108
+ else
109
+ item["electronic_service"].each { |service|
110
+ service_id = { service_id: service["id"].to_s }
111
+ service_ids << params.slice(:collection_id)
112
+ .merge(service_id)
113
+
114
+ log params.merge(service_id)
115
+ .merge(start: start)
116
+ }
117
+ end
118
+
119
+ rescue StandardError => e
120
+ log params.merge("error" => e.message)
121
+ .merge(start: start)
122
+ end
123
+
124
+ service_ids
125
+ end
126
+ end
127
+
128
+ # Builds the notes object using the logs.
129
+ def build_notes(options = {})
130
+ options ||= {}
131
+ type ||= options.fetch(:type, "collection")
132
+
133
+ get_logged_items(options)
134
+ .select { |item| item.slice("authentication_note", "public_note").values.any?(&:present?) }
135
+ .inject({}) do |nodes, item|
136
+
137
+ id = item["#{type}_id"]
138
+ nodes.merge(id => item.slice("authentication_note", "public_note"))
139
+ end
140
+ end
141
+
142
+ # Builds list of ids from logs based on failed attempts.
143
+ # Useful for rebuilding part of collection.
144
+ def build_failed_ids(options = {})
145
+ successful_ids = build_successful_ids(options)
146
+ get_logged_items(options)
147
+ .select { |item| item.slice("authentication_note", "public_note").values.all?(&:nil?) }
148
+ .map { |item| item["collection_id"] }
149
+ .select { |id| !successful_ids.include? id }
150
+ .uniq
151
+ end
152
+
153
+ # Builds list of ids from logs based on successful attempts.
154
+ # Useful for verifying that failed ids have always failed.
155
+ def build_successful_ids(options = {})
156
+ get_logged_items(options)
157
+ .select { |item| item.slice("authentication_note", "public_note").values.present? }
158
+ .map { |item| item["collection_id"] }
159
+ .uniq
160
+ end
161
+
162
+ # Builds a list of all ids for a specific session.
163
+ # Useful for analytics purpose or rebuilds.
164
+ def build_ids(options = {})
165
+ build_failed_ids(options) + build_successful_ids(options)
166
+ end
167
+
168
+ def print_notes(options = {})
169
+ options ||= {}
170
+ chain = options.fetch(:chain, @chain)
171
+ notes = options[:notes] || chain ? build_notes(options) : @notes
172
+ type = options.fetch(:type, @type)
173
+ tag = options.fetch(:tag, @tag)
174
+
175
+ filename = options.fetch(:filename, "spec/fixtures/#{type}_notes.json")
176
+
177
+ File.open(filename, "w") do |file|
178
+ file.write(JSON.pretty_generate(notes))
179
+ end
180
+
181
+ self.class.new(options.merge(
182
+ chain: chain,
183
+ notes: notes,
184
+ type: type,
185
+ tag: tag,
186
+ logger: @@logger,
187
+ ))
188
+ end
189
+
190
+ private
191
+ def log(params)
192
+ if defined?(LogUtils)
193
+ LogUtils.json_request_logger(@@logger, params)
194
+ else
195
+ @@logger.info(params)
196
+ end
197
+ end
198
+
199
+ def get_id(type, params = {})
200
+ id = "#{type}_id".to_sym
201
+ params[id]
202
+ end
203
+
204
+ def make_collection_ids(ids = @ids)
205
+ ids.map { |id|
206
+ id.class == Hash ? id : { collection_id: id.to_s }
207
+ }
208
+ end
209
+
210
+ # Returns JSON parsed list of logged items
211
+ def get_logged_items(options = {})
212
+ options ||= {}
213
+ type ||= options.fetch(:type, "collection")
214
+ tag ||= options.fetch(:tag, @tag)
215
+ filename = (@@logger.instance_variable_get :@logdev).filename
216
+ File.readlines(filename)
217
+ .map { |log| log.match(/{.*}/).to_s }
218
+ .select(&:present?)
219
+ .map { |json| JSON.parse(json) }
220
+ .select { |item| item["tag"] == tag }
221
+ .select { |item| item["type"] == type }
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "alma/electronic"
5
+
6
+ module Alma
7
+ # Holds some custom business logic for our Alma Electronic API.
8
+ # This class is not intended for public use.
9
+ class Electronic::Business
10
+ # The Service ID is usually the Collection ID grouped by
11
+ # 2 digits with the first number incremented by 1 and the
12
+ # fifth number decremented by 1.
13
+ #
14
+ # @note However, this pattern does not hold for all cases.
15
+ #
16
+ # @param collection_id [String] The electronic collection id.
17
+ def service_id(collection_id)
18
+ collection_id.scan(/.{1,2}/).each_with_index.map { |char, index|
19
+ if index == 0
20
+ "%02d" % (char.to_i + 1)
21
+ elsif index == 4
22
+ "%02d" % (char.to_i - 1)
23
+ else
24
+ char
25
+ end
26
+ }.join
27
+ end
28
+ end
29
+ end
@@ -11,5 +11,16 @@ module Alma::Error
11
11
  def error
12
12
  @response.fetch('web_service_result', {})
13
13
  end
14
+ end
14
15
 
15
- end
16
+ module Alma
17
+ class StandardError < ::StandardError
18
+ def initialize(message, loggable = {})
19
+ if Alma.configuration.enable_loggable
20
+ message = { error: message }.merge(loggable).to_json
21
+ end
22
+
23
+ super message
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ module Alma
2
+ class Fine < AlmaRecord
3
+ extend Alma::ApiDefaults
4
+
5
+ def self.where_user(user_id, args={})
6
+
7
+ response = HTTParty.get("#{users_base_path}/#{user_id}/fees", query: args, headers: headers, timeout: timeout)
8
+ if response.code == 200
9
+ Alma::FineSet.new(response)
10
+ else
11
+ raise StandardError, get_body_from(response)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,43 +1,54 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alma
2
- class FineSet
3
- extend Forwardable
4
- include Enumerable
5
- #include Alma::Error
4
+ class FineSet < ResultSet
5
+ class ResponseError < Alma::StandardError
6
+ end
6
7
 
7
- attr_reader :response
8
- def_delegators :response, :[], :fetch
8
+ attr_reader :results, :raw_response
9
+ def_delegators :results, :empty?
9
10
 
10
- def initialize(response_body_hash)
11
- @response = response_body_hash
11
+ def initialize(raw_response)
12
+ @raw_response = raw_response
13
+ @response = raw_response.parsed_response
14
+ validate(raw_response)
15
+ @results = @response.fetch(key, [])
16
+ .map { |item| single_record_class.new(item) }
12
17
  end
13
18
 
14
- def key
15
- 'fee'
19
+ def loggable
20
+ { uri: @raw_response&.request&.uri.to_s
21
+ }.select { |k, v| !(v.nil? || v.empty?) }
16
22
  end
17
23
 
18
- def each
19
- @response.fetch(key, []).map{|item| Alma::AlmaRecord.new(item)}
24
+ def validate(response)
25
+ if response.code != 200
26
+ message = "Could not find fines."
27
+ log = loggable.merge(response.parsed_response)
28
+ raise ResponseError.new(message, log)
29
+ end
20
30
  end
21
- alias list each
22
31
 
23
- def size
24
- each.count
32
+ def each(&block)
33
+ @results.each(&block)
25
34
  end
26
35
 
27
- def sum
28
- fetch('total_sum', 0)
36
+ def success?
37
+ raw_response.response.code.to_s == "200"
29
38
  end
30
- alias :total_sum :sum
31
39
 
32
- def currency
33
- fetch('currency', nil)
40
+ def key
41
+ "fee"
34
42
  end
35
43
 
36
- def total_record_count
37
- fetch('total_record_count', 0)
44
+ def sum
45
+ fetch("total_sum", 0)
38
46
  end
39
- alias :total_records :total_record_count
40
47
 
48
+ alias :total_sum :sum
41
49
 
50
+ def currency
51
+ fetch("currency", nil)
52
+ end
42
53
  end
43
54
  end
@@ -0,0 +1,22 @@
1
+ module Alma
2
+ class ItemRequestOptions < RequestOptions
3
+
4
+ class ResponseError < Alma::StandardError
5
+ end
6
+
7
+ def self.get(mms_id, holding_id=nil, item_pid=nil, options={})
8
+ url = "#{bibs_base_path}/#{mms_id}/holdings/#{holding_id}/items/#{item_pid}/request-options"
9
+ options.select! {|k,_| REQUEST_OPTIONS_PERMITTED_ARGS.include? k }
10
+ response = HTTParty.get(url, headers: headers, query: options, timeout: timeout)
11
+ new(response)
12
+ end
13
+
14
+ def validate(response)
15
+ if response.code != 200
16
+ message = "Could not get item request options."
17
+ log = loggable.merge(response.parsed_response)
18
+ raise ResponseError.new(message, log)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,11 +1,16 @@
1
1
  module Alma
2
2
  class Loan < AlmaRecord
3
+ extend Alma::ApiDefaults
3
4
 
4
5
 
5
6
  def renewable?
6
7
  !!renewable
7
8
  end
8
9
 
10
+ def renewable
11
+ response.fetch("renewable", false)
12
+ end
13
+
9
14
  def overdue?
10
15
  loan_status == "Overdue"
11
16
  end
@@ -14,5 +19,18 @@ module Alma
14
19
  Alma::User.renew_loan({user_id: user_id, loan_id: loan_id})
15
20
  end
16
21
 
22
+ def self.where_user(user_id, args={})
23
+ # Always expand renewable unless you really don't want to
24
+ args[:expand] ||= "renewable"
25
+ # Default to upper limit
26
+ args[:limit] ||= 100
27
+ response = HTTParty.get(
28
+ "#{users_base_path}/#{user_id}/loans",
29
+ query: args,
30
+ headers: headers,
31
+ timeout: timeout
32
+ )
33
+ Alma::LoanSet.new(response, args)
34
+ end
17
35
  end
18
36
  end