add readResult support to LTI grade passback, refs #5892

testplan: you can set up an assignment as an external_tool type
manually, and then hit the IMS certification test tool to verify that
replaceResult and readResult are now fully supported.

Change-Id: Id193ba1943f51b3cb4b6a2d078d8a2262c26659e
Reviewed-on: https://gerrit.instructure.com/6678
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Brian Whitmer <brian@instructure.com>
This commit is contained in:
Brian Palmer 2011-11-03 13:51:18 -06:00
parent c1957aac1a
commit b9b5409d39
4 changed files with 103 additions and 19 deletions

View File

@ -132,7 +132,7 @@ class SubmissionsApiController < ApplicationController
def show def show
@assignment = @context.assignments.active.find(params[:assignment_id]) @assignment = @context.assignments.active.find(params[:assignment_id])
@user = get_user_considering_section(params[:id]) @user = get_user_considering_section(params[:id])
@submission = @assignment.submissions.find_or_initialize_by_user_id(@user.id) or raise ActiveRecord::RecordNotFound @submission = @assignment.submission_for_student(@user)
if authorized_action(@submission, @current_user, :read) if authorized_action(@submission, @current_user, :read)
includes = Array(params[:include]) includes = Array(params[:include])
render :json => submission_json(@submission, @assignment, includes).to_json render :json => submission_json(@submission, @assignment, includes).to_json

View File

@ -484,11 +484,15 @@ class Assignment < ActiveRecord::Base
self.grading_type ||= "points" self.grading_type ||= "points"
end end
def score_to_grade_percent(score=0.0)
result = score.to_f / self.points_possible
result = (result * 1000.0).round / 10.0
end
def score_to_grade(score=0.0) def score_to_grade(score=0.0)
result = score.to_f result = score.to_f
if self.grading_type == "percent" if self.grading_type == "percent"
result = score.to_f / self.points_possible result = score_to_grade_percent(score)
result = (result * 1000.0).round / 10.0
result = "#{result}%" result = "#{result}%"
elsif self.grading_type == "pass_fail" elsif self.grading_type == "pass_fail"
result = score.to_f == self.points_possible ? "complete" : "incomplete" result = score.to_f == self.points_possible ? "complete" : "incomplete"
@ -789,6 +793,10 @@ class Assignment < ActiveRecord::Base
end end
end end
def submission_for_student(user)
self.submissions.find_or_initialize_by_user_id(user.id)
end
def grade_student(original_student, opts={}) def grade_student(original_student, opts={})
raise "Student is required" unless original_student raise "Student is required" unless original_student
raise "Student must be enrolled in the course as a student to be graded" unless original_student && self.context.students.include?(original_student) raise "Student must be enrolled in the course as a student to be graded" unless original_student && self.context.students.include?(original_student)

View File

@ -19,7 +19,6 @@ module BasicLTI::BasicOutcomes
protected protected
def self.handle_request(tool, course, assignment, user, xml, res) def self.handle_request(tool, course, assignment, user, xml, res)
# verify the lis_result_sourcedid param, which will be a canvas-signed # verify the lis_result_sourcedid param, which will be a canvas-signed
# tuple of (course, assignment, user) to ensure that only this launch of # tuple of (course, assignment, user) to ensure that only this launch of
# the tool is attempting to modify this data. # the tool is attempting to modify this data.
@ -28,8 +27,15 @@ module BasicLTI::BasicOutcomes
return false return false
end end
case res.operation_ref_identifier op = res.operation_ref_identifier
when 'replaceResult' if self.respond_to?("handle_#{op}")
return self.send("handle_#{op}", tool, course, assignment, user, xml, res)
end
false
end
def self.handle_replaceResult(tool, course, assignment, user, xml, res)
text_value = xml.at_css('imsx_POXBody > replaceResultRequest > resultRecord > result > resultScore > textString').try(:content) text_value = xml.at_css('imsx_POXBody > replaceResultRequest > resultRecord > result > resultScore > textString').try(:content)
new_value = Float(text_value) rescue false new_value = Float(text_value) rescue false
if new_value && (0.0 .. 1.0).include?(new_value) if new_value && (0.0 .. 1.0).include?(new_value)
@ -43,7 +49,22 @@ module BasicLTI::BasicOutcomes
end end
end end
false def self.handle_readResult(tool, course, assignment, user, xml, res)
submission = assignment.submission_for_student(user)
if submission.graded?
raw_score = assignment.score_to_grade_percent(submission.score)
score = raw_score / 100.0
end
res.body = %{
<readResultResponse>
<result>
<resultScore>
<language>en</language>
<textString>#{score}</textString>
</resultScore>
</result>
</readResultResponse>
}
end end
class LtiResponse class LtiResponse

View File

@ -89,6 +89,30 @@ describe LtiApiController, :type => :integration do
} }
end end
def read_result(sourceid = nil)
sourceid ||= BasicLTI::BasicOutcomes.result_source_id(@tool, @course, @assignment, @student)
body = %{
<?xml version = "1.0" encoding = "UTF-8"?>
<imsx_POXEnvelopeRequest xmlns = "http://www.imsglobal.org/lis/oms1p0/pox">
<imsx_POXHeader>
<imsx_POXRequestHeaderInfo>
<imsx_version>V1.0</imsx_version>
<imsx_messageIdentifier>999999123</imsx_messageIdentifier>
</imsx_POXRequestHeaderInfo>
</imsx_POXHeader>
<imsx_POXBody>
<readResultRequest>
<resultRecord>
<sourcedGUID>
<sourcedId>#{sourceid}</sourcedId>
</sourcedGUID>
</resultRecord>
</readResultRequest>
</imsx_POXBody>
</imsx_POXEnvelopeRequest>
}
end
def check_failure(failure_type = 'unsupported') def check_failure(failure_type = 'unsupported')
response.should be_success response.should be_success
response.content_type.should == 'application/xml' response.content_type.should == 'application/xml'
@ -115,6 +139,7 @@ describe LtiApiController, :type => :integration do
xml.at_css('imsx_POXBody *:first').name.should == 'replaceResultResponse' xml.at_css('imsx_POXBody *:first').name.should == 'replaceResultResponse'
submission = @assignment.submissions.find_by_user_id(@student.id) submission = @assignment.submissions.find_by_user_id(@student.id)
submission.should be_present submission.should be_present
submission.should be_graded
submission.score.should == 12 submission.score.should == 12
end end
@ -145,6 +170,36 @@ describe LtiApiController, :type => :integration do
end end
end end
describe "readResult" do
it "should return an empty string when no grade exists" do
make_call('body' => read_result)
check_success
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 == 'readResult'
xml.at_css('imsx_POXBody *:first').name.should == 'readResultResponse'
xml.at_css('imsx_POXBody > readResultResponse > result > resultScore > language').content.should == 'en'
xml.at_css('imsx_POXBody > readResultResponse > result > resultScore > textString').content.should == ''
end
it "should return the score if the assignment is scored" do
@assignment.grade_student(@student, :grade => "40%")
make_call('body' => read_result)
check_success
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 == 'readResult'
xml.at_css('imsx_POXBody *:first').name.should == 'readResultResponse'
xml.at_css('imsx_POXBody > readResultResponse > result > resultScore > language').content.should == 'en'
xml.at_css('imsx_POXBody > readResultResponse > result > resultScore > textString').content.should == '0.4'
end
end
it "should reject if the assignment doesn't use this tool" do it "should reject if the assignment doesn't use this tool" do
tool = @course.context_external_tools.create!(:shared_secret => 'test_secret_2', :consumer_key => 'test_key_2', :name => 'new tool', :domain => 'example.net') tool = @course.context_external_tools.create!(:shared_secret => 'test_secret_2', :consumer_key => 'test_key_2', :name => 'new tool', :domain => 'example.net')
@assignment.external_tool_tag.destroy! @assignment.external_tool_tag.destroy!