mil-ims-lti 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,159 @@
1
+ module IMS::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,209 @@
1
+ module IMS::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 IMS::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)
79
+ @operation = REPLACE_REQUEST
80
+ @score = score
81
+ post_outcome_request
82
+ end
83
+
84
+ # POSTs a deleteResult to the Tool Consumer
85
+ #
86
+ # @return [OutcomeResponse] The response from the Tool Consumer
87
+ def post_delete_result!
88
+ @operation = DELETE_REQUEST
89
+ post_outcome_request
90
+ end
91
+
92
+ # POSTs a readResult to the Tool Consumer
93
+ #
94
+ # @return [OutcomeResponse] The response from the Tool Consumer
95
+ def post_read_result!
96
+ @operation = READ_REQUEST
97
+ post_outcome_request
98
+ end
99
+
100
+ # Check whether this request is a replaceResult request
101
+ def replace_request?
102
+ @operation == REPLACE_REQUEST
103
+ end
104
+
105
+ # Check whether this request is a deleteResult request
106
+ def delete_request?
107
+ @operation == DELETE_REQUEST
108
+ end
109
+
110
+ # Check whether this request is a readResult request
111
+ def read_request?
112
+ @operation == READ_REQUEST
113
+ end
114
+
115
+ # Check whether the last outcome POST was successful
116
+ def outcome_post_successful?
117
+ @outcome_response && @outcome_response.success?
118
+ end
119
+
120
+ # POST an OAuth signed request to the Tool Consumer
121
+ #
122
+ # @return [OutcomeResponse] The response from the Tool Consumer
123
+ def post_outcome_request
124
+ raise IMS::LTI::InvalidLTIConfigError, "" unless has_required_attributes?
125
+
126
+ consumer = OAuth::Consumer.new(@consumer_key, @consumer_secret)
127
+ token = OAuth::AccessToken.new(consumer)
128
+ res = token.post(
129
+ @lis_outcome_service_url,
130
+ generate_request_xml,
131
+ 'Content-Type' => 'application/xml'
132
+ )
133
+ @outcome_response = extend_outcome_response(OutcomeResponse.new)
134
+ @outcome_response.process_post_response(res)
135
+ end
136
+
137
+ # Parse Outcome Request data from XML
138
+ def process_xml(xml)
139
+ doc = REXML::Document.new xml
140
+ @message_identifier = doc.text("//imsx_POXRequestHeaderInfo/imsx_messageIdentifier")
141
+ @lis_result_sourcedid = doc.text("//resultRecord/sourcedGUID/sourcedId")
142
+
143
+ if REXML::XPath.first(doc, "//deleteResultRequest")
144
+ @operation = DELETE_REQUEST
145
+ elsif REXML::XPath.first(doc, "//readResultRequest")
146
+ @operation = READ_REQUEST
147
+ elsif REXML::XPath.first(doc, "//replaceResultRequest")
148
+ @operation = REPLACE_REQUEST
149
+ @score = doc.get_text("//resultRecord/result/resultScore/textString")
150
+ end
151
+ extention_process_xml(doc)
152
+ end
153
+
154
+ private
155
+
156
+ def extention_process_xml(doc)
157
+ end
158
+
159
+ def has_result_data?
160
+ !!@score
161
+ end
162
+
163
+ def results(node)
164
+ return unless has_result_data?
165
+
166
+ node.result do |res|
167
+ result_values(res)
168
+ end
169
+ end
170
+
171
+ def result_values(node)
172
+ if @score
173
+ node.resultScore do |res_score|
174
+ res_score.language "en" # 'en' represents the format of the number
175
+ res_score.textString @score.to_s
176
+ end
177
+ end
178
+ end
179
+
180
+ def has_required_attributes?
181
+ @consumer_key && @consumer_secret && @lis_outcome_service_url && @lis_result_sourcedid && @operation
182
+ end
183
+
184
+ def generate_request_xml
185
+ builder = Builder::XmlMarkup.new #(:indent=>2)
186
+ builder.instruct!
187
+
188
+ builder.imsx_POXEnvelopeRequest("xmlns" => "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0") do |env|
189
+ env.imsx_POXHeader do |header|
190
+ header.imsx_POXRequestHeaderInfo do |info|
191
+ info.imsx_version "V1.0"
192
+ info.imsx_messageIdentifier @message_identifier || IMS::LTI::generate_identifier
193
+ end
194
+ end
195
+ env.imsx_POXBody do |body|
196
+ body.tag!(@operation + 'Request') do |request|
197
+ request.resultRecord do |record|
198
+ record.sourcedGUID do |guid|
199
+ guid.sourcedId @lis_result_sourcedid
200
+ end
201
+ results(record)
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ end
209
+ end
@@ -0,0 +1,170 @@
1
+ module IMS::LTI
2
+ # Class for consuming/generating LTI Outcome Responses
3
+ #
4
+ # Response documentation: http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649691
5
+ #
6
+ # Error code documentation: http://www.imsglobal.org/gws/gwsv1p0/imsgws_baseProfv1p0.html#1639667
7
+ #
8
+ # This class can be used by both Tool Providers and Tool Consumers. Each will
9
+ # use it a bit differently. The Tool Provider will use it parse the result of
10
+ # an OutcomeRequest to the Tool Consumer. A Tool Consumer will use it generate
11
+ # proper response XML to send back to a Tool Provider
12
+ #
13
+ # === Tool Provider Usage
14
+ # An OutcomeResponse will generally be created when POSTing an OutcomeRequest
15
+ # through a configured ToolProvider. See the ToolProvider documentation for
16
+ # typical usage.
17
+ #
18
+ # === Tool Consumer Usage
19
+ # When an outcome request is sent from a Tool Provider the body of the request
20
+ # is XML. This class parses that XML and provides a simple interface for
21
+ # accessing the information in the request. Typical usage would be:
22
+ #
23
+ # # create a new response and set the appropriate values
24
+ # res = IMS::LTI::OutcomeResponse.new
25
+ # res.message_ref_identifier = outcome_request.message_identifier
26
+ # res.operation = outcome_request.operation
27
+ # res.code_major = 'success'
28
+ # res.severity = 'status'
29
+ #
30
+ # # set a description (optional) and other information based on the type of response
31
+ # if outcome_request.replace_request?
32
+ # res.description = "Your old score of 0 has been replaced with #{outcome_request.score}"
33
+ # elsif outcome_request.read_request?
34
+ # res.description = "You score is 50"
35
+ # res.score = 50
36
+ # elsif outcome_request.delete_request?
37
+ # res.description = "You score has been cleared"
38
+ # else
39
+ # res.code_major = 'unsupported'
40
+ # res.severity = 'status'
41
+ # res.description = "#{outcome_request.operation} is not supported"
42
+ # end
43
+ #
44
+ # # the generated xml is returned to the Tool Provider
45
+ # res.generate_response_xml
46
+ #
47
+ class OutcomeResponse
48
+ include IMS::LTI::Extensions::Base
49
+
50
+ attr_accessor :request_type, :score, :message_identifier, :response_code,
51
+ :post_response, :code_major, :severity, :description, :operation,
52
+ :message_ref_identifier
53
+
54
+ CODE_MAJOR_CODES = %w{success processing failure unsupported}
55
+ SEVERITY_CODES = %w{status warning error}
56
+
57
+ # Create a new OutcomeResponse
58
+ #
59
+ # @param opts [Hash] initialization hash
60
+ def initialize(opts={})
61
+ opts.each_pair do |key, val|
62
+ self.send("#{key}=", val) if self.respond_to?("#{key}=")
63
+ end
64
+ end
65
+
66
+ # Convenience method for creating a new OutcomeResponse from a response object
67
+ #
68
+ # req = IMS::LTI::OutcomeResponse.from_post_response(response)
69
+ def self.from_post_response(post_response)
70
+ response = OutcomeResponse.new
71
+ response.process_post_response(post_response)
72
+ end
73
+
74
+ def process_post_response(post_response)
75
+ self.post_response = post_response
76
+ self.response_code = post_response.code
77
+ xml = post_response.body
78
+ self.process_xml(xml)
79
+ self
80
+ end
81
+
82
+ def success?
83
+ @code_major == 'success'
84
+ end
85
+
86
+ def processing?
87
+ @code_major == 'processing'
88
+ end
89
+
90
+ def failure?
91
+ @code_major == 'failure'
92
+ end
93
+
94
+ def unsupported?
95
+ @code_major == 'unsupported'
96
+ end
97
+
98
+ def has_warning?
99
+ @severity == 'warning'
100
+ end
101
+
102
+ def has_error?
103
+ @severity == 'error'
104
+ end
105
+
106
+ # Parse Outcome Response data from XML
107
+ def process_xml(xml)
108
+ begin
109
+ doc = REXML::Document.new xml
110
+ @message_identifier = doc.text("//imsx_statusInfo/imsx_messageIdentifier").to_s
111
+ @code_major = doc.text("//imsx_statusInfo/imsx_codeMajor")
112
+ @code_major.downcase! if @code_major
113
+ @severity = doc.text("//imsx_statusInfo/imsx_severity")
114
+ @severity.downcase! if @severity
115
+ @description = doc.text("//imsx_statusInfo/imsx_description")
116
+ @description = @description.to_s if @description
117
+ @message_ref_identifier = doc.text("//imsx_statusInfo/imsx_messageRefIdentifier")
118
+ @operation = doc.text("//imsx_statusInfo/imsx_operationRefIdentifier")
119
+ @score = doc.text("//readResultResponse//resultScore/textString")
120
+ rescue REXML::ParseException => e
121
+ @message_identifier = ''
122
+ @code_major = 'failure'
123
+ @severity = 'status'
124
+ @description = "#{e}"
125
+ @message_ref_identifier = '123456789'
126
+ @operation = 'replaceResult'
127
+ @score = ''
128
+ end
129
+ @score = @score.to_s if @score
130
+ end
131
+
132
+ # Generate XML based on the current configuration
133
+ # @return [String] The response xml
134
+ def generate_response_xml
135
+ builder = Builder::XmlMarkup.new
136
+ builder.instruct!
137
+
138
+ builder.imsx_POXEnvelopeResponse("xmlns" => "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0") do |env|
139
+ env.imsx_POXHeader do |header|
140
+ header.imsx_POXResponseHeaderInfo do |info|
141
+ info.imsx_version "V1.0"
142
+ info.imsx_messageIdentifier @message_identifier || IMS::LTI::generate_identifier
143
+ info.imsx_statusInfo do |status|
144
+ status.imsx_codeMajor @code_major
145
+ status.imsx_severity @severity
146
+ status.imsx_description @description
147
+ status.imsx_messageRefIdentifier @message_ref_identifier
148
+ status.imsx_operationRefIdentifier @operation
149
+ end
150
+ end
151
+ end #/header
152
+ env.imsx_POXBody do |body|
153
+ unless unsupported?
154
+ body.tag!(@operation + 'Response') do |request|
155
+ if @operation == OutcomeRequest::READ_REQUEST
156
+ request.result do |res|
157
+ res.resultScore do |res_score|
158
+ res_score.language "en" # 'en' represents the format of the number
159
+ res_score.textString @score.to_s
160
+ end
161
+ end #/result
162
+ end
163
+ end #/operationResponse
164
+ end
165
+ end #/body
166
+ end
167
+ end
168
+
169
+ end
170
+ end