alma 0.2.4 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +54 -0
  3. data/.circleci/setup-rubygems.sh +3 -0
  4. data/.github/dependabot.yml +7 -0
  5. data/.gitignore +3 -0
  6. data/.rubocop.yml +134 -0
  7. data/.ruby-version +1 -1
  8. data/Gemfile +5 -1
  9. data/Guardfile +75 -0
  10. data/README.md +146 -57
  11. data/Rakefile +3 -1
  12. data/alma.gemspec +21 -12
  13. data/lib/alma.rb +34 -54
  14. data/lib/alma/alma_record.rb +3 -3
  15. data/lib/alma/api_defaults.rb +39 -0
  16. data/lib/alma/availability_response.rb +69 -31
  17. data/lib/alma/bib.rb +54 -29
  18. data/lib/alma/bib_holding.rb +25 -0
  19. data/lib/alma/bib_item.rb +164 -0
  20. data/lib/alma/bib_item_set.rb +93 -0
  21. data/lib/alma/bib_set.rb +5 -10
  22. data/lib/alma/config.rb +10 -4
  23. data/lib/alma/course.rb +47 -0
  24. data/lib/alma/course_set.rb +17 -0
  25. data/lib/alma/electronic.rb +167 -0
  26. data/lib/alma/electronic/README.md +20 -0
  27. data/lib/alma/electronic/batch_utils.rb +224 -0
  28. data/lib/alma/electronic/business.rb +29 -0
  29. data/lib/alma/error.rb +16 -4
  30. data/lib/alma/fine.rb +16 -0
  31. data/lib/alma/fine_set.rb +41 -8
  32. data/lib/alma/item_request_options.rb +23 -0
  33. data/lib/alma/library.rb +29 -0
  34. data/lib/alma/library_set.rb +21 -0
  35. data/lib/alma/loan.rb +31 -2
  36. data/lib/alma/loan_set.rb +62 -4
  37. data/lib/alma/location.rb +29 -0
  38. data/lib/alma/location_set.rb +21 -0
  39. data/lib/alma/renewal_response.rb +25 -14
  40. data/lib/alma/request.rb +167 -0
  41. data/lib/alma/request_options.rb +66 -0
  42. data/lib/alma/request_set.rb +69 -5
  43. data/lib/alma/response.rb +45 -0
  44. data/lib/alma/result_set.rb +27 -35
  45. data/lib/alma/user.rb +142 -86
  46. data/lib/alma/user_request.rb +19 -0
  47. data/lib/alma/user_set.rb +5 -6
  48. data/lib/alma/version.rb +3 -1
  49. data/log/.gitignore +4 -0
  50. metadata +149 -10
  51. data/.travis.yml +0 -5
  52. data/lib/alma/api.rb +0 -33
@@ -1,14 +1,78 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alma
2
4
  class RequestSet < ResultSet
5
+ class ResponseError < Alma::StandardError
6
+ end
7
+
8
+ alias :total_records :total_record_count
9
+
10
+ attr_reader :results, :raw_response
11
+ def_delegators :results, :empty?
12
+
13
+ def initialize(raw_response)
14
+ @raw_response = raw_response
15
+ @response = raw_response.parsed_response
16
+ validate(raw_response)
17
+ @results = @response.fetch(key, [])
18
+ .map { |item| single_record_class.new(item) }
19
+ end
20
+
21
+ def loggable
22
+ { uri: @raw_response&.request&.uri.to_s
23
+ }.select { |k, v| !(v.nil? || v.empty?) }
24
+ end
3
25
 
26
+ def validate(response)
27
+ if response.code != 200
28
+ error = "Could not find requests."
29
+ log = loggable.merge(response.parsed_response)
30
+ raise ResponseError.new(error, log)
31
+ end
32
+ end
33
+
34
+ def all
35
+ Enumerator.new do |yielder|
36
+ offset = 0
37
+ loop do
38
+ r = (offset == 0) ? self : single_record_class.where_user(user_id, { limit: 100, offset: offset })
39
+ unless r.empty?
40
+ r.map { |item| yielder << item }
41
+ offset += 100
42
+ else
43
+ raise StopIteration
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def each(&block)
50
+ @results.each(&block)
51
+ end
52
+
53
+ def success?
54
+ raw_response.response.code.to_s == "200"
55
+ end
4
56
 
5
- def top_level_key
6
- 'user_requests'
57
+ def key
58
+ "user_request"
7
59
  end
8
60
 
9
- def response_records_key
10
- 'user_request'
61
+ def single_record_class
62
+ Alma::UserRequest
11
63
  end
12
64
 
65
+ private
66
+ def user_id
67
+ @user_id ||= get_user_id_from_path(raw_response.request.uri.path)
68
+ end
69
+
70
+ def get_user_id_from_path(path)
71
+ # Path in user api calls starts with "/almaws/v1/users/123/maybe_something/else"
72
+ split_path = path.split("/")
73
+ # the part immediately following the "users" is going to be the user_id
74
+ user_id_index = split_path.index("users") + 1
75
+ split_path[user_id_index]
76
+ end
13
77
  end
14
- end
78
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Alma
6
+ class Response
7
+ class StandardError < Alma::StandardError
8
+ end
9
+
10
+ extend ::Forwardable
11
+
12
+ attr_reader :raw_response
13
+ def_delegators :raw_response, :body, :success?, :response, :request
14
+
15
+ def initialize(response)
16
+ @raw_response = response
17
+ # We could validate and throw an error here but currently a
18
+ validate(response)
19
+ end
20
+
21
+ def loggable
22
+ { uri: @raw_response&.request&.uri.to_s
23
+ }.select { |k, v| !(v.nil? || v.empty?) }
24
+ end
25
+
26
+ def validate(response)
27
+ if errors.first&.dig("errorCode") == "401136"
28
+ message = "The requested item already exists."
29
+ log = loggable.merge(response.parsed_response)
30
+
31
+ raise Alma::BibRequest::ItemAlreadyExists.new(message, log)
32
+ end
33
+
34
+ if response.code != 200
35
+ log = loggable.merge(response.parsed_response)
36
+ raise StandardError.new("Invalid Response.", log)
37
+ end
38
+ end
39
+
40
+ # Returns an array of errors
41
+ def errors
42
+ @raw_response.parsed_response&.dig("errorList", "error") || []
43
+ end
44
+ end
45
+ end
@@ -1,50 +1,42 @@
1
- module Alma
2
- class ResultSet
3
- extend Forwardable
1
+ # frozen_string_literal: true
4
2
 
5
- include Enumerable
6
- include Alma::Error
3
+ require "forwardable"
7
4
 
8
- def_delegators :list, :each, :size
5
+ class Alma::ResultSet
6
+ extend ::Forwardable
7
+ include Enumerable
8
+ include Alma::Error
9
9
 
10
- def initialize(ws_response)
11
- @response = ws_response
12
- end
10
+ attr_reader :response
13
11
 
14
- def total_record_count
15
- @response[top_level_key].fetch('total_record_count', 0).to_i
16
- end
12
+ def_delegators :response, :[], :fetch
13
+ def_delegators :each, :each_with_index, :size
17
14
 
18
- def list
19
- @list ||= list_results
20
- end
15
+ def initialize(response_body_hash)
16
+ @response = response_body_hash
17
+ end
21
18
 
19
+ def loggable
20
+ { uri: @response&.request&.uri&.to_s }
21
+ .select { |k, v| !(v.nil? || v.empty?) }
22
+ end
22
23
 
23
- def top_level_key
24
- raise NotImplementedError 'Subclasses of ResultSet Need to define the top level key'
25
- end
24
+ def each
25
+ @results ||= @response.fetch(key, [])
26
+ .map { |item| single_record_class.new(item) }
27
+ end
26
28
 
27
- def response_records_key
28
- raise NotImplementedError 'Subclasses of ResultSet Need to define the key for response records'
29
- end
29
+ def total_record_count
30
+ fetch("total_record_count", 0).to_i
31
+ end
32
+ alias :total_records :total_record_count
30
33
 
31
- private
32
- def response_records
33
- @response[top_level_key].fetch(response_records_key,[])
34
+ protected
35
+ def key
36
+ raise NotImplementedError "Subclasses of ResultSet need to define a response key"
34
37
  end
35
38
 
36
- # Subclasses Can override this to use a Custom Class for single record objects.
37
39
  def single_record_class
38
40
  Alma::AlmaRecord
39
41
  end
40
-
41
- def list_results
42
- #If there is only one record in the response, HTTParty returns as a hash, not
43
- # an array of hashes, so wrap in array to normalize.
44
- response_array = (response_records.is_a? Array) ? response_records : [response_records]
45
- response_array.map do |record|
46
- single_record_class.new(record)
47
- end
48
- end
49
- end
50
42
  end
data/lib/alma/user.rb CHANGED
@@ -1,34 +1,114 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alma
2
- class User < AlmaRecord
3
- extend Alma::Api
4
+ class User
5
+ class ResponseError < Alma::StandardError
6
+ end
7
+ extend Forwardable
8
+ extend Alma::ApiDefaults
4
9
 
5
- attr_accessor :id
10
+ def self.find(user_id, args = {})
11
+ args[:expand] ||= "fees,requests,loans"
12
+ response = HTTParty.get("#{self.users_base_path}/#{user_id}", query: args, headers: headers, timeout: timeout)
6
13
 
7
- def post_initialize
8
- @id = response['primary_id'].to_s
9
- @recheck_loans = true
14
+ Alma::User.new response
10
15
  end
11
16
 
12
- def fines
13
- self.class.get_fines({user_id: self.id})
17
+ # Authenticates a Alma user with their Alma Password
18
+ # @param [Hash] args
19
+ # @option args [String] :user_id The unique id of the user
20
+ # @option args [String] :password The users local alma password
21
+ # @return [Boolean] Whether or not the user Successfully authenticated
22
+ def self.authenticate(args)
23
+ user_id = args.delete(:user_id) { raise ArgumentError }
24
+ args.merge!({ op: "auth" })
25
+ response = HTTParty.post("#{users_base_path}/#{user_id}", query: args, headers: headers, timeout: timeout)
26
+ response.code == 204
27
+ end
28
+
29
+
30
+ # The User object can respond directly to Hash like access of attributes
31
+ def_delegators :response, :[], :[]=, :has_key?, :keys, :to_json
32
+
33
+ def initialize(response)
34
+ @raw_response = response
35
+ @response = response.parsed_response
36
+ validate(response)
14
37
  end
15
38
 
16
- def loans
17
- unless @loans && !recheck_loans?
18
- @loans = self.class.get_loans({user_id: self.id})
19
- @recheck_loans = false
39
+ def loggable
40
+ { uri: @raw_response&.request&.uri.to_s
41
+ }.select { |k, v| !(v.nil? || v.empty?) }
42
+ end
43
+
44
+ def validate(response)
45
+ if response.code != 200
46
+ log = loggable.merge(response.parsed_response)
47
+ error = "The user was not found."
48
+ raise ResponseError.new(error, log)
20
49
  end
21
- @loans
50
+ end
51
+
52
+ def response
53
+ @response
54
+ end
55
+
56
+ def id
57
+ self["primary_id"]
58
+ end
59
+
60
+ def total_fines
61
+ response.dig("fees", "value") || "0"
62
+ end
63
+
64
+ def total_requests
65
+ response.dig("requests", "value") || "0"
66
+ end
67
+
68
+ def total_loans
69
+ response.dig("loans", "value") || "0"
70
+ end
71
+
72
+
73
+ # Access the top level JSON attributes as object methods
74
+ def method_missing(name)
75
+ return response[name.to_s] if has_key?(name.to_s)
76
+ super.method_missing name
77
+ end
78
+
79
+ def respond_to_missing?(name, include_private = false)
80
+ has_key?(name.to_s) || super
81
+ end
82
+
83
+
84
+ # Persist the user in it's current state back to Alma
85
+ def save!
86
+ response = HTTParty.put("#{users_base_path}/#{id}", timeout: timeout, headers: headers, body: to_json)
87
+ get_body_from(response)
88
+ end
89
+
90
+
91
+ def fines
92
+ Alma::Fine.where_user(id)
93
+ end
94
+
95
+ def requests
96
+ Alma::UserRequest.where_user(id)
97
+ end
98
+
99
+
100
+ def loans(args = {})
101
+ @loans ||= Alma::Loan.where_user(id, args)
22
102
  end
23
103
 
24
104
  def renew_loan(loan_id)
25
- response = self.class.renew_loan({user_id: self.id, loan_id: loan_id})
105
+ response = self.class.send_loan_renewal_request({ user_id: id, loan_id: loan_id })
26
106
  if response.renewed?
27
107
  @recheck_loans ||= true
28
108
  end
29
- response
30
109
  end
31
110
 
111
+
32
112
  def renew_multiple_loans(loan_ids)
33
113
  loan_ids.map { |id| renew_loan(id) }
34
114
  end
@@ -37,68 +117,40 @@ module Alma
37
117
  renew_multiple_loans(loans.map(&:loan_id))
38
118
  end
39
119
 
40
- def recheck_loans?
41
- @recheck_loans
120
+ def preferred_email
121
+ self["contact_info"]["email"].select { |k, v| k["preferred"] }.first["email_address"]
42
122
  end
43
123
 
44
- def requests
45
- self.class.get_requests({user_id:self.id})
124
+ def email
125
+ self["contact_info"]["email"].map { |e| e["email_address"] }
46
126
  end
47
127
 
48
- class << self
49
- # Static methods that do the actual querying
128
+ def preferred_first_name
129
+ pref_first = self["pref_first_name"] unless self["pref_first_name"] == ""
130
+ pref_first || self["first_name"] || ""
131
+ end
50
132
 
51
- def find(args = {})
52
- #TODO Handle Search Queries
53
- #TODO Handle Pagination
54
- #TODO Handle looping through all results
133
+ def preferred_middle_name
134
+ pref_middle = self["pref_middle_name"] unless self["pref_middle_name"] == ""
135
+ pref_middle || self["middle_name"] || ""
136
+ end
55
137
 
56
- return find_by_id(user_id: args[:user_id]) if args.fetch(:user_id, nil)
57
- params = query_merge args
58
- response = resources.almaws_v1_users.get(params)
59
- Alma::UserSet.new(response)
60
- end
138
+ def preferred_last_name
139
+ pref_last = self["pref_last_name"] unless self["pref_last_name"] == ""
140
+ pref_last || self["last_name"]
141
+ end
61
142
 
62
- def find_by_id(user_id_hash)
63
- params = query_merge user_id_hash
64
- response = resources.almaws_v1_users.user_id.get(params)
65
- User.new(response['user'])
66
- end
143
+ def preferred_suffix
144
+ self["pref_name_suffix"] || ""
145
+ end
67
146
 
68
- def get_fines(args)
69
- #TODO Handle Additional Parameters
70
- #TODO Handle Pagination
71
- #TODO Handle looping through all results
72
- params = query_merge args
73
- response = resources.almaws_v1_users.user_id_fees.get(params)
74
- Alma::FineSet.new(response)
75
- end
147
+ def preferred_name
148
+ "#{preferred_first_name} #{preferred_middle_name} #{preferred_last_name} #{preferred_suffix}"
149
+ end
76
150
 
77
- def get_loans(args)
78
- #TODO Handle Additional Parameters
79
- #TODO Handle Pagination
80
- #TODO Handle looping through all results
81
- params = query_merge args
82
- response = resources.almaws_v1_users.user_id_loans.get(params)
83
- Alma::LoanSet.new(response)
84
- end
85
151
 
86
- def get_requests(args)
87
- #TODO Handle Additional Parameters
88
- #TODO Handle Pagination
89
- #TODO Handle looping through all results
90
- params = query_merge args
91
- response = resources.almaws_v1_users.user_id_requests.get(params)
92
- Alma::RequestSet.new(response)
93
- end
94
152
 
95
- def authenticate(args)
96
- # Authenticates a Alma user with their Alma Password
97
- args.merge!({op: 'auth'})
98
- params = query_merge args
99
- response = resources.almaws_v1_users.user_id.post(params)
100
- response.code == 204
101
- end
153
+ private
102
154
 
103
155
  # Attempts to renew a single item for a user
104
156
  # @param [Hash] args
@@ -106,36 +158,40 @@ module Alma
106
158
  # @option args [String] :loan_id The unique id of the loan
107
159
  # @option args [String] :user_id_type Type of identifier being used to search. OPTIONAL
108
160
  # @return [RenewalResponse] Object indicating the renewal message
109
- def renew_loan(args)
110
- args.merge!({op: 'renew'})
111
- params = query_merge args
112
- response = resources.almaws_v1_users.user_id_loans_loan_id.post(params)
161
+ def self.send_loan_renewal_request(args)
162
+ loan_id = args.delete(:loan_id) { raise ArgumentError }
163
+ user_id = args.delete(:user_id) { raise ArgumentError }
164
+ params = { op: "renew" }
165
+ response = HTTParty.post("#{users_base_path}/#{user_id}/loans/#{loan_id}", query: params, headers: headers)
113
166
  RenewalResponse.new(response)
114
167
  end
115
168
 
116
- # Attempts to renew a multiple items for a user
169
+ # Attempts to renew multiple items for a user
117
170
  # @param [Hash] args
118
171
  # @option args [String] :user_id The unique id of the user
119
- # @option args [Array<String>] :loan_ids Array of loan ids
172
+ # @option args [Array<String>] :loan_ids The unique ids of the loans
120
173
  # @option args [String] :user_id_type Type of identifier being used to search. OPTIONAL
121
- # @return [Array<RenewalResponse>] Object indicating the renewal message
122
- def renew_multiple_loans(args)
123
-
124
- if args.fetch(:loans_ids, nil).respond_to? :map
125
- args.delete(:loan_ids).map do |loan_id|
126
- renew_loan(args.merge(loan_id: loan_id))
127
- end
128
- else
129
- []
130
- end
174
+ # @return [Array<RenewalResponse>] Array of Objects indicating the renewal messages
175
+ def self.send_multiple_loan_renewal_requests(args)
176
+ loan_ids = args.delete(:loan_ids) { raise ArgumentError }
177
+ loan_ids.map { |id| Alma::User.send_loan_renewal_request(args.merge(loan_id: id)) }
178
+ end
179
+
180
+ def get_body_from(response)
181
+ JSON.parse(response.body)
131
182
  end
132
183
 
133
184
 
185
+ def self.users_base_path
186
+ "https://api-na.hosted.exlibrisgroup.com/almaws/v1/users"
187
+ end
134
188
 
189
+ def users_base_path
190
+ self.class.users_base_path
191
+ end
135
192
 
136
- def set_wadl_filename
137
- 'user.wadl'
193
+ def headers
194
+ self.class.headers
138
195
  end
139
- end
140
196
  end
141
- end
197
+ end