Fix moderated assignments with graded rubrics

Closes: GRADE-2414

Test plan:
- Given a course with a Teacher, on TA and a student
- Given a moderated assignment with one Grader
- Given the teacher is the final grader
- Given a rubric attached to the assignment with the option "use for
  grading" enabled
- Given the TA has graded the student with the rubric
- Given the TA has also given a *different* score in the normal grade
  box than what the rubric score was
- Given on the moderation page the Teacher selects the TA's score.
- When clicking the "Release Grades" button
- Then submitting grades works and does not error

Change-Id: Ia2d06047ec492bd79a17a1fd086861e016ccdf9d
Reviewed-on: https://gerrit.instructure.com/208051
Tested-by: Jenkins
Reviewed-by: Jeremy Neander <jneander@instructure.com>
Reviewed-by: Adrian Packel <apackel@instructure.com>
QA-Review: Adrian Packel <apackel@instructure.com>
Product-Review: Jonathan Fenton <jfenton@instructure.com>
This commit is contained in:
Derek Bender 2019-09-03 21:02:57 -05:00
parent f285948546
commit 8cf3d457d6
8 changed files with 418 additions and 226 deletions

View File

@ -1823,6 +1823,7 @@ class Assignment < ActiveRecord::Base
submission.workflow_state = "graded"
end
submission.group = group
submission.grade_posting_in_progress = opts.fetch(:grade_posting_in_progress, false)
previously_graded ? submission.with_versioning(:explicit => true) { submission.save! } : submission.save!
submission.audit_grade_changes = false

View File

@ -182,30 +182,32 @@ class ModeratedGrading::ProvisionalGrade < ActiveRecord::Base
end
def publish_rubric_assessments!
copy_rubric_assessments!(submission)
self.rubric_assessments.each do |provisional_assessment|
rubric_association = provisional_assessment.rubric_association
params = {
artifact: submission,
assessment_type: provisional_assessment.assessment_type
}
unless rubric_association.assessments_unique_per_asset?(provisional_assessment.assessment_type)
params = params.merge({assessor_id: provisional_assessment.assessor})
end
def copy_rubric_assessments!(dest_artifact)
self.rubric_assessments.each do |prov_assmt|
assoc = prov_assmt.rubric_association
rubric_assessment = rubric_association.rubric_assessments.find_by(params)
rubric_assessment ||= rubric_association.rubric_assessments.build(
params.merge(
assessor: provisional_assessment.assessor,
user: self.student,
rubric: rubric_association.rubric,
)
)
pub_assmt = nil
# see RubricAssociation#assess
if dest_artifact.is_a?(Submission)
if assoc.assessments_unique_per_asset?(prov_assmt.assessment_type)
pub_assmt = assoc.rubric_assessments.where(artifact_id: dest_artifact.id, artifact_type: dest_artifact.class_name,
assessment_type: prov_assmt.assessment_type).first
else
pub_assmt = assoc.rubric_assessments.where(artifact_id: dest_artifact.id, artifact_type: dest_artifact.class_name,
assessment_type: prov_assmt.assessment_type, assessor_id: prov_assmt.assessor).first
end
end
pub_assmt ||= assoc.rubric_assessments.build(:assessor => prov_assmt.assessor, :artifact => dest_artifact,
:user => self.student, :rubric => assoc.rubric, :assessment_type => prov_assmt.assessment_type)
pub_assmt.score = prov_assmt.score
pub_assmt.data = prov_assmt.data
rubric_assessment.score = provisional_assessment.score
rubric_assessment.data = provisional_assessment.data
rubric_assessment.submission.grade_posting_in_progress = submission.grade_posting_in_progress
pub_assmt.save!
rubric_assessment.save!
end
end

View File

@ -157,14 +157,20 @@ class RubricAssessment < ActiveRecord::Base
end
def update_artifact
return unless artifact.present? && rubric_association&.use_for_grading? && artifact.score != score
return if artifact.blank? || !rubric_association&.use_for_grading? || artifact.score == score
case artifact_type
when "Submission"
assignment = rubric_association.association_object
return unless assignment.grants_right?(assessor, :grade)
assignment.grade_student(artifact.student, score: score, grader: assessor, graded_anonymously: @graded_anonymously_set)
assignment.grade_student(
artifact.student,
score: score,
grader: assessor,
graded_anonymously: @graded_anonymously_set,
grade_posting_in_progress: artifact.grade_posting_in_progress
)
artifact.reload
when "ModeratedGrading::ProvisionalGrade"
artifact.update!(score: score)

View File

@ -74,7 +74,7 @@ module Factories
title: 'new outcome',
description: '<p>This is <b>awesome</b>.</p>',
calculation_method: 'highest')
[opts[:outcome_context], @course].compact.uniq.each do |context|
[opts[:outcome_context], course].compact.uniq.each do |context|
root = context.root_outcome_group
root.add_outcome(@outcome)
root.save!
@ -123,8 +123,7 @@ module Factories
}
}
@rubric = @course.rubrics.build
@rubric = course.rubrics.build
@rubric.update_criteria(rubric_params)
@rubric.reload
end
end

View File

@ -1594,88 +1594,122 @@ describe Assignment do
end
describe '#grade_student' do
before(:once) { setup_assignment_without_submission }
context 'with a submission that cannot be graded' do
before :each do
allow_any_instance_of(Submission).to receive(:grader_can_grade?).and_return(false)
let_once(:now) { Time.zone.now }
let_once(:student) { User.create!.tap {|u| course.enroll_student(u, enrollment_state: 'active') } }
let_once(:teacher) { User.create!.tap {|u| course.enroll_teacher(u, enrollment_state: 'active') } }
let_once(:assignment) { course.assignments.create! }
let_once(:course) do
course = Account.default.courses.create!
course.offer!
course
end
it 'raises a GradeError when Submission#grader_can_grade? returns false' do
describe 'grade_posting_in_progress' do
let(:submission) { instance_double("Submission") }
before do
allow(assignment).to receive(:find_or_create_submissions).
with([student], Submission.preload(:grading_period, :stream_item)).
and_yield([submission])
end
it 'sets grade_posting_in_progress to false when absent' do
expect(assignment).to receive(:save_grade_to_submission).
with([submission], student, nil, grade: 10, grader: teacher)
assignment.grade_student(student, grade: 10, grader: teacher)
end
it 'sets grade_posting_in_progress to true when present' do
expect(assignment).to receive(:save_grade_to_submission).
with([submission], student, nil, grade: 10, grader: teacher, grade_posting_in_progress: true)
assignment.grade_student(student, grade: 10, grader: teacher, grade_posting_in_progress: true)
end
it 'sets grade_posting_in_progress to false when present' do
expect(assignment).to receive(:save_grade_to_submission).
with([submission], student, nil, grade: 10, grader: teacher, grade_posting_in_progress: false)
assignment.grade_student(student, grade: 10, grader: teacher, grade_posting_in_progress: false)
end
end
it 'raises a GradeError when grader does not have permission' do
expect {
@assignment.grade_student(@user, grade: 42, grader: @teacher)
assignment.grade_student(student, grade: 42, grader: student)
}.to raise_error(Assignment::GradeError)
end
end
context 'with a submission that has an existing grade' do
it 'applies the late penalty' do
Timecop.freeze do
@assignment.update(points_possible: 100, due_at: 1.5.days.ago, submission_types: %w[online_text_entry])
late_policy_factory(course: @course, deduct: 15.0, every: :day, missing: 80.0)
@assignment.submit_homework(@user, submission_type: 'online_text_entry', body: 'foo')
@assignment.grade_student(@user, grade: "100", grader: @teacher)
@assignment.reload
expect(@assignment.submission_for_student(@user).grade).to eql('70')
@assignment.grade_student(@user, grade: '70', grader: @teacher)
expect(@assignment.submission_for_student(@user).grade).to eql('40')
end
around(:once) do |block|
Timecop.freeze(now) do
block.call
end
end
context 'with a valid student' do
before :once do
@result = @assignment.grade_student(@user, grade: "10", grader: @teacher)
@assignment.reload
before(:once) do
assignment.update!(points_possible: 100, due_at: 36.hours.ago(now), submission_types: %w[online_text_entry])
late_policy_factory(course: course, deduct: 15.0, every: :day, missing: 80.0)
assignment.submit_homework(student, submission_type: :online_text_entry, body: :foo)
end
it 'returns an array' do
expect(@result).to be_is_a(Array)
it 'applies the late penalty to a full credit grade' do
submission, * = assignment.grade_student(student, grade: "100", grader: teacher)
expect(submission.grade).to eql('70')
end
it 'applies the late penalty to a grade less than full credit' do
submission, * = assignment.grade_student(student, grade: '70', grader: teacher)
expect(submission.grade).to eql('40')
end
end
context 'with a submission' do
subject_once { submissions }
let_once(:submissions) { assignment.grade_student(student, grade: "10", grader: teacher) }
it { is_expected.to be_an Array }
it 'now has a submission' do
expect(@assignment.submissions.size).to eql(1)
assignment.reload
expect(assignment.submissions.count).to be 1
end
describe 'the submission after grading' do
subject { @assignment.submissions.first }
subject_once(:submission) { submissions.first }
describe '#state' do
subject { super().state }
it { is_expected.to eql(:graded) }
it { expect(submission.state).to be :graded }
end
it { is_expected.to eq @result[0] }
describe '#score' do
subject { super().score }
it { is_expected.to eq 10.0 }
it { expect(submission.score).to eq 10.0 }
end
describe '#user_id' do
subject { super().user_id }
it { is_expected.to eq @user.id }
it { expect(submission.user_id).to eq student.id }
end
it 'has a version length of one' do
expect(submission.versions.length).to eq 1
end
specify { expect(subject.versions.length).to eq 1 }
end
end
context 'with no student' do
it 'raises an error' do
expect { @assignment.grade_student(nil) }.to raise_error(Assignment::GradeError, 'Student is required')
expect { assignment.grade_student(nil) }.to raise_error(Assignment::GradeError, 'Student is required')
end
end
context 'with a student that does not belong' do
it 'raises an error' do
expect { @assignment.grade_student(User.new) }.to raise_error(Assignment::GradeError, 'Student must be enrolled in the course as a student to be graded')
expect { assignment.grade_student(User.new) }.to raise_error(Assignment::GradeError, 'Student must be enrolled in the course as a student to be graded')
end
end
context 'with an invalid initial grade' do
before :once do
@result = @assignment.grade_student(@user, grade: "{", grader: @teacher)
@assignment.reload
@result = assignment.grade_student(student, grade: "{", grader: teacher)
assignment.reload
end
it 'does not change the workflow_state to graded' do
@ -1686,10 +1720,9 @@ describe Assignment do
context "moderated assignment" do
let_once(:assignment) do
@course.assignments.create!(moderated_grading: true, grader_count: 1, final_grader: @teacher)
course.assignments.create!(moderated_grading: true, grader_count: 1, final_grader: teacher)
end
let_once(:student) { course_with_user("StudentEnrollment", course: @course, active_all: true, name: "Stu").user }
let_once(:ta) { course_with_user("TaEnrollment", course: @course, active_all: true, name: "Ta").user }
let_once(:ta) { course_with_user("TaEnrollment", course: course, active_all: true, name: "Ta").user }
let(:pg) { @result.first.provisional_grades.find_by!(scorer: ta) }
before(:each) do
@ -1740,8 +1773,8 @@ describe Assignment do
context 'with an excused assignment' do
before :once do
@result = @assignment.grade_student(@user, grader: @teacher, excuse: true)
@assignment.reload
@result = assignment.grade_student(student, grader: teacher, excuse: true)
assignment.reload
end
it 'excuses the assignment and marks it as graded' do
@ -1753,18 +1786,18 @@ describe Assignment do
context 'with anonymous grading' do
it 'explicitly sets anonymous grading if given' do
@assignment.grade_student(@user, graded_anonymously: true, grade: "10", grader: @teacher)
@assignment.reload
expect(@assignment.submissions.first.graded_anonymously).to be_truthy
assignment.grade_student(student, graded_anonymously: true, grade: "10", grader: teacher)
assignment.reload
expect(assignment.submissions.first.graded_anonymously).to be_truthy
end
it 'does not set anonymous grading if not given' do
@assignment.grade_student(@user, graded_anonymously: true, grade: "10", grader: @teacher)
@assignment.reload
@assignment.grade_student(@user, grade: "10", grader: @teacher)
@assignment.reload
assignment.grade_student(student, graded_anonymously: true, grade: "10", grader: teacher)
assignment.reload
assignment.grade_student(student, grade: "10", grader: teacher)
assignment.reload
# should still true because grade didn't actually change
expect(@assignment.submissions.first.graded_anonymously).to be_truthy
expect(assignment.submissions.first.graded_anonymously).to be_truthy
end
end
@ -1887,7 +1920,6 @@ describe Assignment do
end
describe 'AnonymousOrModerationEvent creation on grading a submission' do
let_once(:course) { Course.create! }
let_once(:assignment) do
course.assignments.create!(
anonymous_grading: true,
@ -1896,10 +1928,6 @@ describe Assignment do
)
end
let_once(:student) { course.enroll_student(User.create!, active_all: true).user }
let_once(:submission) { assignment.submission_for_student(student) }
let_once(:teacher) { course.enroll_teacher(User.create!, enrollment_state: 'active').user }
let(:last_event) do
AnonymousOrModerationEvent.where(assignment: assignment, event_type: 'submission_updated').last
end
@ -1929,7 +1957,7 @@ describe Assignment do
end
it 'includes the affected submission on the event' do
assignment.grade_student(student, grader: teacher, score: 75)
submission, * = assignment.grade_student(student, grader: teacher, score: 75)
expect(last_event.submission_id).to eq submission.id
end
@ -1990,12 +2018,14 @@ describe Assignment do
it "creates a submission_changed event when unexcusing the assignment" do
moderated_assignment.grade_student(student, grader: teacher, provisional: true, excuse: true)
moderated_assignment.grade_student(student, grader: teacher, provisional: true, score: 40)
expect(last_event.payload['excused']).to eq [true, false]
end
it "does not capture score changes in the submission_changed event" do
moderated_assignment.grade_student(student, grader: teacher, provisional: true, excuse: true)
moderated_assignment.grade_student(student, grader: teacher, provisional: true, score: 40)
expect(last_event.payload).not_to include('score')
end
end
@ -2011,22 +2041,14 @@ describe Assignment do
end
describe "submission posting" do
let(:course) { Course.create! }
let(:student) { course.enroll_student(User.create!, enrollment_state: 'active').user }
let(:teacher) { course.enroll_teacher(User.create!, enrollment_state: 'active').user }
let(:assignment) { course.assignments.create!(title: "hi") }
let(:submission) { assignment.submission_for_student(student) }
context "when the submission is unposted" do
it "posts the submission if a grade is assigned" do
assignment.grade_student(student, grader: teacher, score: 50)
submission, * = assignment.grade_student(student, grader: teacher, score: 50)
expect(submission).to be_posted
end
it "posts the submission if an excusal is granted" do
assignment.grade_student(student, grader: teacher, excused: true)
submission, * = assignment.grade_student(student, grader: teacher, excused: true)
expect(submission).to be_posted
end
@ -2035,13 +2057,13 @@ describe Assignment do
PostPolicy.enable_feature!
assignment.post_policy.update!(post_manually: true)
assignment.grade_student(student, grader: teacher, score: 50)
submission, * = assignment.grade_student(student, grader: teacher, score: 50)
expect(submission).not_to be_posted
end
it "does not post the submission for a muted assignment when post policies are not enabled" do
assignment.mute!
assignment.grade_student(student, grader: teacher, score: 50)
submission, * = assignment.grade_student(student, grader: teacher, score: 50)
expect(submission).not_to be_posted
end
@ -2052,51 +2074,49 @@ describe Assignment do
final_grader: teacher,
grader_count: 2
)
moderated_assignment.grade_student(student, grader: teacher, provisional: true, score: 40)
expect(moderated_assignment.submission_for_student(student)).not_to be_posted
submission, * = moderated_assignment.grade_student(student, grader: teacher, provisional: true, score: 40)
expect(submission).not_to be_posted
end
end
it "does not update the posted_at date for a previously-posted submission" do
submission = assignment.submissions.find_by!(user: student)
submission.update!(posted_at: 1.day.ago)
expect {
assignment.grade_student(student, grader: teacher, score: 50)
}.not_to change {
assignment.submission_for_student(student).posted_at
submission.reload.posted_at
}
end
end
describe "grade change audit records" do
let(:student) { @course.enroll_student(User.create!, enrollment_state: :active).user }
let(:teacher) { @course.enroll_teacher(User.create!, enrollment_state: :active).user }
context "when post policies are enabled" do
before(:once) do
PostPolicy.enable_feature!
@course.enable_feature!(:new_gradebook)
course.enable_feature!(:new_gradebook)
end
context "when assignment posts manually" do
before(:each) do
@assignment.ensure_post_policy(post_manually: true)
assignment.ensure_post_policy(post_manually: true)
end
it "inserts a record" do
expect(Auditors::GradeChange::Stream).to receive(:insert).once
@assignment.grade_student(student, grade: 10, grader: teacher)
assignment.grade_student(student, grade: 10, grader: teacher)
end
end
context "when assignment posts automatically" do
before(:each) do
@assignment.ensure_post_policy(post_manually: false)
assignment.ensure_post_policy(post_manually: false)
end
it "inserts a record" do
expect(Auditors::GradeChange::Stream).to receive(:insert).once
@assignment.grade_student(student, grade: 10, grader: teacher)
assignment.grade_student(student, grade: 10, grader: teacher)
end
end
end
@ -2108,57 +2128,57 @@ describe Assignment do
context "when assignment is muted" do
before(:each) do
@assignment.mute!
assignment.mute!
end
it "inserts a record" do
expect(Auditors::GradeChange::Stream).to receive(:insert).once
@assignment.grade_student(student, grade: 10, grader: teacher)
assignment.grade_student(student, grade: 10, grader: teacher)
end
end
context "when assignment is unmuted" do
before(:each) do
@assignment.unmute!
assignment.unmute!
end
it "inserts a record" do
expect(Auditors::GradeChange::Stream).to receive(:insert).once
@assignment.grade_student(student, grade: 10, grader: teacher)
assignment.grade_student(student, grade: 10, grader: teacher)
end
end
end
end
describe "grade change live events" do
let(:student) { @course.enroll_student(User.create!, enrollment_state: :active).user }
let(:teacher) { @course.enroll_teacher(User.create!, enrollment_state: :active).user }
let(:student) { course.enroll_student(User.create!, enrollment_state: :active).user }
let(:teacher) { course.enroll_teacher(User.create!, enrollment_state: :active).user }
context "when post policies are enabled" do
before(:once) do
PostPolicy.enable_feature!
@course.enable_feature!(:new_gradebook)
course.enable_feature!(:new_gradebook)
end
context "when assignment posts manually" do
before(:each) do
@assignment.ensure_post_policy(post_manually: true)
assignment.ensure_post_policy(post_manually: true)
end
it "emits an event" do
expect(Canvas::LiveEvents).to receive(:grade_changed).once
@assignment.grade_student(student, grade: 10, grader: teacher)
assignment.grade_student(student, grade: 10, grader: teacher)
end
end
context "when assignment posts automatically" do
before(:each) do
@assignment.ensure_post_policy(post_manually: false)
assignment.ensure_post_policy(post_manually: false)
end
it "emits two events when grading: one for grading and one for posting" do
expect(Canvas::LiveEvents).to receive(:grade_changed).twice
@assignment.grade_student(student, grade: 10, grader: teacher)
assignment.grade_student(student, grade: 10, grader: teacher)
end
end
end
@ -2170,23 +2190,23 @@ describe Assignment do
context "when assignment is muted" do
before(:each) do
@assignment.mute!
assignment.mute!
end
it "emits an event" do
expect(Canvas::LiveEvents).to receive(:grade_changed).once
@assignment.grade_student(student, grade: 10, grader: teacher)
assignment.grade_student(student, grade: 10, grader: teacher)
end
end
context "when assignment is unmuted" do
before(:each) do
@assignment.unmute!
assignment.unmute!
end
it "emits an event" do
expect(Canvas::LiveEvents).to receive(:grade_changed).once
@assignment.grade_student(student, grade: 10, grader: teacher)
assignment.grade_student(student, grade: 10, grader: teacher)
end
end
end

View File

@ -270,63 +270,66 @@ describe ModeratedGrading::ProvisionalGrade do
describe "grade_matches_current_submission" do
it "returns true if the grade is newer than the submission" do
sub = nil
submission = nil
Timecop.freeze(10.minutes.ago) do
sub = assignment.submit_homework(student, :submission_type => 'online_text_entry', :body => 'hallo')
submission = assignment.submit_homework(student, submission_type: 'online_text_entry', body: 'hallo')
end
pg = sub.find_or_create_provisional_grade!(scorer, score: 1)
expect(pg.reload.grade_matches_current_submission).to eq true
provisional_grade = submission.find_or_create_provisional_grade!(scorer, score: 1)
expect(provisional_grade.reload.grade_matches_current_submission).to eq true
end
it "returns false if the submission is newer than the grade" do
sub = nil
pg = nil
submission = nil
provisional_grade = nil
Timecop.freeze(10.minutes.ago) do
sub = assignment.submit_homework(student, :submission_type => 'online_text_entry', :body => 'hallo')
pg = sub.find_or_create_provisional_grade!(scorer, score: 1)
submission = assignment.submit_homework(student, submission_type: 'online_text_entry', body: 'hallo')
provisional_grade = submission.find_or_create_provisional_grade!(scorer, score: 1)
end
assignment.submit_homework(student, :submission_type => 'online_text_entry', :body => 'resubmit')
expect(pg.reload.grade_matches_current_submission).to eq false
assignment.submit_homework(student, submission_type: 'online_text_entry', body: 'resubmit')
expect(provisional_grade.reload.grade_matches_current_submission).to eq false
end
end
describe 'unique constraint' do
it "disallows multiple provisional grades from the same user" do
pg1 = submission.provisional_grades.build(score: 75)
pg1.scorer = scorer
pg1.save!
pg2 = submission.provisional_grades.build(score: 80)
pg2.scorer = scorer
expect { pg2.save! }.to raise_error(ActiveRecord::RecordNotUnique)
first_provisional_grade = submission.provisional_grades.build(score: 75)
first_provisional_grade.scorer = scorer
first_provisional_grade.save!
second_provisional_grade = submission.provisional_grades.build(score: 80)
second_provisional_grade.scorer = scorer
expect { second_provisional_grade.save! }.to raise_error(ActiveRecord::RecordNotUnique)
end
it "disallows multiple final provisional grades" do
pg1 = submission.provisional_grades.build(score: 75, final: false)
pg1.scorer = scorer
pg1.save!
pg2 = submission.provisional_grades.build(score: 75, final: true)
pg2.scorer = scorer
pg2.save!
pg3 = submission.provisional_grades.build(score: 80, final: true)
pg3.scorer = User.create!
expect { pg3.save! }.to raise_error(ActiveRecord::RecordNotUnique)
first_provisional_grade = submission.provisional_grades.build(score: 75, final: false)
first_provisional_grade.scorer = scorer
first_provisional_grade.save!
second_provisional_grade = submission.provisional_grades.build(score: 75, final: true)
second_provisional_grade.scorer = scorer
second_provisional_grade.save!
third_provisional_grade = submission.provisional_grades.build(score: 80, final: true)
third_provisional_grade.scorer = User.create!
expect { third_provisional_grade.save! }.to raise_error(ActiveRecord::RecordNotUnique)
end
end
describe '#graded_at when a grade changes' do
it { expect(provisional_grade.graded_at).to be_nil }
it 'updates the graded_at timestamp when changing grade' do
Timecop.freeze(@now) do
provisional_grade.update_attributes(grade: 'B')
expect(provisional_grade.graded_at).to eql @now
end
end
it 'updates the graded_at timestamp when changing score' do
Timecop.freeze(@now) do
provisional_grade.update_attributes(score: 80)
expect(provisional_grade.graded_at).to eql @now
end
end
it 'updated graded_at when force_save is set, regardless of whether the grade actually changed' do
Timecop.freeze(@now) do
provisional_grade.force_save = true
@ -338,102 +341,133 @@ describe ModeratedGrading::ProvisionalGrade do
describe 'infer_grade' do
it 'infers a grade if only score is given' do
pg = submission.find_or_create_provisional_grade!(scorer, score: 0)
expect(pg.grade).not_to be_nil
provisional_grade = submission.find_or_create_provisional_grade!(scorer, score: 0)
expect(provisional_grade.grade).not_to be_nil
end
it 'leaves grade nil if score is nil' do
pg = submission.find_or_create_provisional_grade! scorer
expect(pg.grade).to be_nil
provisional_grade = submission.find_or_create_provisional_grade! scorer
expect(provisional_grade.grade).to be_nil
end
end
describe "publish_rubric_assessments!" do
it "publishes rubric assessments to the submission" do
@course = course
outcome_with_rubric
association = @rubric.associate_with(assignment, course, :purpose => 'grading', :use_for_grading => true)
outcome_with_rubric(course: course)
association = @rubric.associate_with(assignment, course, purpose: 'grading', use_for_grading: true)
sub = assignment.submit_homework(student, :submission_type => 'online_text_entry', :body => 'hallo')
pg = sub.find_or_create_provisional_grade!(scorer, score: 1)
submission = assignment.submit_homework(student, submission_type: 'online_text_entry', body: 'hallo')
provisional_grade = submission.find_or_create_provisional_grade!(scorer, score: 1)
prov_assmt = association.assess(:user => student, :assessor => scorer, :artifact => pg,
:assessment => { :assessment_type => 'grading',
:"criterion_#{@rubric.criteria_object.first.id}" => { :points => 3, :comments => "good 4 u" } })
provisional_assessment = association.assess(
user: student,
assessor: scorer,
artifact: provisional_grade,
assessment: {
assessment_type: 'grading',
:"criterion_#{@rubric.criteria_object.first.id}" => {
points: 3,
comments: "good 4 u"
}
}
)
expect(provisional_assessment.score).to eq 3
expect(prov_assmt.score).to eq 3
pg.publish!
real_assmt = sub.rubric_assessments.first
expect(real_assmt.score).to eq 3
expect(real_assmt.assessor).to eq scorer
expect(real_assmt.rubric_association).to eq association
expect(real_assmt.data).to eq prov_assmt.data
provisional_grade.publish!
real_assessment = submission.rubric_assessments.first
expect(real_assessment.score).to eq 3
expect(real_assessment.assessor).to eq scorer
expect(real_assessment.rubric_association).to eq association
expect(real_assessment.data).to eq provisional_assessment.data
end
it "posts learning outcome results" do
@course = course
outcome_with_rubric
association = @rubric.associate_with(assignment, course, :purpose => 'grading', :use_for_grading => true)
outcome_with_rubric(course: course)
association = @rubric.associate_with(assignment, course, purpose: 'grading', use_for_grading: true)
sub = assignment.submit_homework(student, :submission_type => 'online_text_entry', :body => 'hallo')
pg = sub.find_or_create_provisional_grade!(scorer, score: 1)
submission = assignment.submit_homework(student, submission_type: 'online_text_entry', body: 'hallo')
provisional_grade = submission.find_or_create_provisional_grade!(scorer, score: 1)
expect do
association.assess(:user => student, :assessor => scorer, :artifact => pg,
:assessment => { :assessment_type => 'grading',
:"criterion_#{@rubric.criteria_object.first.id}" => { :points => 3, :comments => "good 4 u" } })
association.assess(
user: student,
assessor: scorer,
artifact: provisional_grade,
assessment: {
assessment_type: 'grading',
:"criterion_#{@rubric.criteria_object.first.id}" => {
points: 3,
comments: "good 4 u"
}
}
)
end.to change { LearningOutcomeResult.count }.by(0)
expect { provisional_grade.publish!}.to change { LearningOutcomeResult.count }.by(1)
end
expect do
pg.publish!
end.to change { LearningOutcomeResult.count }.by(1)
it "sets grade_posting_in_progress on the rubric_assessment's submission" do
outcome_with_rubric(course: course)
association = @rubric.associate_with(assignment, course, purpose: 'grading', use_for_grading: true)
submission = assignment.submit_homework(student, submission_type: 'online_text_entry', body: 'hallo')
provisional_grade = submission.find_or_create_provisional_grade!(scorer, score: 1)
provisional_grade.submission.grade_posting_in_progress = true
association.assess(
user: student,
assessor: scorer,
artifact: provisional_grade,
assessment: {
assessment_type: :grading,
:"criterion_#{@rubric.criteria_object.first.id}" => {
points: 3,
comments: "good 4 u"
}
}
)
provisional_grade.publish!
expect(submission.reload.score).to eq 3
end
end
describe "publish!" do
it "sets the submission as 'graded'" do
assignment.update!(moderated_grading: true, grader_count: 2)
sub = submission_model(assignment: assignment, user: student)
provisional_grade = sub.find_or_create_provisional_grade!(scorer, score: 80, graded_anonymously: true)
submission = submission_model(assignment: assignment, user: student)
provisional_grade = submission.find_or_create_provisional_grade!(scorer, score: 80, graded_anonymously: true)
provisional_grade.publish!
sub.reload
submission.reload
expect(sub.workflow_state).to eq 'graded'
expect(submission.workflow_state).to eq 'graded'
end
it "updates the submission with provisional grade attributes" do
@course = course
sub = assignment.submit_homework(student, :submission_type => 'online_text_entry', :body => 'hallo')
pg = sub.find_or_create_provisional_grade!(scorer, score: 80, graded_anonymously: true)
sub.reload
submission = assignment.submit_homework(student, submission_type: 'online_text_entry', body: 'hallo')
provisional_grade = submission.find_or_create_provisional_grade!(scorer, score: 80, graded_anonymously: true)
submission.reload
expect(pg).to receive(:publish_rubric_assessments!).once
pg.publish!
sub.reload
expect(provisional_grade).to receive(:publish_rubric_assessments!).once
provisional_grade.publish!
submission.reload
expect(sub.grade_matches_current_submission).to eq true
expect(sub.graded_at).not_to be_nil
expect(sub.grader_id).to eq scorer.id
expect(sub.score).to eq 80
expect(sub.grade).not_to be_nil
expect(sub.graded_anonymously).to eq true
expect(submission.grade_matches_current_submission).to eq true
expect(submission.graded_at).not_to be_nil
expect(submission.grader_id).to eq scorer.id
expect(submission.score).to eq 80
expect(submission.grade).not_to be_nil
expect(submission.graded_anonymously).to eq true
end
it "duplicates submission comments from the provisional grade to the submission" do
@course = course
sub = assignment.submit_homework(student, :submission_type => 'online_text_entry', :body => 'hallo')
pg = sub.find_or_create_provisional_grade!(scorer, score: 1)
provisional_comment = sub.add_comment(commenter: scorer, comment: 'blah', provisional: true)
submission = assignment.submit_homework(student, submission_type: 'online_text_entry', body: 'hallo')
provisional_grade = submission.find_or_create_provisional_grade!(scorer, score: 1)
provisional_comment = submission.add_comment(commenter: scorer, comment: 'blah', provisional: true)
pg.publish!
sub.reload
provisional_grade.publish!
submission.reload
real_comment = sub.submission_comments.first
real_comment = submission.submission_comments.first
expect(real_comment.provisional_grade_id).to be_nil
expect(real_comment.author).to eq scorer
expect(real_comment.comment).to eq provisional_comment.comment
@ -441,44 +475,42 @@ describe ModeratedGrading::ProvisionalGrade do
end
it "shares attachments between duplicated submission comments" do
@course = course
sub = assignment.submit_homework(student, :submission_type => 'online_text_entry', :body => 'hallo')
pg = sub.find_or_create_provisional_grade!(scorer, score: 1)
submission = assignment.submit_homework(student, submission_type: 'online_text_entry', body: 'hallo')
provisional_grade = submission.find_or_create_provisional_grade!(scorer, score: 1)
file = assignment.attachments.create! uploaded_data: default_uploaded_data
provisional_comment = sub.add_comment(commenter: scorer, comment: 'blah', provisional: true, attachments: [file])
provisional_comment = submission.add_comment(commenter: scorer, comment: 'blah', provisional: true, attachments: [file])
pg.publish!
sub.reload
provisional_grade.publish!
submission.reload
real_comment = sub.submission_comments.first
real_comment = submission.submission_comments.first
expect(real_comment.attachments).to eq provisional_comment.attachments
end
it "does not duplicate submission comments not associated with the provisional grade" do
@course = course
sub = assignment.submit_homework(student, :submission_type => 'online_text_entry', :body => 'hallo')
pg = sub.find_or_create_provisional_grade!(scorer, score: 1)
sub.add_comment(commenter: scorer, comment: 'provisional', provisional: true)
sub.add_comment(commenter: scorer, comment: 'normal', provisional: false)
submission = assignment.submit_homework(student, submission_type: 'online_text_entry', body: 'hallo')
provisional_grade = submission.find_or_create_provisional_grade!(scorer, score: 1)
submission.add_comment(commenter: scorer, comment: 'provisional', provisional: true)
submission.add_comment(commenter: scorer, comment: 'normal', provisional: false)
pg.publish!
sub.reload
provisional_grade.publish!
submission.reload
expect(sub.submission_comments.map(&:comment)).to match_array(['provisional', 'normal'])
expect(submission.submission_comments.map(&:comment)).to match_array(['provisional', 'normal'])
end
it "triggers GradeCalculator#recompute_final_score by default" do
sub = assignment.submit_homework(student, submission_type: "online_text_entry", body: "hello")
pg = sub.find_or_create_provisional_grade!(scorer, score: 1)
submission = assignment.submit_homework(student, submission_type: "online_text_entry", body: "hello")
provisional_grade = submission.find_or_create_provisional_grade!(scorer, score: 1)
expect(GradeCalculator).to receive(:recompute_final_score).once
pg.publish!
provisional_grade.publish!
end
it "does not triggers GradeCalculator#recompute_final_score if passed skip_grade_calc true" do
sub = assignment.submit_homework(student, submission_type: "online_text_entry", body: "hello")
pg = sub.find_or_create_provisional_grade!(scorer, score: 1)
submission = assignment.submit_homework(student, submission_type: "online_text_entry", body: "hello")
provisional_grade = submission.find_or_create_provisional_grade!(scorer, score: 1)
expect(GradeCalculator).not_to receive(:recompute_final_score)
pg.publish!(skip_grade_calc: true)
provisional_grade.publish!(skip_grade_calc: true)
end
it 'does not create a duplicate submission comment created event when a provisional grade is published' do

View File

@ -495,6 +495,53 @@ describe RubricAssessment do
end
describe '#update_artifact' do
describe 'grade_posting_in_progress' do
subject_once(:ra) do
RubricAssessment.new(
score: 2.0,
assessment_type: :grading,
rubric: rubric,
artifact: submission,
assessor: @teacher
)
end
let_once(:rubric) { rubric_model }
let_once(:submission) { @assignment.submissions.find_by!(user: @student) }
before do
submission.score = 1
ra.build_rubric_association(
use_for_grading: true,
association_object: @assignment
)
end
it 'is nil by default' do
expect(@assignment).to receive(:grade_student).with(
ra.submission.student,
score: ra.score,
grader: ra.assessor,
graded_anonymously: nil,
grade_posting_in_progress: nil
)
ra.save!
end
it 'passes grade_posting_in_progress from submission' do
submission.grade_posting_in_progress = true
expect(@assignment).to receive(:grade_student).with(
ra.submission.student,
score: ra.score,
grader: ra.assessor,
graded_anonymously: nil,
grade_posting_in_progress: submission.grade_posting_in_progress
)
ra.save!
end
end
it 'should set group on submission' do
group_category = @course.group_categories.create!(name: "Test Group Set")
group = @course.groups.create!(name: "Group A", group_category: group_category)

View File

@ -0,0 +1,85 @@
#
# Copyright (C) 2019 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require_relative '../../common'
require_relative '../pages/moderate_page'
require_relative '../pages/speedgrader_page'
describe 'Moderated Marking' do
include_context 'in-process server selenium tests'
let_once(:account) do
account = Account.default
account.enable_feature!(:moderated_grading)
account
end
let_once(:course) do
course = account.courses.create!
course.offer!
course
end
let_once(:teacher) { teacher_in_course(course: course, active_enrollment: true).user }
let_once(:ta) { ta_in_course(course: course, active_enrollment: true).user }
let_once(:student) { student_in_course(course: course, active_enrollment: true).user }
let_once(:assignment) do
course.assignments.create!(
final_grader: teacher,
grader_count: 2,
moderated_grading: true,
points_possible: 10,
submission_types: :online_text_entry,
title:'Moderated Assignment',
)
end
context 'moderation page' do
it 'allows viewing provisional grades and releasing final grade' do
rubric = outcome_with_rubric
association = rubric.associate_with(assignment, course, purpose: 'grading')
association.update!(use_for_grading: true)
assignment.submit_homework(student, submission_type: :online_text_entry, body: :asdf)
user_session(ta)
Speedgrader.visit(course.id, assignment.id)
scroll_into_view('.toggle_full_rubric')
f('.toggle_full_rubric').click
f('td.criterion_points input').send_keys('3')
Speedgrader.expand_right_pane
fj("span:contains('Amazing'):visible").click
wait_for_ajaximations
scroll_into_view('.save_rubric_button')
f('#rubric_full .save_rubric_button').click
wait_for_ajaximations
Speedgrader.grade_input.send_keys 10
Speedgrader.grade_input.send_keys :tab
wait_for_ajaximations
user_session(teacher)
ModeratePage.visit(course.id, assignment.id)
ModeratePage.select_provisional_grade_for_student_by_position(student, 1)
ModeratePage.click_release_grades_button
driver.switch_to.alert.accept
expect_instui_flash_message "Grades were successfully released to the gradebook"
wait_for_ajax_requests
end
end
end