ims-lti 1.1.13 → 1.2.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 349045d52db72ab3885b106c5874f5e6c217fed2
4
- data.tar.gz: 3dd49abc26a739d0c313f8bdb477317a79c2598d
2
+ SHA256:
3
+ metadata.gz: 801e423357504d51cc0289e45e364d026abd4bbad20dfca90107725796cd258f
4
+ data.tar.gz: cfdcb37216eef088886486f951af05f719b086f7f32f7d8d83dc04f3020b563e
5
5
  SHA512:
6
- metadata.gz: 14f47290fd89679b75b5e5b4957b08c9431e50b69632a313f89a194454b61126c8ee801b5329b246b8c64d7458698629bb289de1c66450498f20051be803506c
7
- data.tar.gz: befc124ca8958e2912466f3a79dd8f20a9105cba6614a3bfe96d374d6dcffa2634493138a08727ef7ffc1d1a929624ee8d3b40e92cfe6f69b5d53f1f984172d0
6
+ metadata.gz: fcc31ec46d3fccb7585bbe3d6dff5ab88227ccea12b4c056a2f8c588a864b26bb9d74d13601e2b08f7bcc404d215b4b0be14a3b96b613b79e795edfeb8ad2d0c
7
+ data.tar.gz: 0bfa98f889d2b848e03200a22f76447bab3df748c9b6a351d42d5ec9218838ba749f5751af59efd5ae8f7db701a28fcf5b8b92cecb253ebfc097cce3cfb7ff59
data/Changelog CHANGED
@@ -1,3 +1,31 @@
1
+ 2023-05-30 Version 1.2.9
2
+ * Loosen version requirement for OAuth gem
3
+
4
+ 2023-01-03 Version 1.2.8
5
+ * Add Ruby 3.0 support by requiring rexml
6
+ * Fix URI encoding bug for queries with an ampersand `&`
7
+
8
+ 2022-12-13 Version 1.2.7
9
+ * Fix content return parameter encoding
10
+
11
+ 2022-04-26 Version 1.2.6
12
+ * Add support for prioritizeNonToolGrade
13
+ * Add support for needsAdditionalReview
14
+
15
+ 2020-02-03 Version 1.2.4
16
+ * Add support for submittedAt date
17
+
18
+ 2020-02-03 Version 1.2.3 -- yanked
19
+
20
+ 2017-06-19 Version 1.2.2
21
+ * Explicitly require 'oauth' gem and specify version range
22
+
23
+ 2017-04-13 Version 1.2.1
24
+ * Remove date field from gemspec
25
+
26
+ 2017-03-08 Version 1.2.0
27
+ * Don't downcase roles
28
+
1
29
  2016-10-05 Version 1.1.13
2
30
  * Fix Oauth::Unauthorized initialization bug
3
31
  * Relax oauth dependency
data/README.md CHANGED
@@ -52,7 +52,7 @@ and it will be signed with OAuth using a key/secret that both the TP and TC shar
52
52
  This is covered in the [LTI security model](http://www.imsglobal.org/LTI/v1p1/ltiIMGv1p1.html#_Toc319560466)
53
53
 
54
54
  Here is an example of a simple TP Sinatra app using this gem:
55
- [LTI Tool Provider](https://github.com/instructure/lti_tool_provider_example)
55
+ [LTI Tool Provider](https://github.com/instructure/lti1_tool_provider_example)
56
56
 
57
57
  Once you find the `oauth_consumer_secret` based on the `oauth_consumer_key` in
58
58
  the request, you can initialize a `ToolProvider` object with them and the post parameters:
@@ -112,5 +112,18 @@ As a Tool Consumer your app will POST an OAuth-signed launch requests to TPs wit
112
112
  [LTI launch data](http://www.imsglobal.org/LTI/v1p1/ltiIMGv1p1.html#_Toc319560465).
113
113
  This is covered in the [LTI security model](http://www.imsglobal.org/LTI/v1p1/ltiIMGv1p1.html#_Toc319560466)
114
114
 
115
+ ```ruby
116
+
117
+ params = { user_id: '123', lti_message_type: IMS::LTI::Models::Messages::BasicLTILaunchRequest::MESSAGE_TYPE }
118
+
119
+ header = SimpleOAuth::Header.new(:post, 'https://yoursite.com', params, consumer_key: oauth_consumer_key, consumer_secret: secret)
120
+
121
+ signed_params = header.signed_attributes.merge(params)
122
+
123
+ IMS::LTI::Services::MessageAuthenticator.new(launch_url, signed_params, secret)
124
+
125
+ ```
126
+
127
+ ## Contributing
115
128
  Here is an example of a simple TC Sinatra app using this gem:
116
129
  [LTI Tool Consumer](https://github.com/instructure/lti_tool_consumer_example)
@@ -6,7 +6,7 @@ module DeprecatedRoleChecks
6
6
  # Check whether the Launch Parameters have a role
7
7
  def has_role?(role)
8
8
  role = role.downcase
9
- @roles && @roles.any?{|r| r.index(role)}
9
+ @roles && @roles.any?{|r| r.downcase.index(role)}
10
10
  end
11
11
 
12
12
  # Convenience method for checking if the user has 'learner' or 'student' role
@@ -96,67 +96,116 @@ module IMS::LTI
96
96
 
97
97
  #generates the return url for file submissions
98
98
  def file_content_return_url(url, text, content_type = nil)
99
- url = CGI::escape(url)
100
- text = CGI::escape(text)
101
- content_type = CGI::escape(content_type) if content_type
102
99
 
103
- return_url = "#{content_return_url}?return_type=file&url=#{url}&text=#{text}"
104
- return_url = "#{return_url}&content_type=#{content_type}" if content_type
100
+ return_url = add_parameters(content_return_url, {
101
+ return_type: 'file',
102
+ url: url,
103
+ text: text
104
+ })
105
+
106
+ if content_type
107
+ return_url = add_parameters(return_url, {
108
+ content_type: content_type
109
+ })
110
+ end
105
111
 
106
112
  return return_url
107
113
  end
108
114
 
109
115
  #generates the return url for url submissions
110
116
  def url_content_return_url(url, title = nil, text = 'link', target = '_blank')
111
- url = CGI::escape(url)
112
- text = CGI::escape(text)
113
- target = CGI::escape(target)
114
117
 
115
- return_url = "#{content_return_url}?return_type=url&url=#{url}&text=#{text}&target=#{target}"
116
- return_url = "#{return_url}&title=#{CGI::escape(title)}" if title
118
+ return_url = add_parameters(content_return_url, {
119
+ return_type: 'url',
120
+ url: url,
121
+ text: text,
122
+ target: target
123
+ })
124
+
125
+ if title
126
+ return_url = add_parameters(return_url, {
127
+ title:title
128
+ })
129
+ end
117
130
 
118
131
  return return_url
119
132
  end
120
133
 
121
134
  #generates the return url for lti launch submissions
122
135
  def lti_launch_content_return_url(url, text='link', title=nil)
123
- url = CGI::escape(url)
124
- text = CGI::escape(text)
125
136
 
126
- return_url = "#{content_return_url}?return_type=lti_launch_url&url=#{url}&text=#{text}"
127
- return_url = "#{return_url}&title=#{CGI::escape(title)}" if title
137
+ return_url = add_parameters(content_return_url, {
138
+ return_type: 'lti_launch_url',
139
+ url: url,
140
+ text: text
141
+ })
142
+
143
+ if title
144
+ return_url = add_parameters(return_url, {
145
+ title:title
146
+ })
147
+ end
128
148
 
129
149
  return return_url
130
150
  end
131
151
 
132
152
  #generates the return url for image submissions
133
153
  def image_content_return_url(url, width, height, alt = '')
134
- url = CGI::escape(url)
135
- width = CGI::escape(width.to_s)
136
- height = CGI::escape(height.to_s)
137
- alt = CGI::escape(alt)
138
154
 
139
- "#{content_return_url}?return_type=image_url&url=#{url}&width=#{width}&height=#{height}&alt=#{alt}"
155
+ add_parameters(content_return_url, {
156
+ return_type: 'image_url',
157
+ url: url,
158
+ width: width.to_s,
159
+ height: height.to_s,
160
+ alt: alt
161
+ })
140
162
  end
141
163
 
142
164
  #generates the return url for iframe submissions
143
165
  def iframe_content_return_url(url, width, height, title = nil)
144
- url = CGI::escape(url)
145
- width = CGI::escape(width.to_s)
146
- height = CGI::escape(height.to_s)
147
166
 
148
- return_url = "#{content_return_url}?return_type=iframe&url=#{url}&width=#{width}&height=#{height}"
149
- return_url = "#{return_url}&title=#{CGI::escape(title)}" if title
167
+ return_url = add_parameters(content_return_url, {
168
+ return_type: 'iframe',
169
+ url: url,
170
+ width: width.to_s,
171
+ height: height.to_s
172
+ })
173
+
174
+ if title
175
+ return_url = add_parameters(return_url, {
176
+ title:title
177
+ })
178
+ end
150
179
 
151
180
  return return_url
152
181
  end
153
182
 
154
183
  #generates the return url for oembed submissions
155
184
  def oembed_content_return_url(url, endpoint)
156
- url = CGI::escape(url)
157
- endpoint = CGI::escape(endpoint)
158
185
 
159
- "#{content_return_url}?return_type=oembed&url=#{url}&endpoint=#{endpoint}"
186
+ add_parameters(content_return_url, {
187
+ return_type: 'oembed',
188
+ url: url,
189
+ endpoint: endpoint
190
+ })
191
+ end
192
+
193
+ private
194
+
195
+ # adds parameters to a url, with consideration for
196
+ # already existing parameters
197
+ def add_parameters(url, params)
198
+ parsed = URI.parse(url)
199
+ query = if parsed.query
200
+ CGI.parse(parsed.query)
201
+ else
202
+ {}
203
+ end
204
+
205
+ query = query.merge(params)
206
+
207
+ parsed.query = URI.encode_www_form(query)
208
+ parsed.to_s
160
209
  end
161
210
  end
162
211
 
@@ -67,6 +67,12 @@ module IMS::LTI
67
67
  accepted_outcome_types.member?("url")
68
68
  end
69
69
 
70
+ # check if the consumer accepts a submitted at date as outcome data
71
+ def accepts_submitted_at?
72
+ accepted_outcome_types.member?("submitted_at") ||
73
+ @ext_params["ext_outcome_submission_submitted_at_accepted"] == "true"
74
+ end
75
+
70
76
  def accepts_outcome_lti_launch_url?
71
77
  accepted_outcome_types.member?("lti_launch_url")
72
78
  end
@@ -75,8 +81,19 @@ module IMS::LTI
75
81
  !!@ext_params["outcome_result_total_score_accepted"]
76
82
  end
77
83
 
84
+ def accepts_needs_additional_review?
85
+ @ext_params["ext_outcome_submission_needs_additional_review_accepted"] == "true"
86
+ end
87
+
88
+ def accepts_prioritize_non_tool_grade?
89
+ @ext_params["ext_outcome_submission_prioritize_non_tool_grade_accepted"] == "true"
90
+ end
91
+
78
92
  # POSTs the given score to the Tool Consumer with a replaceResult and
79
- # adds the specified data. The data hash can have the keys "text", "cdata_text", "url", or "lti_launch_url"
93
+ # adds the specified data.
94
+ #
95
+ # The data hash can have the keys "text", "cdata_text", "url", "submitted_at"
96
+ # "needs_additional_review", "prioritize_non_tool_grade", or "lti_launch_url"
80
97
  #
81
98
  # If both cdata_text and text are sent, cdata_text will be used
82
99
  #
@@ -93,7 +110,8 @@ module IMS::LTI
93
110
 
94
111
  # POSTs the given score to the Tool Consumer with a replaceResult and
95
112
  # adds the specified data. The options hash can have the keys
96
- # :text, :cdata_text, :url, :lti_launch_url, :score, or :total_score
113
+ # :text, :cdata_text, :url, :submitted_at, :lti_launch_url, :score,
114
+ # :needs_additional_review, or :total_score
97
115
  #
98
116
  # If both cdata_text and text are sent, cdata_text will be used
99
117
  # If both total_score and score are sent, total_score will be used
@@ -110,6 +128,9 @@ module IMS::LTI
110
128
  req.outcome_cdata_text = opts[:cdata_text]
111
129
  req.outcome_text = opts[:text]
112
130
  req.outcome_url = opts[:url]
131
+ req.submitted_at = opts[:submitted_at]
132
+ req.needs_additional_review = opts[:needs_additional_review]
133
+ req.prioritize_non_tool_grade = opts[:prioritize_non_tool_grade]
113
134
  req.outcome_lti_launch_url = opts[:lti_launch_url]
114
135
  req.total_score = opts[:total_score]
115
136
  req.post_replace_result!(opts[:score])
@@ -120,9 +141,9 @@ module IMS::LTI
120
141
  include IMS::LTI::Extensions::ExtensionBase
121
142
  include Base
122
143
 
123
- OUTCOME_DATA_TYPES = %w{text url lti_launch_url}
144
+ OUTCOME_DATA_TYPES = %w{text url lti_launch_url submitted_at}
124
145
 
125
- # a list of the outcome data types accepted, currently only 'url' and
146
+ # a list of the outcome data types accepted, currently only 'url', 'submitted_at' and
126
147
  # 'text' are valid
127
148
  #
128
149
  # tc.outcome_data_values_accepted(['url', 'text'])
@@ -150,7 +171,14 @@ module IMS::LTI
150
171
  include IMS::LTI::Extensions::ExtensionBase
151
172
  include Base
152
173
 
153
- attr_accessor :outcome_text, :outcome_url, :outcome_lti_launch_url, :outcome_cdata_text, :total_score
174
+ attr_accessor :outcome_text,
175
+ :outcome_url,
176
+ :submitted_at,
177
+ :outcome_lti_launch_url,
178
+ :outcome_cdata_text,
179
+ :total_score,
180
+ :needs_additional_review,
181
+ :prioritize_non_tool_grade
154
182
 
155
183
  def result_values(node)
156
184
  super
@@ -178,6 +206,15 @@ module IMS::LTI
178
206
  end
179
207
  end
180
208
 
209
+ def details(node)
210
+ super
211
+ return unless has_details_data?
212
+
213
+ node.submittedAt submitted_at if submitted_at
214
+ node.needsAdditionalReview if needs_additional_review
215
+ node.prioritizeNonToolGrade if prioritize_non_tool_grade
216
+ end
217
+
181
218
  def score
182
219
  total_score ? nil : @score
183
220
  end
@@ -186,6 +223,10 @@ module IMS::LTI
186
223
  !!outcome_text || !!outcome_url || !!outcome_lti_launch_url || !!outcome_cdata_text || !!total_score || super
187
224
  end
188
225
 
226
+ def has_details_data?
227
+ !!submitted_at || !!needs_additional_review || !!prioritize_non_tool_grade
228
+ end
229
+
189
230
  def extention_process_xml(doc)
190
231
  super
191
232
  @outcome_text = doc.get_text("//resultRecord/result/resultData/text")
@@ -196,4 +237,4 @@ module IMS::LTI
196
237
 
197
238
  end
198
239
  end
199
- end
240
+ end
@@ -96,7 +96,7 @@ module IMS::LTI
96
96
  if roles_list.is_a?(Array)
97
97
  @roles = roles_list
98
98
  else
99
- @roles = roles_list.split(",").map(&:downcase)
99
+ @roles = roles_list.split(",")
100
100
  end
101
101
  else
102
102
  @roles = nil
@@ -168,6 +168,7 @@ module IMS::LTI
168
168
  end
169
169
  results(record)
170
170
  end
171
+ submission_details(request)
171
172
  end
172
173
  end
173
174
  end
@@ -182,6 +183,10 @@ module IMS::LTI
182
183
  !!score
183
184
  end
184
185
 
186
+ def has_details_data?
187
+ false
188
+ end
189
+
185
190
  def results(node)
186
191
  return unless has_result_data?
187
192
 
@@ -190,6 +195,16 @@ module IMS::LTI
190
195
  end
191
196
  end
192
197
 
198
+ def submission_details(request)
199
+ return unless has_details_data?
200
+ request.submissionDetails do |record|
201
+ details(record)
202
+ end
203
+ end
204
+
205
+ def details(record)
206
+ end
207
+
193
208
  def result_values(node)
194
209
  if score
195
210
  node.resultScore do |res_score|
@@ -46,7 +46,7 @@ module IMS::LTI
46
46
  #
47
47
  class OutcomeResponse
48
48
  include IMS::LTI::Extensions::Base
49
-
49
+
50
50
  attr_accessor :request_type, :score, :message_identifier, :response_code,
51
51
  :post_response, :code_major, :severity, :description, :operation,
52
52
  :message_ref_identifier
@@ -70,7 +70,7 @@ module IMS::LTI
70
70
  response = OutcomeResponse.new
71
71
  response.process_post_response(post_response)
72
72
  end
73
-
73
+
74
74
  def process_post_response(post_response)
75
75
  self.post_response = post_response
76
76
  self.response_code = post_response.code
@@ -105,7 +105,11 @@ module IMS::LTI
105
105
 
106
106
  # Parse Outcome Response data from XML
107
107
  def process_xml(xml)
108
- doc = REXML::Document.new xml
108
+ begin
109
+ doc = REXML::Document.new xml
110
+ rescue => e
111
+ raise IMS::LTI::XMLParseError, "#{e}\nOriginal xml: '#{xml}'"
112
+ end
109
113
  @message_identifier = doc.text("//imsx_statusInfo/imsx_messageIdentifier").to_s
110
114
  @code_major = doc.text("//imsx_statusInfo/imsx_codeMajor")
111
115
  @code_major.downcase! if @code_major
@@ -16,7 +16,7 @@ module IMS::LTI
16
16
  # Check whether the Launch Parameters have a given role
17
17
  def has_exact_role?(role)
18
18
  role = role.downcase
19
- @roles && @roles.any? { |r| r == role }
19
+ @roles && @roles.any? { |r| r.downcase == role }
20
20
  end
21
21
 
22
22
  # Check whether the Launch Parameters have a given role ignoring
@@ -25,7 +25,7 @@ module IMS::LTI
25
25
  # will return true if the role is `urn:lti:role:ims/lis/Instructor/GuestInstructor`
26
26
  def has_base_role?(role)
27
27
  role = role.downcase
28
- @roles && @roles.any? { |r| r.start_with?(role) }
28
+ @roles && @roles.any? { |r| r.downcase.start_with?(role) }
29
29
  end
30
30
 
31
31
  # Convenience method for checking if the user is the system administrator of the TC
@@ -1,3 +1,5 @@
1
+ require 'cgi'
2
+
1
3
  module IMS::LTI
2
4
 
3
5
  # Class for implementing an LTI Tool Provider
@@ -121,7 +123,7 @@ module IMS::LTI
121
123
  messages = []
122
124
  %w{lti_errormsg lti_errorlog lti_msg lti_log}.each do |m|
123
125
  if message = self.send(m)
124
- messages << "#{m}=#{URI.escape(message)}"
126
+ messages << "#{m}=#{CGI.escape(message)}"
125
127
  end
126
128
  end
127
129
  q_string = messages.any? ? ("?" + messages.join("&")) : ''
@@ -135,9 +137,9 @@ module IMS::LTI
135
137
  :consumer_secret => @consumer_secret,
136
138
  :lis_outcome_service_url => lis_outcome_service_url,
137
139
  :lis_result_sourcedid =>lis_result_sourcedid)
138
-
140
+
139
141
  extend_outcome_request(@outcome_requests.last)
140
142
  end
141
-
143
+
142
144
  end
143
145
  end
@@ -0,0 +1,5 @@
1
+ module IMS
2
+ module LTI
3
+ VERSION = '1.2.9'.freeze
4
+ end
5
+ end
data/lib/ims/lti.rb CHANGED
@@ -2,6 +2,7 @@ require 'oauth'
2
2
  require 'builder'
3
3
  require "rexml/document"
4
4
  require 'cgi'
5
+ require 'securerandom'
5
6
 
6
7
  module IMS # :nodoc:
7
8
 
@@ -25,13 +26,16 @@ module IMS # :nodoc:
25
26
  #
26
27
  # require 'ims/lti'
27
28
  module LTI
28
-
29
+
29
30
  # The versions of LTI this library supports
30
31
  VERSIONS = %w{1.0 1.1}
31
-
32
+
32
33
  class InvalidLTIConfigError < StandardError
33
34
  end
34
35
 
36
+ class XMLParseError < StandardError
37
+ end
38
+
35
39
  # POST a signed oauth request with the given key/secret/data
36
40
  def self.post_service_request(key, secret, url, content_type, body)
37
41
  raise IMS::LTI::InvalidLTIConfigError, "" unless key && secret
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ims-lti
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.13
4
+ version: 1.2.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Instructure
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-01-09 00:00:00.000000000 Z
11
+ date: 2023-06-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: builder
@@ -16,14 +16,20 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '1.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '4.0'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
27
  - - ">="
25
28
  - !ruby/object:Gem::Version
26
- version: '0'
29
+ version: '1.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '4.0'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: oauth
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -31,9 +37,6 @@ dependencies:
31
37
  - - ">="
32
38
  - !ruby/object:Gem::Version
33
39
  version: 0.4.5
34
- - - "<"
35
- - !ruby/object:Gem::Version
36
- version: '0.6'
37
40
  type: :runtime
38
41
  prerelease: false
39
42
  version_requirements: !ruby/object:Gem::Requirement
@@ -41,23 +44,40 @@ dependencies:
41
44
  - - ">="
42
45
  - !ruby/object:Gem::Version
43
46
  version: 0.4.5
44
- - - "<"
45
- - !ruby/object:Gem::Version
46
- version: '0.6'
47
47
  - !ruby/object:Gem::Dependency
48
- name: rspec
48
+ name: rexml
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
- type: :development
54
+ type: :runtime
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
60
  version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rspec
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.0'
68
+ - - ">"
69
+ - !ruby/object:Gem::Version
70
+ version: '3.0'
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: '3.0'
78
+ - - ">"
79
+ - !ruby/object:Gem::Version
80
+ version: '3.0'
61
81
  description:
62
82
  email:
63
83
  executables: []
@@ -84,6 +104,7 @@ files:
84
104
  - lib/ims/lti/tool_config.rb
85
105
  - lib/ims/lti/tool_consumer.rb
86
106
  - lib/ims/lti/tool_provider.rb
107
+ - lib/ims/lti/version.rb
87
108
  homepage: http://github.com/instructure/ims-lti
88
109
  licenses:
89
110
  - MIT
@@ -103,8 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
103
124
  - !ruby/object:Gem::Version
104
125
  version: '0'
105
126
  requirements: []
106
- rubyforge_project:
107
- rubygems_version: 2.5.1
127
+ rubygems_version: 3.1.6
108
128
  signing_key:
109
129
  specification_version: 4
110
130
  summary: Ruby library for creating IMS LTI tool providers and consumers