mil-ims-lti 1.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Changelog +30 -0
- data/LICENSE +18 -0
- data/README.md +112 -0
- data/ims-lti.gemspec +35 -0
- data/lib/ims.rb +1 -0
- data/lib/ims/lti.rb +50 -0
- data/lib/ims/lti/extensions.rb +43 -0
- data/lib/ims/lti/extensions/outcome_data.rb +156 -0
- data/lib/ims/lti/launch_params.rb +159 -0
- data/lib/ims/lti/outcome_request.rb +209 -0
- data/lib/ims/lti/outcome_response.rb +170 -0
- data/lib/ims/lti/request_validator.rb +50 -0
- data/lib/ims/lti/tool_config.rb +225 -0
- data/lib/ims/lti/tool_consumer.rb +95 -0
- data/lib/ims/lti/tool_provider.rb +197 -0
- metadata +128 -0
@@ -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
|