aj-ims-lti 1.1.1

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.
@@ -0,0 +1,185 @@
1
+ module AJIMS::LTI
2
+ module Extensions
3
+
4
+ # An LTI extension that adds support for sending data back to the consumer
5
+ # in addition to the score.
6
+ #
7
+ # # Initialize TP object with OAuth creds and post parameters
8
+ # provider = IMS::LTI::ToolProvider.new(consumer_key, consumer_secret, params)
9
+ # # add extension
10
+ # provider.extend IMS::LTI::Extensions::OutcomeData::ToolProvider
11
+ #
12
+ # If the tool was launch as an outcome service and it supports the data extension
13
+ # you can POST a score to the TC.
14
+ # The POST calls all return an OutcomeResponse object which can be used to
15
+ # handle the response appropriately.
16
+ #
17
+ # # post the score to the TC, score should be a float >= 0.0 and <= 1.0
18
+ # # this returns an OutcomeResponse object
19
+ # if provider.accepts_outcome_text?
20
+ # response = provider.post_replace_result_with_data!(score, "text" => "submission text")
21
+ # else
22
+ # response = provider.post_replace_result!(score)
23
+ # end
24
+ # if response.success?
25
+ # # grade write worked
26
+ # elsif response.processing?
27
+ # elsif response.unsupported?
28
+ # else
29
+ # # failed
30
+ # end
31
+ module OutcomeData
32
+
33
+ #IMS::LTI::Extensions::OutcomeData::ToolProvider
34
+ module Base
35
+ def outcome_request_extensions
36
+ super + [AJIMS::LTI::Extensions::OutcomeData::OutcomeRequest]
37
+ end
38
+ end
39
+
40
+ module ToolProvider
41
+ include AJIMS::LTI::Extensions::ExtensionBase
42
+ include Base
43
+
44
+ # a list of the supported outcome data types
45
+ def accepted_outcome_types
46
+ return @outcome_types if @outcome_types
47
+ @outcome_types = []
48
+ if val = @ext_params["outcome_data_values_accepted"]
49
+ @outcome_types = val.split(',')
50
+ end
51
+
52
+ @outcome_types
53
+ end
54
+
55
+ # check if the outcome data extension is supported
56
+ def accepts_outcome_data?
57
+ !!@ext_params["outcome_data_values_accepted"]
58
+ end
59
+
60
+ # check if the consumer accepts text as outcome data
61
+ def accepts_outcome_text?
62
+ accepted_outcome_types.member?("text")
63
+ end
64
+
65
+ # check if the consumer accepts a url as outcome data
66
+ def accepts_outcome_url?
67
+ accepted_outcome_types.member?("url")
68
+ end
69
+
70
+ # POSTs the given score to the Tool Consumer with a replaceResult and
71
+ # adds the specified data. The data hash can have the keys "text", "cdata_text", or "url"
72
+ #
73
+ # If both cdata_text and text are sent, cdata_text will be used
74
+ #
75
+ # If score is nil, the replace result XML will not contain a resultScore node
76
+ #
77
+ # Creates a new OutcomeRequest object and stores it in @outcome_requests
78
+ #
79
+ # @return [OutcomeResponse] the response from the Tool Consumer
80
+ def post_replace_result_with_data!(score = nil, data={})
81
+ req = new_request
82
+ if data["cdata_text"]
83
+ req.outcome_cdata_text = data["cdata_text"]
84
+ elsif data["text"]
85
+ req.outcome_text = data["text"]
86
+ end
87
+
88
+ if data["lti_launch_url"]
89
+ req.outcome_lti_launch_url = data["lti_launch_url"] if data["lti_launch_url"]
90
+ elsif data["download_url"] && data["document_name"]
91
+ req.outcome_download_url = data["download_url"]
92
+ req.outcome_document_name = data["document_name"]
93
+ else
94
+ req.outcome_url = data["url"] if data["url"]
95
+ end
96
+ req.post_replace_result!(score, submitted_at: data["submitted_at"])
97
+ end
98
+ end
99
+
100
+ module ToolConsumer
101
+ include AJIMS::LTI::Extensions::ExtensionBase
102
+ include Base
103
+
104
+ OUTCOME_DATA_TYPES = %w{text url}
105
+
106
+ # a list of the outcome data types accepted, currently only 'url' and
107
+ # 'text' are valid
108
+ #
109
+ # tc.outcome_data_values_accepted(['url', 'text'])
110
+ # tc.outcome_data_valued_accepted("url,text")
111
+ def outcome_data_values_accepted=(val)
112
+ if val.is_a? Array
113
+ val = val.join(',')
114
+ end
115
+
116
+ set_ext_param('outcome_data_values_accepted', val)
117
+ end
118
+
119
+ # a comma-separated string of the supported outcome data types
120
+ def outcome_data_values_accepted
121
+ get_ext_param('outcome_data_values_accepted')
122
+ end
123
+
124
+ # convenience method for setting support for all current outcome data types
125
+ def support_outcome_data!
126
+ self.outcome_data_values_accepted = OUTCOME_DATA_TYPES
127
+ end
128
+ end
129
+
130
+ module OutcomeRequest
131
+ include AJIMS::LTI::Extensions::ExtensionBase
132
+ include Base
133
+
134
+ attr_accessor(
135
+ :outcome_text,
136
+ :outcome_url,
137
+ :outcome_cdata_text,
138
+ :outcome_lti_launch_url,
139
+ :outcome_download_url,
140
+ :outcome_document_name,
141
+ )
142
+
143
+ def result_values(node)
144
+ super
145
+ if has_non_score_result_data?
146
+ node.resultData do |res_data|
147
+ if @outcome_cdata_text
148
+ res_data.text {
149
+ res_data.cdata! @outcome_cdata_text
150
+ }
151
+ elsif @outcome_text
152
+ res_data.text @outcome_text
153
+ end
154
+ res_data.url @outcome_url if @outcome_url
155
+ res_data.ltiLaunchUrl @outcome_lti_launch_url if @outcome_lti_launch_url
156
+ res_data.downloadUrl @outcome_download_url if @outcome_download_url
157
+ res_data.documentName @outcome_document_name if @outcome_document_name
158
+ end
159
+ end
160
+ end
161
+
162
+ def has_non_score_result_data?
163
+ !!@outcome_text || !!@outcome_cdata_text ||
164
+ !!@outcome_url ||
165
+ !!@outcome_lti_launch_url ||
166
+ (!!@outcome_download_url && !!@outcome_document_name)
167
+ end
168
+
169
+ def has_result_data?
170
+ has_non_score_result_data? || super
171
+ end
172
+
173
+ def extention_process_xml(doc)
174
+ super
175
+ @outcome_text = doc.get_text("//resultRecord/result/resultData/text")
176
+ @outcome_url = doc.get_text("//resultRecord/result/resultData/url")
177
+ @outcome_lti_launch_url = doc.get_text("//resultRecord/result/resultData/ltiLaunchUrl")
178
+ @outcome_download_url = doc.get_text("//resultRecord/result/resultData/downloadUrl")
179
+ @outcome_document_name = doc.get_text("//resultRecord/result/resultData/documentName")
180
+ end
181
+ end
182
+
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,159 @@
1
+ module AJIMS::LTI
2
+ # Mixin module for managing LTI Launch Data
3
+ #
4
+ # Launch data documentation:
5
+ # http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649684
6
+ module LaunchParams
7
+
8
+ # List of the standard launch parameters for an LTI launch
9
+ LAUNCH_DATA_PARAMETERS = %w{
10
+ context_id
11
+ context_label
12
+ context_title
13
+ context_type
14
+ launch_presentation_css_url
15
+ launch_presentation_document_target
16
+ launch_presentation_height
17
+ launch_presentation_locale
18
+ launch_presentation_return_url
19
+ launch_presentation_width
20
+ lis_course_offering_sourcedid
21
+ lis_course_section_sourcedid
22
+ lis_outcome_service_url
23
+ lis_person_contact_email_primary
24
+ lis_person_name_family
25
+ lis_person_name_full
26
+ lis_person_name_given
27
+ lis_person_sourcedid
28
+ lis_result_sourcedid
29
+ lti_message_type
30
+ lti_version
31
+ oauth_callback
32
+ oauth_consumer_key
33
+ oauth_nonce
34
+ oauth_signature
35
+ oauth_signature_method
36
+ oauth_timestamp
37
+ oauth_version
38
+ resource_link_description
39
+ resource_link_id
40
+ resource_link_title
41
+ roles
42
+ tool_consumer_info_product_family_code
43
+ tool_consumer_info_version
44
+ tool_consumer_instance_contact_email
45
+ tool_consumer_instance_description
46
+ tool_consumer_instance_guid
47
+ tool_consumer_instance_name
48
+ tool_consumer_instance_url
49
+ user_id
50
+ user_image
51
+ }
52
+
53
+ LAUNCH_DATA_PARAMETERS.each { |p| attr_accessor p }
54
+
55
+ # Hash of custom parameters, the keys will be prepended with "custom_" at launch
56
+ attr_accessor :custom_params
57
+
58
+ # Hash of extension parameters, the keys will be prepended with "ext_" at launch
59
+ attr_accessor :ext_params
60
+
61
+ # Hash of parameters to add to the launch. These keys will not be prepended
62
+ # with any value at launch
63
+ attr_accessor :non_spec_params
64
+
65
+ # Set the roles for the current launch
66
+ #
67
+ # Full list of roles can be found here:
68
+ # http://www.imsglobal.org/LTI/v1p1pd/ltiIMGv1p1pd.html#_Toc309649700
69
+ #
70
+ # LIS roles include:
71
+ # * Student
72
+ # * Faculty
73
+ # * Member
74
+ # * Learner
75
+ # * Instructor
76
+ # * Mentor
77
+ # * Staff
78
+ # * Alumni
79
+ # * ProspectiveStudent
80
+ # * Guest
81
+ # * Other
82
+ # * Administrator
83
+ # * Observer
84
+ # * None
85
+ #
86
+ # @param roles_list [String,Array] An Array or comma-separated String of roles
87
+ def roles=(roles_list)
88
+ if roles_list
89
+ if roles_list.is_a?(Array)
90
+ @roles = roles_list
91
+ else
92
+ @roles = roles_list.split(",").map(&:downcase)
93
+ end
94
+ else
95
+ @roles = nil
96
+ end
97
+ end
98
+
99
+ def set_custom_param(key, val)
100
+ @custom_params[key] = val
101
+ end
102
+
103
+ def get_custom_param(key)
104
+ @custom_params[key]
105
+ end
106
+
107
+ def set_non_spec_param(key, val)
108
+ @non_spec_params[key] = val
109
+ end
110
+
111
+ def get_non_spec_param(key)
112
+ @non_spec_params[key]
113
+ end
114
+
115
+ def set_ext_param(key, val)
116
+ @ext_params[key] = val
117
+ end
118
+
119
+ def get_ext_param(key)
120
+ @ext_params[key]
121
+ end
122
+
123
+ # Create a new Hash with all launch data. Custom/Extension keys will have the
124
+ # appropriate value prepended to the keys and the roles are set as a comma
125
+ # separated String
126
+ def to_params
127
+ params = launch_data_hash.merge(add_key_prefix(@custom_params, 'custom')).merge(add_key_prefix(@ext_params, 'ext')).merge(@non_spec_params)
128
+ params["roles"] = @roles.join(",") if @roles
129
+ params
130
+ end
131
+
132
+ # Populates the launch data from a Hash
133
+ #
134
+ # Only keys in LAUNCH_DATA_PARAMETERS and that start with 'custom_' or 'ext_'
135
+ # will be pulled from the provided Hash
136
+ def process_params(params)
137
+ params.each_pair do |key, val|
138
+ if LAUNCH_DATA_PARAMETERS.member?(key)
139
+ self.send("#{key}=", val)
140
+ elsif key =~ /custom_(.*)/
141
+ @custom_params[$1] = val
142
+ elsif key =~ /ext_(.*)/
143
+ @ext_params[$1] = val
144
+ end
145
+ end
146
+ end
147
+
148
+ private
149
+
150
+ def launch_data_hash
151
+ LAUNCH_DATA_PARAMETERS.inject({}) { |h, k| h[k] = self.send(k) if self.send(k); h }
152
+ end
153
+
154
+ def add_key_prefix(hash, prefix)
155
+ hash.keys.inject({}) { |h, k| h["#{prefix}_#{k}"] = hash[k]; h }
156
+ end
157
+
158
+ end
159
+ end
@@ -0,0 +1,227 @@
1
+ module AJIMS::LTI
2
+ # Class for consuming/generating LTI Outcome Requests
3
+ #
4
+ # Outcome Request documentation: http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649691
5
+ #
6
+ # This class can be used by both Tool Providers and Tool Consumers. Each will
7
+ # use it a bit differently. The Tool Provider will use it to POST an OAuth-signed
8
+ # request to a TC. A Tool Consumer will use it to parse such a request from a TP.
9
+ #
10
+ # === Tool Provider Usage
11
+ # An OutcomeRequest will generally be created through a configured ToolProvider
12
+ # object. See the ToolProvider documentation.
13
+ #
14
+ # === Tool Consumer Usage
15
+ # When an outcome request is sent from a TP the body of the request is XML.
16
+ # This class parses that XML and provides a simple interface for accessing the
17
+ # information in the request. Typical usage would be:
18
+ #
19
+ # # create an OutcomeRequest from the request object
20
+ # req = IMS::LTI::OutcomeRequest.from_post_request(request)
21
+ #
22
+ # # access the source id to identify the user who's grade you'd like to access
23
+ # req.lis_result_sourcedid
24
+ #
25
+ # # process the request
26
+ # if req.replace_request?
27
+ # # set a new score for the user
28
+ # elsif req.read_request?
29
+ # # return the score for the user
30
+ # elsif req.delete_request?
31
+ # # clear the score for the user
32
+ # else
33
+ # # return an unsupported OutcomeResponse
34
+ # end
35
+ class OutcomeRequest
36
+ include AJIMS::LTI::Extensions::Base
37
+
38
+ REPLACE_REQUEST = 'replaceResult'
39
+ DELETE_REQUEST = 'deleteResult'
40
+ READ_REQUEST = 'readResult'
41
+
42
+ attr_accessor :operation, :score, :outcome_response, :message_identifier,
43
+ :lis_outcome_service_url, :lis_result_sourcedid,
44
+ :consumer_key, :consumer_secret, :post_request
45
+
46
+ # Create a new OutcomeRequest
47
+ #
48
+ # @param opts [Hash] initialization hash
49
+ def initialize(opts={})
50
+ opts.each_pair do |key, val|
51
+ self.send("#{key}=", val) if self.respond_to?("#{key}=")
52
+ end
53
+ end
54
+
55
+ # Convenience method for creating a new OutcomeRequest from a request object
56
+ #
57
+ # req = IMS::LTI::OutcomeRequest.from_post_request(request)
58
+ def self.from_post_request(post_request)
59
+ request = OutcomeRequest.new
60
+ request.process_post_request(post_request)
61
+ end
62
+
63
+ def process_post_request(post_request)
64
+ self.post_request = post_request
65
+ if post_request.body.respond_to?(:read)
66
+ xml = post_request.body.read
67
+ post_request.body.rewind
68
+ else
69
+ xml = post_request.body
70
+ end
71
+ self.process_xml(xml)
72
+ self
73
+ end
74
+
75
+ # POSTs the given score to the Tool Consumer with a replaceResult
76
+ #
77
+ # @return [OutcomeResponse] The response from the Tool Consumer
78
+ def post_replace_result!(score, submitted_at: nil)
79
+ @operation = REPLACE_REQUEST
80
+ @score = score
81
+ @submitted_at = submitted_at
82
+
83
+ post_outcome_request
84
+ end
85
+
86
+ # POSTs a deleteResult to the Tool Consumer
87
+ #
88
+ # @return [OutcomeResponse] The response from the Tool Consumer
89
+ def post_delete_result!
90
+ @operation = DELETE_REQUEST
91
+ post_outcome_request
92
+ end
93
+
94
+ # POSTs a readResult to the Tool Consumer
95
+ #
96
+ # @return [OutcomeResponse] The response from the Tool Consumer
97
+ def post_read_result!
98
+ @operation = READ_REQUEST
99
+ post_outcome_request
100
+ end
101
+
102
+ # Check whether this request is a replaceResult request
103
+ def replace_request?
104
+ @operation == REPLACE_REQUEST
105
+ end
106
+
107
+ # Check whether this request is a deleteResult request
108
+ def delete_request?
109
+ @operation == DELETE_REQUEST
110
+ end
111
+
112
+ # Check whether this request is a readResult request
113
+ def read_request?
114
+ @operation == READ_REQUEST
115
+ end
116
+
117
+ # Check whether the last outcome POST was successful
118
+ def outcome_post_successful?
119
+ @outcome_response && @outcome_response.success?
120
+ end
121
+
122
+ # POST an OAuth signed request to the Tool Consumer
123
+ #
124
+ # @return [OutcomeResponse] The response from the Tool Consumer
125
+ def post_outcome_request
126
+ raise AJIMS::LTI::InvalidLTIConfigError, "" unless has_required_attributes?
127
+
128
+ consumer = OAuth::Consumer.new(@consumer_key, @consumer_secret)
129
+ token = OAuth::AccessToken.new(consumer)
130
+
131
+ res = token.post(
132
+ @lis_outcome_service_url,
133
+ generate_request_xml,
134
+ 'Content-Type' => 'application/xml'
135
+ )
136
+
137
+ @outcome_response = extend_outcome_response(OutcomeResponse.new)
138
+ @outcome_response.process_post_response(res)
139
+ end
140
+
141
+ # Parse Outcome Request data from XML
142
+ def process_xml(xml)
143
+ doc = REXML::Document.new xml
144
+ @message_identifier = doc.text("//imsx_POXRequestHeaderInfo/imsx_messageIdentifier")
145
+ @lis_result_sourcedid = doc.text("//resultRecord/sourcedGUID/sourcedId")
146
+
147
+ if REXML::XPath.first(doc, "//deleteResultRequest")
148
+ @operation = DELETE_REQUEST
149
+ elsif REXML::XPath.first(doc, "//readResultRequest")
150
+ @operation = READ_REQUEST
151
+ elsif REXML::XPath.first(doc, "//replaceResultRequest")
152
+ @operation = REPLACE_REQUEST
153
+ @score = doc.get_text("//resultRecord/result/resultScore/textString")
154
+ end
155
+ extention_process_xml(doc)
156
+ end
157
+
158
+ private
159
+
160
+ def extention_process_xml(doc)
161
+ end
162
+
163
+ def has_result_time?
164
+ !@submitted_at.nil?
165
+ end
166
+
167
+ def submission_details(node)
168
+ return unless has_result_time?
169
+
170
+ node.submittedAt @submitted_at.to_s
171
+ end
172
+
173
+ def has_result_data?
174
+ !!@score
175
+ end
176
+
177
+ def results(node)
178
+ return unless has_result_data?
179
+
180
+ node.result do |res|
181
+ result_values(res)
182
+ end
183
+ end
184
+
185
+ def result_values(node)
186
+ if @score
187
+ node.resultScore do |res_score|
188
+ res_score.language "en" # 'en' represents the format of the number
189
+ res_score.textString @score.to_s
190
+ end
191
+ end
192
+ end
193
+
194
+ def has_required_attributes?
195
+ @consumer_key && @consumer_secret && @lis_outcome_service_url && @lis_result_sourcedid && @operation
196
+ end
197
+
198
+ def generate_request_xml
199
+ builder = Builder::XmlMarkup.new #(:indent=>2)
200
+ builder.instruct!
201
+
202
+ builder.imsx_POXEnvelopeRequest("xmlns" => "http://www.imsglobal.org/lis/oms1p0/pox") do |env|
203
+ env.imsx_POXHeader do |header|
204
+ header.imsx_POXRequestHeaderInfo do |info|
205
+ info.imsx_version "V1.0"
206
+ info.imsx_messageIdentifier @message_identifier || AJIMS::LTI::generate_identifier
207
+ end
208
+ end
209
+ env.imsx_POXBody do |body|
210
+ body.tag!(@operation + 'Request') do |request|
211
+ request.submissionDetails do |details|
212
+ submission_details(details)
213
+ end
214
+
215
+ request.resultRecord do |record|
216
+ record.sourcedGUID do |guid|
217
+ guid.sourcedId @lis_result_sourcedid
218
+ end
219
+ results(record)
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
225
+
226
+ end
227
+ end