alma 0.2.4 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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