add content passback extension to lti assignment launches

This creates an LTI extension to pass text or urls along
with the score when doing an LTI 1.1 outcome request.

Test Plan:
 * use a tool that supports this extension on an assignment
 * After doing the tool activity the submission should have the expected value

refs #mebipenny

Change-Id: I296df1e7c7d99af61724a904511f9bf63d5d2613
Reviewed-on: https://gerrit.instructure.com/12878
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
This commit is contained in:
Bracken Mosbacker 2012-08-13 09:21:04 -06:00
parent 57297715b3
commit bf14f3b9ee
5 changed files with 229 additions and 19 deletions

View File

@ -14,4 +14,94 @@ Tools can know that they have been launched in a graded context because
an additional parameter is sent across, <code>lis_outcome_service_url</code>,
as specified in the LTI 1.1 specification. Grades are passed back to Canvas
from the tool's servers using the
<a href="http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649691">outcomes component of LTI 1.1</a>.
<a href="http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649691">outcomes component of LTI 1.1</a>.
## Data Return Extension
Canvas sends an extension parameter for assignment launches that allows the tool
provider to pass back values as submission text in canvas.
The key is `ext_outcome_data_values_accepted` and the value is a comma separated list of
types of data accepted. The currently available data types are `url` and `text`.
So the added launch parameter will look like this:
`ext_outcome_data_values_accepted=url,text`
### Returning Data Values from Tool Provider
If the external tool wants to supply these values, it can augment the POX sent
with the grading value. <a href="http://www.imsglobal.org/LTI/v1p1/ltiIMGv1p1.html#_Toc319560473">LTI replaceResult POX</a>
Only one type of resultData should be sent, if multiple types are sent the tool
consumer behavior is undefined and is implementation-specific. Canvas will take
the text value and ignore the url value if both are sent.
####Text
Add a `resultData` node with a `text` node of plain text in the same encoding as
the rest of the document within it like this:
<pre>
<?xml version = "1.0" encoding = "UTF-8"?>
<imsx_POXEnvelopeRequest xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
<imsx_POXHeader>
<imsx_POXRequestHeaderInfo>
<imsx_version>V1.0</imsx_version>
<imsx_messageIdentifier>999999123</imsx_messageIdentifier>
</imsx_POXRequestHeaderInfo>
</imsx_POXHeader>
<imsx_POXBody>
<replaceResultRequest>
<resultRecord>
<sourcedGUID>
<sourcedId>3124567</sourcedId>
</sourcedGUID>
<result>
<resultScore>
<language>en</language>
<textString>0.92</textString>
</resultScore>
<!-- Added element -->
<resultData>
<text>text data for canvas submission</text>
</resultData>
</result>
</resultRecord>
</replaceResultRequest>
</imsx_POXBody>
</imsx_POXEnvelopeRequest>
</pre>
####URL
Add a `resultData` node with a `url` node within it like this:
<pre>
<?xml version = "1.0" encoding = "UTF-8"?>
<imsx_POXEnvelopeRequest xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
<imsx_POXHeader>
<imsx_POXRequestHeaderInfo>
<imsx_version>V1.0</imsx_version>
<imsx_messageIdentifier>999999123</imsx_messageIdentifier>
</imsx_POXRequestHeaderInfo>
</imsx_POXHeader>
<imsx_POXBody>
<replaceResultRequest>
<resultRecord>
<sourcedGUID>
<sourcedId>3124567</sourcedId>
</sourcedGUID>
<result>
<resultScore>
<language>en</language>
<textString>0.92</textString>
</resultScore>
<!-- Added element -->
<resultData>
<url>https://www.example.com/cool_lti_link_submission</url>
</resultData>
</result>
</resultRecord>
</replaceResultRequest>
</imsx_POXBody>
</imsx_POXEnvelopeRequest>
</pre>

View File

@ -69,6 +69,7 @@ module BasicLTI
hash['lis_result_sourcedid'] = BasicLTI::BasicOutcomes.encode_source_id(tool, context, assignment, user)
hash['lis_outcome_service_url'] = outcome_service_url
hash['ext_ims_lis_basic_outcome_url'] = legacy_outcome_service_url
hash['ext_outcome_data_values_accepted'] = ['url', 'text'].join(',')
if tool.public?
hash['custom_canvas_assignment_id'] = assignment.id
end

View File

@ -77,6 +77,14 @@ module BasicLTI::BasicOutcomes
@lti_request.at_css('imsx_POXBody > replaceResultRequest > resultRecord > result > resultScore > textString').try(:content)
end
def result_data_text
@lti_request && @lti_request.at_css('imsx_POXBody > replaceResultRequest > resultRecord > result > resultData > text').try(:content)
end
def result_data_url
@lti_request && @lti_request.at_css('imsx_POXBody > replaceResultRequest > resultRecord > result > resultData > url').try(:content)
end
def to_xml
xml = LtiResponse.envelope.dup
xml.at_css('imsx_POXHeader imsx_statusInfo imsx_codeMajor').content = code_major
@ -136,16 +144,40 @@ module BasicLTI::BasicOutcomes
def handle_replaceResult(tool, course, assignment, user)
text_value = self.result_score
new_value = Float(text_value) rescue false
if new_value && (0.0 .. 1.0).include?(new_value)
submission_hash = { :grade => "#{new_value * 100}%", :submission_type => 'external_tool' }
new_score = Float(text_value) rescue false
error_message = nil
submission_hash = { :submission_type => 'external_tool' }
if text = result_data_text
submission_hash[:body] = text
submission_hash[:submission_type] = 'online_text_entry'
elsif url = result_data_url
submission_hash[:url] = url
submission_hash[:submission_type] = 'online_url'
end
if new_score
if (0.0 .. 1.0).include?(new_score)
submission_hash[:grade] = "#{new_score * 100}%"
else
error_message = I18n.t('lib.basic_lti.bad_score', "Score is not between 0 and 1")
end
elsif !text && !url
error_message = I18n.t('lib.basic_lti.no_score', "No score given")
end
if error_message
self.code_major = 'failure'
self.description = error_message
else
if submission_hash[:submission_type] != 'external_tool'
assignment.submit_homework(user, submission_hash.clone)
end
@submission = assignment.grade_student(user, submission_hash).first
self.body = "<replaceResultResponse />"
return true
else
self.code_major = 'failure'
return true
end
true
end
def handle_deleteResult(tool, course, assignment, user)

View File

@ -59,8 +59,28 @@ describe LtiApiController, :type => :integration do
response.status.should == "401 Unauthorized"
end
def replace_result(score, sourceid = nil)
def replace_result(score=nil, sourceid = nil, result_data=nil)
sourceid ||= BasicLTI::BasicOutcomes.encode_source_id(@tool, @course, @assignment, @student)
score_xml = ''
if score
score_xml = <<-XML
<resultScore>
<language>en</language>
<textString>#{score}</textString>
</resultScore>
XML
end
result_data_xml = ''
if result_data && !result_data.empty?
result_data_xml = "<resultData>\n"
result_data.each_pair do |key, val|
result_data_xml += "<#{key}>#{val}</#{key}>"
end
result_data_xml += "\n</resultData>\n"
end
body = %{
<?xml version = "1.0" encoding = "UTF-8"?>
<imsx_POXEnvelopeRequest xmlns = "http://www.imsglobal.org/lis/oms1p0/pox">
@ -77,10 +97,8 @@ describe LtiApiController, :type => :integration do
<sourcedId>#{sourceid}</sourcedId>
</sourcedGUID>
<result>
<resultScore>
<language>en</language>
<textString>#{score}</textString>
</resultScore>
#{score_xml}
#{result_data_xml}
</result>
</resultRecord>
</replaceResultRequest>
@ -151,16 +169,22 @@ describe LtiApiController, :type => :integration do
end
describe "replaceResult" do
it "should allow updating the submission score" do
@assignment.submissions.find_by_user_id(@student.id).should be_nil
make_call('body' => replace_result('0.6'))
check_success
def verify_xml(response)
xml = Nokogiri::XML.parse(response.body)
xml.at_css('imsx_codeMajor').content.should == 'success'
xml.at_css('imsx_messageRefIdentifier').content.should == '999999123'
xml.at_css('imsx_operationRefIdentifier').content.should == 'replaceResult'
xml.at_css('imsx_POXBody *:first').name.should == 'replaceResultResponse'
end
it "should allow updating the submission score" do
@assignment.submissions.find_by_user_id(@student.id).should be_nil
make_call('body' => replace_result('0.6'))
check_success
verify_xml(response)
submission = @assignment.submissions.find_by_user_id(@student.id)
submission.should be_present
submission.should be_graded
@ -168,7 +192,69 @@ describe LtiApiController, :type => :integration do
submission.submission_type.should eql 'external_tool'
submission.score.should == 12
end
it "should set the submission data text" do
make_call('body' => replace_result('0.6', nil, {:text =>"oioi"}))
check_success
verify_xml(response)
submission = @assignment.submissions.find_by_user_id(@student.id)
submission.score.should == 12
submission.body.should == "oioi"
end
it "should set complex submission text" do
text = CGI::escapeHTML("<p>stuff</p>")
make_call('body' => replace_result('0.6', nil, {:text => "<![CDATA[#{text}]]>" }))
check_success
verify_xml(response)
submission = @assignment.submissions.find_by_user_id(@student.id)
submission.submission_type.should == 'online_text_entry'
submission.body.should == text
end
it "should set the submission data url" do
make_call('body' => replace_result('0.6', nil, {:url =>"http://www.example.com/lti"}))
check_success
verify_xml(response)
submission = @assignment.submissions.find_by_user_id(@student.id)
submission.submission_type.should == 'online_url'
submission.score.should == 12
submission.url.should == "http://www.example.com/lti"
end
it "should set the submission data text even with no score" do
make_call('body' => replace_result(nil, nil, {:text =>"oioi"}))
check_success
verify_xml(response)
submission = @assignment.submissions.find_by_user_id(@student.id)
submission.score.should == nil
submission.body.should == "oioi"
end
it "should fail if no score and not submission data" do
make_call('body' => replace_result(nil, nil))
response.should be_success
xml = Nokogiri::XML.parse(response.body)
xml.at_css('imsx_codeMajor').content.should == 'failure'
xml.at_css('imsx_description').content.should == "No score given"
@assignment.submissions.find_by_user_id(@student.id).should be_nil
end
it "should fail if bad score given" do
make_call('body' => replace_result('1.5', nil))
response.should be_success
xml = Nokogiri::XML.parse(response.body)
xml.at_css('imsx_codeMajor').content.should == 'failure'
xml.at_css('imsx_description').content.should == "Score is not between 0 and 1"
@assignment.submissions.find_by_user_id(@student.id).should be_nil
end
it "should reject out of bound scores" do
@assignment.submissions.find_by_user_id(@student.id).should be_nil
make_call('body' => replace_result('-1'))

View File

@ -259,6 +259,7 @@ describe BasicLTI do
hash['lis_result_sourcedid'].should == BasicLTI::BasicOutcomes.encode_source_id(@tool, @course, @assignment, @user)
hash['lis_outcome_service_url'].should == "/my/test/url"
hash['ext_ims_lis_basic_outcome_url'].should == "/my/other/test/url"
hash['ext_outcome_data_values_accepted'].should == 'url,text'
end
context "sharding" do