aj-ims-lti 1.1.1

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