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
|
||||
@assignment = @context.assignments.active.find(params[:assignment_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)
|
||||
includes = Array(params[:include])
|
||||
render :json => submission_json(@submission, @assignment, includes).to_json
|
||||
|
|
|
@ -483,12 +483,16 @@ class Assignment < ActiveRecord::Base
|
|||
def infer_grading_type
|
||||
self.grading_type ||= "points"
|
||||
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)
|
||||
result = score.to_f
|
||||
if self.grading_type == "percent"
|
||||
result = score.to_f / self.points_possible
|
||||
result = (result * 1000.0).round / 10.0
|
||||
result = score_to_grade_percent(score)
|
||||
result = "#{result}%"
|
||||
elsif self.grading_type == "pass_fail"
|
||||
result = score.to_f == self.points_possible ? "complete" : "incomplete"
|
||||
|
@ -788,7 +792,11 @@ class Assignment < ActiveRecord::Base
|
|||
self.context_module_action(user, action, points)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def submission_for_student(user)
|
||||
self.submissions.find_or_initialize_by_user_id(user.id)
|
||||
end
|
||||
|
||||
def grade_student(original_student, opts={})
|
||||
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)
|
||||
|
|
|
@ -19,7 +19,6 @@ module BasicLTI::BasicOutcomes
|
|||
protected
|
||||
|
||||
def self.handle_request(tool, course, assignment, user, xml, res)
|
||||
|
||||
# verify the lis_result_sourcedid param, which will be a canvas-signed
|
||||
# tuple of (course, assignment, user) to ensure that only this launch of
|
||||
# the tool is attempting to modify this data.
|
||||
|
@ -28,24 +27,46 @@ module BasicLTI::BasicOutcomes
|
|||
return false
|
||||
end
|
||||
|
||||
case res.operation_ref_identifier
|
||||
when 'replaceResult'
|
||||
text_value = xml.at_css('imsx_POXBody > replaceResultRequest > resultRecord > result > resultScore > textString').try(:content)
|
||||
new_value = Float(text_value) rescue false
|
||||
if new_value && (0.0 .. 1.0).include?(new_value)
|
||||
submission_hash = { :grade => "#{new_value * 100}%" }
|
||||
submission = assignment.grade_student(user, submission_hash).first
|
||||
res.body = "<replaceResultResponse />"
|
||||
return true
|
||||
else
|
||||
res.code_major = 'failure'
|
||||
return true
|
||||
end
|
||||
op = res.operation_ref_identifier
|
||||
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)
|
||||
new_value = Float(text_value) rescue false
|
||||
if new_value && (0.0 .. 1.0).include?(new_value)
|
||||
submission_hash = { :grade => "#{new_value * 100}%" }
|
||||
submission = assignment.grade_student(user, submission_hash).first
|
||||
res.body = "<replaceResultResponse />"
|
||||
return true
|
||||
else
|
||||
res.code_major = 'failure'
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
class LtiResponse
|
||||
attr_accessor :code_major, :severity, :description, :body
|
||||
|
||||
|
|
|
@ -89,6 +89,30 @@ describe LtiApiController, :type => :integration do
|
|||
}
|
||||
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')
|
||||
response.should be_success
|
||||
response.content_type.should == 'application/xml'
|
||||
|
@ -115,6 +139,7 @@ describe LtiApiController, :type => :integration do
|
|||
xml.at_css('imsx_POXBody *:first').name.should == 'replaceResultResponse'
|
||||
submission = @assignment.submissions.find_by_user_id(@student.id)
|
||||
submission.should be_present
|
||||
submission.should be_graded
|
||||
submission.score.should == 12
|
||||
end
|
||||
|
||||
|
@ -145,6 +170,36 @@ describe LtiApiController, :type => :integration do
|
|||
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
|
||||
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!
|
||||
|
|
Loading…
Reference in New Issue