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:
parent
c1957aac1a
commit
b9b5409d39
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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!
|
||||||
|
|
Loading…
Reference in New Issue