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
data/lib/alma/loan.rb CHANGED
@@ -1,9 +1,38 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alma
2
4
  class Loan < AlmaRecord
5
+ extend Alma::ApiDefaults
6
+
7
+
8
+ def renewable?
9
+ !!renewable
10
+ end
11
+
12
+ def renewable
13
+ response.fetch("renewable", false)
14
+ end
15
+
16
+ def overdue?
17
+ loan_status == "Overdue"
18
+ end
3
19
 
4
20
  def renew
5
- Alma::User.renew_loan({user_id: user_id, loan_id: loan_id})
21
+ Alma::User.renew_loan({ user_id: user_id, loan_id: loan_id })
6
22
  end
7
23
 
24
+ def self.where_user(user_id, args = {})
25
+ # Always expand renewable unless you really don't want to
26
+ args[:expand] ||= "renewable"
27
+ # Default to upper limit
28
+ args[:limit] ||= 100
29
+ response = HTTParty.get(
30
+ "#{users_base_path}/#{user_id}/loans",
31
+ query: args,
32
+ headers: headers,
33
+ timeout: timeout
34
+ )
35
+ Alma::LoanSet.new(response, args)
36
+ end
8
37
  end
9
- end
38
+ end
data/lib/alma/loan_set.rb CHANGED
@@ -1,18 +1,76 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alma
2
4
  class LoanSet < ResultSet
5
+ class ResponseError < Alma::StandardError
6
+ end
7
+
8
+ alias :total_records :total_record_count
9
+
10
+
11
+ attr_reader :results, :raw_response
12
+ def_delegators :results, :empty?
13
+
14
+ def initialize(raw_response, search_args = {})
15
+ @raw_response = raw_response
16
+ @response = raw_response.parsed_response
17
+ @search_args = search_args
18
+ validate(raw_response)
19
+ @results = @response.fetch(key, [])
20
+ .map { |item| single_record_class.new(item) }
21
+ # args passed to the search that returned this set
22
+ # such as limit, expand, order_by, etc
23
+ end
3
24
 
25
+ def loggable
26
+ { search_args: @search_args,
27
+ uri: @raw_response&.request&.uri.to_s
28
+ }.select { |k, v| !(v.nil? || v.empty?) }
29
+ end
30
+
31
+ def validate(response)
32
+ if response.code != 200
33
+ error = "Could not find loans info."
34
+ log = loggable.merge(response.parsed_response)
35
+ raise ResponseError.new(error, log)
36
+ end
37
+ end
38
+
39
+ def all
40
+ Enumerator.new do |yielder|
41
+ offset = 0
42
+ loop do
43
+ extra_args = @search_args.merge({ limit: 100, offset: offset })
44
+ r = (offset == 0) ? self : single_record_class.where_user(user_id, extra_args)
45
+ unless r.empty?
46
+ r.map { |item| yielder << item }
47
+ offset += 100
48
+ else
49
+ raise StopIteration
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def each(&block)
56
+ @results.each(&block)
57
+ end
4
58
 
5
- def top_level_key
6
- 'item_loans'
59
+ def success?
60
+ raw_response.response.code.to_s == "200"
7
61
  end
8
62
 
9
- def response_records_key
10
- 'item_loan'
63
+ def key
64
+ "item_loan"
11
65
  end
12
66
 
13
67
  def single_record_class
14
68
  Alma::Loan
15
69
  end
16
70
 
71
+ private
72
+ def user_id
73
+ @user_id ||= results.first.user_id
74
+ end
17
75
  end
18
76
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alma
4
+ class Location < AlmaRecord
5
+ extend Alma::ApiDefaults
6
+
7
+ def self.all(library_code:, args: {})
8
+ response = HTTParty.get("#{configuration_base_path}/libraries/#{library_code}/locations", query: args, headers: headers, timeout: timeout)
9
+ if response.code == 200
10
+ LocationSet.new(response)
11
+ else
12
+ raise StandardError, get_body_from(response)
13
+ end
14
+ end
15
+
16
+ def self.find(library_code:, location_code:, args: {})
17
+ response = HTTParty.get("#{configuration_base_path}/libraries/#{library_code}/locations/#{location_code}", query: args, headers: headers, timeout: timeout)
18
+ if response.code == 200
19
+ AlmaRecord.new(response)
20
+ else
21
+ raise StandardError, get_body_from(response)
22
+ end
23
+ end
24
+
25
+ def self.get_body_from(response)
26
+ JSON.parse(response.body)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alma
4
+ class LocationSet < ResultSet
5
+ def_delegators :results, :[], :empty?
6
+
7
+ def each(&block)
8
+ results.each(&block)
9
+ end
10
+
11
+ def results
12
+ @results ||= @response.fetch(key, [])
13
+ .map { |item| single_record_class.new(item) }
14
+ end
15
+
16
+ protected
17
+ def key
18
+ "location"
19
+ end
20
+ end
21
+ end
@@ -1,42 +1,53 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alma
2
4
  class RenewalResponse
3
-
4
- include Alma::Error
5
-
6
5
  def initialize(response)
7
- @response = response
8
- @success = response.fetch('item_loan', {})
9
- @renewed = error.empty?
6
+ @raw_response = response
7
+ @response = response.parsed_response
8
+ @success = response.has_key?("loan_id")
9
+ end
10
+
11
+ def loggable
12
+ { uri: @raw_response&.request&.uri.to_s
13
+ }.select { |k, v| !(v.nil? || v.empty?) }
10
14
  end
11
15
 
12
16
  def renewed?
13
- @renewed
17
+ @success
18
+ end
19
+
20
+ def has_error?
21
+ !renewed?
14
22
  end
15
23
 
16
24
  def due_date
17
- @success.fetch('due_date', '')
25
+ @response.fetch("due_date", "")
18
26
  end
19
27
 
20
28
 
21
29
  def due_date_pretty
22
- Time.parse(due_date).strftime('%m-%e-%y %H:%M')
30
+ Time.parse(due_date).strftime("%m-%e-%y %H:%M")
23
31
  end
24
32
 
25
33
  def item_title
26
- if @success
27
- @success['title']
34
+ if renewed?
35
+ @response["title"]
28
36
  else
29
- 'This Item'
37
+ "This Item"
30
38
  end
31
39
  end
32
40
 
33
41
  def message
34
- if @success
42
+ if renewed?
35
43
  "#{item_title} is now due #{due_date}"
36
44
  else
37
45
  "#{item_title} could not be renewed."
38
46
  end
39
47
  end
40
48
 
49
+ def error_message
50
+ @response unless renewed?
51
+ end
41
52
  end
42
- end
53
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alma
4
+ class BibRequest
5
+ class ItemAlreadyExists < Alma::StandardError
6
+ end
7
+
8
+ extend Alma::ApiDefaults
9
+
10
+ REQUEST_TYPES = %w[HOLD DIGITIZATION BOOKING]
11
+
12
+ def self.submit(args)
13
+ request = new(args)
14
+ response = HTTParty.post(
15
+ "#{bibs_base_path}/#{request.mms_id}/requests",
16
+ query: { user_id: request.user_id },
17
+ headers: headers,
18
+ body: request.body.to_json
19
+ )
20
+ Alma::Response.new(response)
21
+ end
22
+
23
+ attr_reader :mms_id, :user_id, :body, :request_type
24
+ def initialize(args)
25
+ @mms_id = args.delete(:mms_id) { raise ArgumentError.new(":mms_id option must be specified to create request") }
26
+ @user_id = args.delete(:user_id) { raise ArgumentError.new(":user_id option must be specified to create request") }
27
+ @request_type = args.fetch(:request_type, "NOT_SPECIFIED")
28
+ validate!(args)
29
+ normalize!(args)
30
+ @body = args
31
+ end
32
+
33
+
34
+ def normalize!(args)
35
+ request_type_normalization!(args)
36
+ additional_normalization!(args)
37
+ end
38
+
39
+ def request_type_normalization!(args)
40
+ method = "#{@request_type.downcase}_normalization".to_sym
41
+ send(method, args) if respond_to? method
42
+ end
43
+
44
+ # Intended to be overridden by subclasses, allowing extra normalization logic to be provided
45
+ def additional_normalization!(args)
46
+ end
47
+
48
+ def validate!(args)
49
+ unless REQUEST_TYPES.include?(request_type)
50
+ raise ArgumentError.new(":request_type option must be specified and one of #{REQUEST_TYPES.join(", ")} to submit a request")
51
+ end
52
+ request_type_validation!(args)
53
+ additional_validation!(args)
54
+ end
55
+
56
+ def request_type_validation!(args)
57
+ method = "#{@request_type.downcase}_validation".to_sym
58
+ send(method, args) if respond_to? method
59
+ end
60
+
61
+ # Intended to be overridden by subclasses, allowing extra validation logic to be provided
62
+ def additional_validation!(args)
63
+ end
64
+
65
+ def digitization_normalization(args)
66
+ if args[:target_destination].is_a? String
67
+ args[:target_destination] = { value: args[:target_destination] }
68
+ end
69
+ end
70
+
71
+ def digitization_validation(args)
72
+ args.fetch(:target_destination) do
73
+ raise ArgumentError.new(
74
+ ":target_destination option must be specified when request_type is DIGITIZATION"
75
+ )
76
+ end
77
+ pd = args.fetch(:partial_digitization) do
78
+ raise ArgumentError.new(
79
+ ":partial_digitization option must be specified when request_type is DIGITIZATION"
80
+ )
81
+ end
82
+ if pd == true
83
+ args.fetch(:comment) do
84
+ raise ArgumentError.new(
85
+ ":comment option must be specified when :request_type is DIGITIZATION and :partial_digitization is true"
86
+ )
87
+ end
88
+ end
89
+ end
90
+
91
+ def booking_normalization(args)
92
+ if args[:material_type].is_a? String
93
+ args[:material_type] = { value: args[:material_type] }
94
+ end
95
+ end
96
+
97
+ def booking_validation(args)
98
+ args.fetch(:booking_start_date) do
99
+ raise ArgumentError.new(
100
+ ":booking_start_date option must be specified when request_type is BOOKING"
101
+ )
102
+ end
103
+ args.fetch(:booking_end_date) do
104
+ raise ArgumentError.new(
105
+ ":booking_end_date option must be specified when request_type is BOOKING"
106
+ )
107
+ end
108
+ args.fetch(:pickup_location_type) do
109
+ raise ArgumentError.new(
110
+ ":pickup_location_type option must be specified when request_type is BOOKING"
111
+ )
112
+ end
113
+ args.fetch(:pickup_location_library) do
114
+ raise ArgumentError.new(
115
+ ":pickup_location_library option must be specified when request_type is BOOKING"
116
+ )
117
+ end
118
+ end
119
+
120
+ def hold_normalization(args)
121
+ # if args[:material_type].is_a? String
122
+ # args[:material_type] = { value: args[:material_type] }
123
+ # end
124
+ end
125
+
126
+ def hold_validation(args)
127
+ args.fetch(:pickup_location_type) do
128
+ raise ArgumentError.new(
129
+ ":pickup_location_type option must be specified when request_type is HOLD"
130
+ )
131
+ end
132
+ args.fetch(:pickup_location_library) do
133
+ raise ArgumentError.new(
134
+ ":pickup_location_library option must be specified when request_type is HOLD"
135
+ )
136
+ end
137
+ end
138
+ end
139
+
140
+ class ItemRequest < BibRequest
141
+ def self.submit(args)
142
+ request = new(args)
143
+ response = HTTParty.post(
144
+ "#{bibs_base_path}/#{request.mms_id}/holdings/#{request.holding_id}/items/#{request.item_pid}/requests",
145
+ query: { user_id: request.user_id },
146
+ headers: headers,
147
+ body: request.body.to_json
148
+ )
149
+ Alma::Response.new(response)
150
+ end
151
+
152
+ attr_reader :holding_id, :item_pid
153
+ def initialize(args)
154
+ super(args)
155
+ @holding_id = args.delete(:holding_id) { raise ArgumentError.new(":holding_id option must be specified to create request") }
156
+ @item_pid = args.delete(:item_pid) { raise ArgumentError.new(":item_pid option must be specified to create request") }
157
+ end
158
+
159
+ def additional_validation!(args)
160
+ args.fetch(:description) do
161
+ raise ArgumentError.new(
162
+ ":description option must be specified when request_type is DIGITIZATION"
163
+ )
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alma
4
+ class RequestOptions
5
+ class ResponseError < Alma::StandardError
6
+ end
7
+
8
+ extend Forwardable
9
+ extend Alma::ApiDefaults
10
+
11
+ attr_accessor :request_options, :raw_response
12
+ def_delegators :raw_response, :response, :request
13
+
14
+ REQUEST_OPTIONS_PERMITTED_ARGS = [:user_id]
15
+
16
+ def initialize(response)
17
+ @raw_response = response
18
+ validate(response)
19
+ @request_options = response.parsed_response["request_option"]
20
+ end
21
+
22
+
23
+ def self.get(mms_id, options = {})
24
+ url = "#{bibs_base_path}/#{mms_id}/request-options"
25
+ options.select! { |k, _| REQUEST_OPTIONS_PERMITTED_ARGS.include? k }
26
+ response = HTTParty.get(url, headers: headers, query: options, timeout: timeout)
27
+ new(response)
28
+ end
29
+
30
+ def loggable
31
+ { uri: @raw_response&.request&.uri.to_s
32
+ }.select { |k, v| !(v.nil? || v.empty?) }
33
+ end
34
+
35
+ def validate(response)
36
+ if response.code != 200
37
+ raise ResponseError.new("Could not get request options.", loggable.merge(response.parsed_response))
38
+ end
39
+ end
40
+
41
+ def hold_allowed?
42
+ !request_options.nil? &&
43
+ !request_options.select { |option| option["type"]["value"] == "HOLD" }.empty?
44
+ end
45
+
46
+ def digitization_allowed?
47
+ !request_options.nil? &&
48
+ !request_options.select { |option| option["type"]["value"] == "DIGITIZATION" }.empty?
49
+ end
50
+
51
+ def booking_allowed?
52
+ !request_options.nil? &&
53
+ !request_options.select { |option| option["type"]["value"] == "BOOKING" }.empty?
54
+ end
55
+
56
+ def resource_sharing_broker_allowed?
57
+ !request_options.nil? &&
58
+ !request_options.select { |option| option["type"]["value"] == "RS_BROKER" }.empty?
59
+ end
60
+
61
+ def ez_borrow_link
62
+ broker = request_options.select { |option| option["type"]["value"] == "RS_BROKER" }
63
+ broker.collect { |opt| opt["request_url"] }.first
64
+ end
65
+ end
66
+ end