atomic_lti 1.5.7 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -93,5 +93,8 @@ module AtomicLti
93
93
  super(msg)
94
94
  end
95
95
  end
96
+
97
+ class PaginationLimitExceeded < AtomicLtiException
98
+ end
96
99
  end
97
100
  end
@@ -0,0 +1,36 @@
1
+ module AtomicLti
2
+ module PagingHelper
3
+ MAX_PAGES = 200
4
+
5
+ def self.response_link_urls(response, *rels)
6
+ links = response.headers["link"]&.split(",") || []
7
+ urls = {}
8
+ rels.each do |rel|
9
+ urls[rel] = link_url(links, rel)
10
+ end
11
+ urls.values_at(*rels)
12
+ end
13
+
14
+ def self.link_url(links, rel)
15
+ matching_link = links.detect { |link| link.include?("rel=\"#{rel}\"") }
16
+
17
+ return unless matching_link
18
+
19
+ matching_link.split(";")[0].gsub(/[<>\s]/, "")
20
+ end
21
+
22
+ def self.paginate_request
23
+ all = []
24
+ next_link = nil
25
+ loop do
26
+ result, next_link = yield(next_link)
27
+ all << result
28
+
29
+ break if next_link.blank? || result.blank?
30
+
31
+ raise AtomicLti::Exceptions::PaginationLimitExceeded if all.count > MAX_PAGES
32
+ end
33
+ all.compact.flatten
34
+ end
35
+ end
36
+ end
@@ -26,14 +26,57 @@ module AtomicLti
26
26
  end
27
27
 
28
28
  def get_next_url(response)
29
- link = response.headers["link"]
30
- return nil if link.blank?
29
+ next_url, = AtomicLti::PagingHelper.response_link_urls(response, "next")
30
+ next_url
31
+ end
31
32
 
32
- if url = link.split(",").detect { |l| l.split(";")[1].strip == 'rel="next"' }
33
- url.split(";")[0].gsub(/[\<\>\s]/, "")
34
- end
33
+ def service_get(*args)
34
+ logged_service_call(:get, *args)
35
+ end
36
+
37
+ def service_put(*args)
38
+ logged_service_call(:put, *args)
39
+ end
40
+
41
+ def service_post(*args)
42
+ logged_service_call(:post, *args)
43
+ end
44
+
45
+ def service_delete(*args)
46
+ logged_service_call(:delete, *args)
35
47
  end
36
48
 
49
+ def logged_service_call(method, *args)
50
+ Rails.logger.debug("Making service call #{method} #{args}")
51
+ response = HTTParty.send(method, *args)
52
+ Rails.logger.debug("Got status #{response.code} for service call #{method} #{args}")
53
+
54
+ if response.body.present? && response.success?
55
+ parsed_body = JSON.parse(response.body)
56
+ end
57
+
58
+ if !response.success? && response.code != 404
59
+ Rails.logger.error("Encountered an error while making service request #{method} #{args}")
60
+ Rails.logger.error("Got code #{response.code}")
61
+ Rails.logger.error(response.body)
62
+ end
63
+
64
+ [response, parsed_body]
65
+ rescue JSON::ParserError => e
66
+ # We do not reraise this error as previously we did not check at all for valid json. This is purely for
67
+ # logging purposes.
68
+ Rails.logger.error("Encountered an error while parsing response for service request #{method} #{args}")
69
+ Rails.logger.error(response.body)
70
+ Rails.logger.error(e)
71
+
72
+ [response, nil]
73
+ rescue StandardError => e
74
+ Rails.logger.error("Encountered an error while making service request #{method} #{args}")
75
+ Rails.logger.error(response&.body)
76
+ Rails.logger.error(e)
77
+
78
+ raise e
79
+ end
37
80
  end
38
81
  end
39
82
  end
@@ -60,16 +60,33 @@ module AtomicLti
60
60
 
61
61
  # List line items
62
62
  # Canvas: https://canvas.beta.instructure.com/doc/api/line_items.html#method.lti/ims/line_items.index
63
- def list(query = {})
63
+ def list(query = {}, page_url: nil)
64
+ url = if page_url.present?
65
+ page_url
66
+ else
67
+ uri = Addressable::URI.parse(endpoint(@id_token_decoded))
68
+ uri.query_values = (uri.query_values || {}).merge(query)
69
+ uri.to_str
70
+ end
71
+
64
72
  accept = { "Accept" => "application/vnd.ims.lis.v2.lineitemcontainer+json" }
65
- HTTParty.get(endpoint(@id_token_decoded), headers: headers(accept), query: query)
73
+ response, = service_get(url, headers: headers(accept))
74
+ response
75
+ end
76
+
77
+ def list_all(query = {})
78
+ AtomicLti::PagingHelper.paginate_request do |next_link|
79
+ result_page = list(query, page_url: next_link)
80
+ [JSON.parse(result_page.body), get_next_url(result_page)]
81
+ end
66
82
  end
67
83
 
68
84
  # Get a specific line item
69
85
  # https://canvas.beta.instructure.com/doc/api/line_items.html#method.lti/ims/line_items.show
70
86
  def show(line_item_url)
71
87
  accept = { "Accept" => "application/vnd.ims.lis.v2.lineitem+json" }
72
- HTTParty.get(line_item_url, headers: headers(accept))
88
+ response, = service_get(line_item_url, headers: headers(accept))
89
+ response
73
90
  end
74
91
 
75
92
  # Create a line item
@@ -77,18 +94,21 @@ module AtomicLti
77
94
  # Canvas: https://canvas.beta.instructure.com/doc/api/line_items.html#method.lti/ims/line_items.create
78
95
  def create(attrs = nil)
79
96
  content_type = { "Content-Type" => "application/vnd.ims.lis.v2.lineitem+json" }
80
- HTTParty.post(endpoint(@id_token_decoded), body: JSON.dump(attrs), headers: headers(content_type))
97
+ response, = service_post(endpoint(@id_token_decoded), body: JSON.dump(attrs), headers: headers(content_type))
98
+ response
81
99
  end
82
100
 
83
101
  # Update a line item
84
102
  # Canvas: https://canvas.beta.instructure.com/doc/api/line_items.html#method.lti/ims/line_items.update
85
103
  def update(line_item_url, attrs)
86
104
  content_type = { "Content-Type" => "application/vnd.ims.lis.v2.lineitem+json" }
87
- HTTParty.put(line_item_url, body: JSON.dump(attrs), headers: headers(content_type))
105
+ response, = service_put(line_item_url, body: JSON.dump(attrs), headers: headers(content_type))
106
+ response
88
107
  end
89
108
 
90
109
  def delete(line_item_url)
91
- HTTParty.delete(line_item_url, headers: headers)
110
+ response, = service_delete(line_item_url, headers: headers)
111
+ response
92
112
  end
93
113
  end
94
114
  end
@@ -49,16 +49,29 @@ module AtomicLti
49
49
  uri.query_values = (uri.query_values || {}).merge(query)
50
50
  uri.to_str
51
51
  end
52
- verify_received_user_names(
53
- HTTParty.get(
54
- url,
55
- headers: headers(
56
- {
57
- "Accept" => "application/vnd.ims.lti-nrps.v2.membershipcontainer+json",
58
- },
59
- ),
52
+ response, = service_get(
53
+ url,
54
+ headers: headers(
55
+ {
56
+ "Accept" => "application/vnd.ims.lti-nrps.v2.membershipcontainer+json",
57
+ },
60
58
  ),
61
59
  )
60
+
61
+ verify_received_user_names(response)
62
+ end
63
+
64
+ def list_all(query: {})
65
+ page_body = nil
66
+
67
+ members = AtomicLti::PagingHelper.paginate_request do |next_link|
68
+ result_page = list(query: query, page_url: next_link)
69
+ page_body = JSON.parse(result_page.body)
70
+ [page_body["members"], get_next_url(result_page)]
71
+ end
72
+
73
+ page_body["members"] = members
74
+ page_body
62
75
  end
63
76
 
64
77
  def verify_received_user_names(names_and_roles_memberships)
@@ -7,14 +7,30 @@ module AtomicLti
7
7
  [AtomicLti::Definitions::AGS_SCOPE_RESULT]
8
8
  end
9
9
 
10
- def list(line_item_id)
11
- url = "#{line_item_id}/results"
12
- HTTParty.get(url, headers: headers)
10
+ def list(line_item_id, query: {}, page_url: nil)
11
+ url = if page_url.present?
12
+ page_url
13
+ else
14
+ uri = Addressable::URI.parse("#{line_item_id}/results")
15
+ uri.query_values = (uri.query_values || {}).merge(query)
16
+ uri.to_str
17
+ end
18
+
19
+ accept = { "Accept" => "application/vnd.ims.lis.v2.resultcontainer+json" }
20
+ response, = service_get(url, headers: headers(accept))
21
+ response
22
+ end
23
+
24
+ def list_all(line_item_id, query: {})
25
+ AtomicLti::PagingHelper.paginate_request do |next_link|
26
+ result_page = list(line_item_id, query: query, page_url: next_link)
27
+ [JSON.parse(result_page.body), get_next_url(result_page)]
28
+ end
13
29
  end
14
30
 
15
- def show(line_item_id, result_id)
16
- url = "#{line_item_id}/results/#{result_id}"
17
- HTTParty.get(url, headers: headers)
31
+ def show(result_id)
32
+ response, = service_get(result_id, headers: headers)
33
+ response
18
34
  end
19
35
 
20
36
  end
@@ -190,7 +190,7 @@ module AtomicLti
190
190
  state_verified: state_verified,
191
191
  }
192
192
 
193
- if !state_verified && request.params["lti_storage_target"].present? && AtomicLti.use_post_message_storage
193
+ if !state_verified && AtomicLti.use_post_message_storage
194
194
  env["atomic.validated.state_validation"][:lti_storage_params] =
195
195
  build_lti_storage_params(request, platform)
196
196
  end
@@ -361,7 +361,7 @@ module AtomicLti
361
361
  target: request.params["lti_storage_target"],
362
362
  originSupportBroken: !AtomicLti.set_post_message_origin,
363
363
  platformOIDCUrl: platform.oidc_url,
364
- }
364
+ }.compact
365
365
  end
366
366
  end
367
367
  end
@@ -1,3 +1,3 @@
1
1
  module AtomicLti
2
- VERSION = "1.5.7".freeze
2
+ VERSION = "1.6.0".freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atomic_lti
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.7
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Petro
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2023-10-30 00:00:00.000000000 Z
13
+ date: 2023-12-06 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: pg
@@ -71,6 +71,7 @@ files:
71
71
  - app/lib/atomic_lti/exceptions.rb
72
72
  - app/lib/atomic_lti/lti.rb
73
73
  - app/lib/atomic_lti/open_id.rb
74
+ - app/lib/atomic_lti/paging_helper.rb
74
75
  - app/lib/atomic_lti/params.rb
75
76
  - app/lib/atomic_lti/role_enforcement_mode.rb
76
77
  - app/lib/atomic_lti/services/base.rb
@@ -132,7 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
132
133
  - !ruby/object:Gem::Version
133
134
  version: '0'
134
135
  requirements: []
135
- rubygems_version: 3.4.19
136
+ rubygems_version: 3.4.10
136
137
  signing_key:
137
138
  specification_version: 4
138
139
  summary: AtomicLti implements the LTI Advantage specification.