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:
parent
57297715b3
commit
bf14f3b9ee
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue