update grading period scores when student is graded

When a student is graded, the score for their grading period will now
be updated in addition to their total course score. Previously, when a
student was graded, their grading period score was not updated.

closes CNVS-26709

test plan:
1) In a course with multiple grading periods enabled and grading
   periods set up, grade students for assignments that fall in grading
   periods and make sure the scores get updated for the grading period
   and for the course total.

   * To make sure the scores get updated for the grading period, open a
     rails console and find the grading period scores (we'll assume the
     student's enrollment ID is 5, and the ID of the grading period
     that the assignment falls in is 8).

       $ Enrollment.find(5).scores.where(grading_period_id: 8)

     Then, give a different grade to the student for the assignment
     that falls in the grading period and run the command above again.
     You should see that the current_score and final_score have
     changed.

    * To make sure the scores get updated for the course total, open a
      rails console and find the course total scores (we'll assume the
      student's enrollment ID is 5).

        $ Enrollment.find(5).scores.where(grading_period_id: nil)

      Then, give a different grade to the student for the assignment
      that falls in the grading period and run the command above again.
      You should see that the current_score and final_score have
      changed.

Edge case: make sure that grading period scores continue to get updated
even if MGP is turned off. To verify this, turn off MGP in a course
that originally had MGP turned on. Then, grade students in assignments
that fell in a grading period when MGP was enabled. After grading, the
grading period scores should be updated.

Change-Id: I29e4e22a0b73d1062a93c6ad420d876c2f5d040a
Reviewed-on: https://gerrit.instructure.com/98603
Reviewed-by: Jeremy Neander <jneander@instructure.com>
Reviewed-by: Derek Bender <djbender@instructure.com>
Tested-by: Jenkins
QA-Review: Anju Reddy <areddy@instructure.com>
Product-Review: Keith T. Garner <kgarner@instructure.com>
This commit is contained in:
Spencer Olson 2016-12-29 15:05:07 -06:00
parent 43e64d7c52
commit 37fa769aef
12 changed files with 460 additions and 148 deletions

View File

@ -1083,10 +1083,10 @@ class Course < ActiveRecord::Base
end
end
def recompute_student_scores(student_ids = nil)
def recompute_student_scores(student_ids = nil, grading_period_id: nil)
student_ids ||= self.student_ids
Rails.logger.info "GRADES: recomputing scores in course=#{global_id} students=#{student_ids.inspect}"
Enrollment.recompute_final_score(student_ids, self.id)
Enrollment.recompute_final_score(student_ids, self.id, grading_period_id: grading_period_id)
end
handle_asynchronously_if_production :recompute_student_scores,
:singleton => proc { |c| "recompute_student_scores:#{ c.global_id }" }

View File

@ -984,39 +984,45 @@ class Enrollment < ActiveRecord::Base
# please add appropriate calls to this so that the cached values don't get
# stale! And once you've added the call, add the condition to the comment
# here for future enlightenment.
def self.recompute_final_score(user_ids, course_id)
GradeCalculator.recompute_final_score(user_ids, course_id)
def self.recompute_final_score(*args)
GradeCalculator.recompute_final_score(*args)
end
def self.recompute_final_score_if_stale(course, user=nil)
Rails.cache.fetch(['recompute_final_scores', course.id, user].cache_key, :expires_in => Setting.get('recompute_grades_window', 600).to_i.seconds) do
recompute_final_score user ? user.id : course.student_enrollments.except(:preload).distinct.pluck(:user_id), course.id
def self.recompute_final_score_if_stale(course, user=nil, compute_score_opts = {})
Rails.cache.fetch(
['recompute_final_scores', course.id, user, compute_score_opts[:grading_period_id]].cache_key,
expires_in: Setting.get('recompute_grades_window', 600).to_i.seconds
) do
user_id = user ? user.id : course.student_enrollments.except(:preload).distinct.pluck(:user_id)
recompute_final_score(user_id, course.id, compute_score_opts)
yield if block_given?
true
end
end
def computed_current_grade
cached_score_or_grade(:current, :grade)
def computed_current_grade(grading_period_id: nil)
cached_score_or_grade(:current, :grade, grading_period_id: grading_period_id)
end
def computed_final_grade
cached_score_or_grade(:final, :grade)
def computed_final_grade(grading_period_id: nil)
cached_score_or_grade(:final, :grade, grading_period_id: grading_period_id)
end
def computed_current_score
cached_score_or_grade(:current, :score)
def computed_current_score(grading_period_id: nil)
cached_score_or_grade(:current, :score, grading_period_id: grading_period_id)
end
def computed_final_score
cached_score_or_grade(:final, :score)
def computed_final_score(grading_period_id: nil)
cached_score_or_grade(:final, :score, grading_period_id: grading_period_id)
end
def cached_score_or_grade(current_or_final, score_or_grade)
score = find_score
def cached_score_or_grade(current_or_final, score_or_grade, grading_period_id: nil)
score = find_score(grading_period_id: grading_period_id)
if score.present?
score.send("#{current_or_final}_#{score_or_grade}")
else
return nil if grading_period_id.present?
# TODO: drop the computed_current_score / computed_final_score columns
# after the data fixup to populate the scores table completes
score = read_attribute("computed_#{current_or_final}_score")

View File

@ -32,6 +32,8 @@ class GradingPeriod < ActiveRecord::Base
before_validation :adjust_close_date_for_course_period
before_validation :ensure_close_date
after_save :recompute_scores, if: :dates_changed?
scope :current, -> do
period_table = GradingPeriod.arel_table
now = Time.zone.now
@ -208,4 +210,19 @@ class GradingPeriod < ActiveRecord::Base
errors.add(:close_date, t('must be on or after end date'))
end
end
def recompute_scores
if course_group?
courses = [grading_period_group.course]
else
term_ids = grading_period_group.enrollment_terms.pluck(:id)
courses = Course.where(enrollment_term_id: term_ids)
end
# Course#recompute_student_scores is asynchronous
courses.each { |course| course.recompute_student_scores(grading_period_id: self.id) }
end
def dates_changed?
start_date_changed? || end_date_changed?
end
end

View File

@ -398,7 +398,19 @@ class Submission < ActiveRecord::Base
else
Rails.logger.info "GRADES: submission #{global_id} score changed. recomputing grade for course #{context.global_id} user #{user_id}."
self.class.connection.after_transaction_commit do
Enrollment.send_later_if_production_enqueue_args(:recompute_final_score, { run_at: 3.seconds.from_now }, self.user_id, self.context.id)
effective_due_dates = EffectiveDueDates.new(self.context, self.assignment_id)
grading_period_id = effective_due_dates.grading_period_id_for(
student_id: self.user_id,
assignment_id: self.assignment_id
)
Enrollment.send_later_if_production_enqueue_args(
:recompute_final_score,
{ run_at: 3.seconds.from_now },
self.user_id,
self.context.id,
grading_period_id: grading_period_id,
update_all_grading_period_scores: false
)
end
end
self.assignment.send_later_if_production(:multiple_module_actions, [self.user_id], :scored, self.score) if self.assignment

View File

@ -78,8 +78,20 @@ class EffectiveDueDates
end
end
def grading_period_id_for(student_id:, assignment_id:)
find_effective_due_date(student_id, assignment_id).fetch(:grading_period_id, nil)
end
private
def find_effective_due_date(student_id, assignment_id)
find_effective_due_dates_for_assignment(assignment_id).fetch(student_id, {})
end
def find_effective_due_dates_for_assignment(assignment_id)
to_hash.fetch(assignment_id, {})
end
def usable_student_id?(student_id)
return false unless student_id.present?
return false unless student_id.to_i.present?

View File

@ -20,7 +20,11 @@ class GradeCalculator
attr_accessor :submissions, :assignments, :groups
def initialize(user_ids, course, opts = {})
opts = opts.reverse_merge(:ignore_muted => true)
opts = opts.reverse_merge(
ignore_muted: true,
update_all_grading_period_scores: true,
update_course_score: true
)
Rails.logger.info("GRADES: calc args: user_ids=#{user_ids.inspect}")
Rails.logger.info("GRADES: calc args: course=#{course.inspect}")
@ -29,6 +33,12 @@ class GradeCalculator
@course = course.is_a?(Course) ? course : Course.find(course)
@groups = @course.assignment_groups.active
@grading_period = opts[:grading_period]
# if we're updating an overall course score (no grading period specified), we
# want to first update all grading period scores for the users
@update_all_grading_period_scores = @grading_period.nil? && opts[:update_all_grading_period_scores]
# if we're updating a grading period score, we also need to update the
# overall course score
@update_course_score = @grading_period.present? && opts[:update_course_score]
@assignments = @course.assignments.published.gradeable.to_a
@user_ids = Array(user_ids).map { |id| Shard.relative_id_for(id, Shard.current, @course.shard) }
@current_updates = {}
@ -37,12 +47,19 @@ class GradeCalculator
end
# recomputes the scores and saves them to each user's Enrollment
def self.recompute_final_score(user_ids, course_id)
def self.recompute_final_score(user_ids, course_id, compute_score_opts = {})
user_ids = Array(user_ids).uniq.map(&:to_i)
return if user_ids.empty?
course = Course.active.find(course_id)
grading_period = GradingPeriod.for(course).find_by(
id: compute_score_opts.delete(:grading_period_id)
)
user_ids.in_groups_of(1000, false) do |user_ids_group|
calc = GradeCalculator.new user_ids_group, course_id
calc.compute_and_save_scores
GradeCalculator.new(
user_ids_group,
course_id,
compute_score_opts.merge(grading_period: grading_period)
).compute_and_save_scores
end
end
@ -78,12 +95,40 @@ class GradeCalculator
end
def compute_and_save_scores
calculate_grading_period_scores if @update_all_grading_period_scores
compute_scores
save_scores
calculate_course_score if @update_course_score
end
private
def calculate_grading_period_scores
GradingPeriod.for(@course).each do |grading_period|
# update this grading period score, and do not
# update any other scores (grading period or course)
# after this one
GradeCalculator.new(
@user_ids,
@course,
update_all_grading_period_scores: false,
update_course_score: false,
grading_period: grading_period
).compute_and_save_scores
end
end
def calculate_course_score
# update the overall course score now that we've finished
# updating the grading period score
GradeCalculator.new(
@user_ids,
@course,
update_all_grading_period_scores: false,
update_course_score: false
).compute_and_save_scores
end
def enrollments
@enrollments ||= Enrollment.shard(@course).active.
where(user_id: @user_ids, course_id: @course.id).

View File

@ -52,19 +52,17 @@ describe DataFixup::PopulateScoresTable do
@student2_enrollment.scores.create!(grading_period_id: nil, final_score: 29.3)
create_grading_periods_for(@course2)
gp = @course2.grading_periods.first
@student3_enrollment.scores.create!(grading_period: gp, final_score: 100.3)
expect { DataFixup::PopulateScoresTable.run }.to change { Score.count }.from(2).to(5)
.and change { @student1_enrollment.scores.count }.from(0).to(1)
@student3_enrollment.scores.where(grading_period: gp).first.update(final_score: 100.3)
@student3_enrollment.scores.where(grading_period_id: nil).first.delete
expect { DataFixup::PopulateScoresTable.run }.to change { Score.count }.from(4).to(6)
.and change { @student3_enrollment.scores.count }.from(1).to(2)
.and change { @student4_enrollment.scores.count }.from(0).to(1)
expect(Score.where(grading_period: nil).pluck(:current_score, :final_score)).to match_array(
[[76.3, 24], [nil, 29.3], [56.5, nil], [nil, nil]]
)
expect(Score.pluck(:grading_period_id)).to match_array([nil, nil, gp.id, nil, nil])
expect(Score.pluck(:grading_period_id)).to match_array([nil, nil, gp.id, gp.id, nil, nil])
end
it "creates Score object for each grading period" do
it "creates Score object for each grading period, if one doesn't already exist" do
grader1 = teacher_in_course(course: @course1, active_all: true).user
grader2 = teacher_in_course(course: @course2, active_all: true).user
group1 = @course1.grading_period_groups.create!
@ -83,6 +81,7 @@ describe DataFixup::PopulateScoresTable do
assignment3.grade_student(@student4_enrollment.user, grader: grader2, score: 20)
assignment4 = @course2.assignments.create!(due_at: 6.months.ago, points_possible: 100)
assignment4.grade_student(@student3_enrollment.user, grader: grader2, score: 30)
Score.where.not(grading_period_id: nil).delete_all
expect { DataFixup::PopulateScoresTable.run }.to change { Score.count }.from(4).to(10)
.and change { @student1_enrollment.scores.count }.from(1).to(3)

View File

@ -1276,7 +1276,7 @@ describe Course do
end
end
describe '#any_in_closed_grading_period?' do
context 'grading periods' do
before(:once) do
@now = Time.zone.now
@test_course = Course.create!
@ -1286,141 +1286,147 @@ describe Course do
@assignment2 = @test_course.assignments.create!
@gp_group = Factories::GradingPeriodGroupHelper.new.create_for_account(@test_course.account)
@gp_group.enrollment_terms << @test_course.enrollment_term
end
it 'returns false if multiple grading periods is disabled' do
Factories::GradingPeriodHelper.new.create_for_group(@gp_group, {
@grading_period = Factories::GradingPeriodHelper.new.create_for_group(
@gp_group,
start_date: 20.days.ago(@now),
end_date: 15.days.ago(@now),
close_date: 10.days.ago(@now)
})
@assignment2.due_at = 17.days.ago(@now)
@assignment2.only_visible_to_overrides = false
@assignment2.save!
@test_course.disable_feature! :multiple_grading_periods
edd = EffectiveDueDates.for_course(@test_course)
edd.expects(:to_hash).never
expect(edd.any_in_closed_grading_period?).to eq(false)
)
end
context 'with multiple grading periods' do
before(:once) do
@test_course.enable_feature! :multiple_grading_periods
end
it 'returns true if any students in any assignments have a due date in a closed grading period' do
Factories::GradingPeriodHelper.new.create_for_group(@gp_group, {
start_date: 20.days.ago(@now),
end_date: 15.days.ago(@now),
close_date: 10.days.ago(@now)
})
@assignment2.due_at = 1.day.ago(@now)
describe '#any_in_closed_grading_period?' do
it 'returns false if multiple grading periods is disabled' do
@assignment2.due_at = 17.days.ago(@now)
@assignment2.only_visible_to_overrides = false
@assignment2.save!
override = @assignment2.assignment_overrides.create!(due_at: 19.days.ago(@now), due_at_overridden: true)
override.assignment_override_students.create!(user: @student2)
edd = EffectiveDueDates.for_course(@test_course)
expect(edd.any_in_closed_grading_period?).to eq(true)
end
it 'returns false if no student in any assignments has a due date in a closed grading period' do
Factories::GradingPeriodHelper.new.create_for_group(@gp_group, {
start_date: 20.days.ago(@now),
end_date: 15.days.ago(@now),
close_date: 10.days.ago(@now)
})
@assignment2.due_at = 1.day.ago(@now)
@assignment2.only_visible_to_overrides = false
@assignment2.save!
override = @assignment2.assignment_overrides.create!(due_at: 2.days.ago(@now), due_at_overridden: true)
override.assignment_override_students.create!(user: @student2)
@test_course.disable_feature! :multiple_grading_periods
edd = EffectiveDueDates.for_course(@test_course)
edd.expects(:to_hash).never
expect(edd.any_in_closed_grading_period?).to eq(false)
end
it 'memoizes the result' do
edd = EffectiveDueDates.for_course(@test_course)
edd.expects(:to_hash).once.returns({})
2.times { edd.any_in_closed_grading_period? }
context 'with multiple grading periods' do
before(:once) do
@test_course.enable_feature! :multiple_grading_periods
end
it 'returns true if any students in any assignments have a due date in a closed grading period' do
@assignment2.due_at = 1.day.ago(@now)
@assignment2.only_visible_to_overrides = false
@assignment2.save!
override = @assignment2.assignment_overrides.create!(due_at: 19.days.ago(@now), due_at_overridden: true)
override.assignment_override_students.create!(user: @student2)
edd = EffectiveDueDates.for_course(@test_course)
expect(edd.any_in_closed_grading_period?).to eq(true)
end
it 'returns false if no student in any assignments has a due date in a closed grading period' do
@assignment2.due_at = 1.day.ago(@now)
@assignment2.only_visible_to_overrides = false
@assignment2.save!
override = @assignment2.assignment_overrides.create!(due_at: 2.days.ago(@now), due_at_overridden: true)
override.assignment_override_students.create!(user: @student2)
edd = EffectiveDueDates.for_course(@test_course)
expect(edd.any_in_closed_grading_period?).to eq(false)
end
it 'memoizes the result' do
edd = EffectiveDueDates.for_course(@test_course)
edd.expects(:to_hash).once.returns({})
2.times { edd.any_in_closed_grading_period? }
end
end
end
end
describe '#in_closed_grading_period?' do
before(:once) do
@now = Time.zone.now
@test_course = Course.create!
@student1 = student_in_course(course: @test_course, active_all: true).user
@student2 = student_in_course(course: @test_course, active_all: true).user
@assignment1 = @test_course.assignments.create!(due_at: 2.weeks.from_now(@now))
@assignment2 = @test_course.assignments.create!
@gp_group = Factories::GradingPeriodGroupHelper.new.create_for_account(@test_course.account)
@gp_group.enrollment_terms << @test_course.enrollment_term
end
it 'returns false if multiple grading periods is disabled' do
Factories::GradingPeriodHelper.new.create_for_group(@gp_group, {
start_date: 20.days.ago(@now),
end_date: 15.days.ago(@now),
close_date: 10.days.ago(@now)
})
@assignment2.due_at = 17.days.ago(@now)
@assignment2.only_visible_to_overrides = false
@assignment2.save!
@test_course.disable_feature! :multiple_grading_periods
edd = EffectiveDueDates.for_course(@test_course)
edd.expects(:to_hash).never
expect(edd.in_closed_grading_period?(@assignment2)).to eq(false)
end
it 'returns false if assignment id is nil' do
edd = EffectiveDueDates.for_course(@test_course, @assignment1)
edd.expects(:to_hash).never
expect(edd.in_closed_grading_period?(nil)).to eq(false)
end
context 'with multiple grading periods' do
before(:once) do
@test_course.enable_feature! :multiple_grading_periods
describe '#grading_period_id_for' do
it 'returns the grading_period_id for the given student and assignment' do
@assignment1.update!(due_at: 2.days.from_now(@grading_period.start_date))
effective_due_dates = EffectiveDueDates.new(@test_course, @assignment1.id)
grading_period_id = effective_due_dates.grading_period_id_for(
student_id: @student1.id,
assignment_id: @assignment1.id
)
expect(grading_period_id).to eq(@grading_period.id)
end
before(:each) do
Factories::GradingPeriodHelper.new.create_for_group(@gp_group, {
start_date: 20.days.ago(@now),
end_date: 15.days.ago(@now),
close_date: 10.days.ago(@now)
})
@assignment2.due_at = 1.day.ago(@now)
it 'returns nil if there if the given student and assignment do not fall in a grading period' do
effective_due_dates = EffectiveDueDates.new(@test_course, @assignment1.id)
grading_period_id = effective_due_dates.grading_period_id_for(
student_id: @student1.id,
assignment_id: @assignment1.id
)
expect(grading_period_id).to be_nil
end
it 'returns nil if the assignment is not assigned to the student' do
@assignment1.update!(
due_at: 2.days.from_now(@grading_period.start_date),
only_visible_to_overrides: true
)
effective_due_dates = EffectiveDueDates.new(@test_course, @assignment1.id)
grading_period_id = effective_due_dates.grading_period_id_for(
student_id: @student1.id,
assignment_id: @assignment1.id
)
expect(grading_period_id).to be_nil
end
end
describe '#in_closed_grading_period?' do
it 'returns false if multiple grading periods is disabled' do
@assignment2.due_at = 17.days.ago(@now)
@assignment2.only_visible_to_overrides = false
@assignment2.save!
override = @assignment2.assignment_overrides.create!(due_at: 19.days.ago(@now), due_at_overridden: true)
override.assignment_override_students.create!(user: @student2)
@edd = EffectiveDueDates.for_course(@test_course)
@test_course.disable_feature! :multiple_grading_periods
edd = EffectiveDueDates.for_course(@test_course)
edd.expects(:to_hash).never
expect(edd.in_closed_grading_period?(@assignment2)).to eq(false)
end
it 'returns true if any students in the given assignment have a due date in a closed grading period' do
expect(@edd.in_closed_grading_period?(@assignment2)).to eq(true)
it 'returns false if assignment id is nil' do
edd = EffectiveDueDates.for_course(@test_course, @assignment1)
edd.expects(:to_hash).never
expect(edd.in_closed_grading_period?(nil)).to eq(false)
end
it 'accepts assignment id as the argument' do
expect(@edd.in_closed_grading_period?(@assignment2.id)).to eq(true)
end
context 'with multiple grading periods' do
before(:once) do
@test_course.enable_feature! :multiple_grading_periods
end
it 'returns false if no student in the given assignment has a due date in a closed grading period' do
expect(@edd.in_closed_grading_period?(@assignment1)).to eq(false)
end
before(:each) do
@assignment2.due_at = 1.day.ago(@now)
@assignment2.only_visible_to_overrides = false
@assignment2.save!
override = @assignment2.assignment_overrides.create!(due_at: 19.days.ago(@now), due_at_overridden: true)
override.assignment_override_students.create!(user: @student2)
it 'returns false if the specified student has a due date in an open grading period' do
override = @assignment2.assignment_overrides.create!(due_at: 1.day.from_now(@now), due_at_overridden: true)
override.assignment_override_students.create!(user: @student1)
@edd = EffectiveDueDates.for_course(@test_course)
end
expect(@edd.in_closed_grading_period?(@assignment2, @student1)).to be_falsey
expect(@edd.in_closed_grading_period?(@assignment2, @student1.id)).to be_falsey
it 'returns true if any students in the given assignment have a due date in a closed grading period' do
expect(@edd.in_closed_grading_period?(@assignment2)).to eq(true)
end
it 'accepts assignment id as the argument' do
expect(@edd.in_closed_grading_period?(@assignment2.id)).to eq(true)
end
it 'returns false if no student in the given assignment has a due date in a closed grading period' do
expect(@edd.in_closed_grading_period?(@assignment1)).to eq(false)
end
it 'returns false if the specified student has a due date in an open grading period' do
override = @assignment2.assignment_overrides.create!(due_at: 1.day.from_now(@now), due_at_overridden: true)
override.assignment_override_students.create!(user: @student1)
expect(@edd.in_closed_grading_period?(@assignment2, @student1)).to be_falsey
expect(@edd.in_closed_grading_period?(@assignment2, @student1.id)).to be_falsey
end
end
end
end

View File

@ -456,6 +456,71 @@ describe GradeCalculator do
end
end
describe '#compute_and_save_scores' do
before(:once) do
@first_period, @second_period = grading_periods(count: 2)
first_assignment = @course.assignments.create!(
due_at: 1.day.from_now(@first_period.start_date),
points_possible: 100
)
second_assignment = @course.assignments.create!(
due_at: 1.day.from_now(@second_period.start_date),
points_possible: 100
)
first_assignment.grade_student(@student, grade: 25, grader: @teacher)
second_assignment.grade_student(@student, grade: 75, grader: @teacher)
# update_column to avoid callbacks on submission that would trigger score updates
Submission.where(user: @student, assignment: first_assignment).first.update_column(:score, 100.0)
Submission.where(user: @student, assignment: second_assignment).first.update_column(:score, 95.0)
end
let(:scores) { @student.enrollments.first.scores }
let(:overall_course_score) { scores.where(grading_period_id: nil).first }
it 'updates the overall course score' do
GradeCalculator.new(@student.id, @course).compute_and_save_scores
expect(overall_course_score.current_score).to eq(97.5)
end
it 'updates all grading period scores' do
GradeCalculator.new(@student.id, @course).compute_and_save_scores
grading_period_scores = scores.where.not(grading_period_id: nil).order(:current_score).pluck(:current_score)
expect(grading_period_scores).to match_array([100.0, 95.0])
end
it 'does not update grading period scores if update_all_grading_period_scores is false' do
GradeCalculator.new(@student.id, @course, update_all_grading_period_scores: false).compute_and_save_scores
grading_period_scores = scores.where.not(grading_period_id: nil).order(:current_score).pluck(:current_score)
expect(grading_period_scores).to match_array([25.0, 75.0])
end
context 'grading period is provided' do
it 'updates the grading period score' do
GradeCalculator.new(@student.id, @course, grading_period: @first_period).compute_and_save_scores
score = scores.where(grading_period_id: @first_period).first
expect(score.current_score).to eq(100.0)
end
it 'updates the overall course score' do
GradeCalculator.new(@student.id, @course, grading_period: @first_period).compute_and_save_scores
expect(overall_course_score.current_score).to eq(97.5)
end
it 'does not update scores for other grading periods' do
GradeCalculator.new(@student.id, @course, grading_period: @first_period).compute_and_save_scores
score = scores.where(grading_period_id: @second_period).first
expect(score.current_score).to eq(75.0)
end
it 'does not update the overall course score if update_course_score is false' do
GradeCalculator.new(
@student.id, @course, grading_period: @first_period, update_course_score: false
).compute_and_save_scores
expect(overall_course_score.current_score).to eq(50.0)
end
end
end
it "should return grades in the order they are requested" do
@student1 = @student
student_in_course

View File

@ -134,7 +134,7 @@ describe Enrollment do
expect(@enrollment.computed_current_score).to eq 95.5
end
it 'ignores grading period scores' do
it 'ignores grading period scores when passed no arguments' do
@enrollment.scores.create!(current_score: 80.3, grading_period: period)
expect(@enrollment.computed_current_score).to eq 95.5
end
@ -144,11 +144,23 @@ describe Enrollment do
score.destroy
expect(@enrollment.computed_current_score).to eq 95.5
end
it 'computes current score for a given grading period id' do
@enrollment.scores.create!(current_score: 80.3)
@enrollment.scores.create!(current_score: 70.6, grading_period: period)
current_score = @enrollment.computed_current_score(grading_period_id: period.id)
expect(current_score).to eq 70.6
end
it 'returns nil if a grading period score is requested and does not exist' do
current_score = @enrollment.computed_current_score(grading_period_id: period.id)
expect(current_score).to eq nil
end
end
describe '#computed_current_grade' do
before(:each) do
Course.any_instance.expects(:grading_standard_enabled?).at_least_once.returns(true)
Course.any_instance.stubs(:grading_standard_enabled?).returns(true)
end
it 'uses the value from the associated score object, if one exists' do
@ -160,7 +172,7 @@ describe Enrollment do
expect(@enrollment.computed_current_grade).to eq 'A'
end
it 'ignores grading period grades' do
it 'ignores grading period grades when passed no arguments' do
@enrollment.scores.create!(current_score: 80.3, grading_period: period)
expect(@enrollment.computed_current_grade).to eq 'A'
end
@ -170,6 +182,18 @@ describe Enrollment do
score.destroy
expect(@enrollment.computed_current_grade).to eq 'A'
end
it 'computes current grade for a given grading period id' do
@enrollment.scores.create!(current_score: 80.3)
@enrollment.scores.create!(current_score: 70.6, grading_period: period)
current_grade = @enrollment.computed_current_grade(grading_period_id: period.id)
expect(current_grade).to eq 'C-'
end
it 'returns nil if a grading period grade is requested and does not exist' do
current_grade = @enrollment.computed_current_grade(grading_period_id: period.id)
expect(current_grade).to eq nil
end
end
describe '#graded_at' do
@ -226,7 +250,7 @@ describe Enrollment do
expect(@enrollment.computed_final_score).to eq 95.5
end
it 'ignores grading period scores' do
it 'ignores grading period scores when passed no arguments' do
@enrollment.scores.create!(final_score: 80.3, grading_period: period)
expect(@enrollment.computed_final_score).to eq 95.5
end
@ -236,11 +260,23 @@ describe Enrollment do
score.destroy
expect(@enrollment.computed_final_score).to eq 95.5
end
it 'computes final score for a given grading period id' do
@enrollment.scores.create!(final_score: 80.3)
@enrollment.scores.create!(final_score: 70.6, grading_period: period)
final_score = @enrollment.computed_final_score(grading_period_id: period.id)
expect(final_score).to eq 70.6
end
it 'returns nil if a grading period score is requested and does not exist' do
final_score = @enrollment.computed_final_score(grading_period_id: period.id)
expect(final_score).to eq nil
end
end
describe '#computed_final_grade' do
before(:each) do
Course.any_instance.expects(:grading_standard_enabled?).at_least_once.returns(true)
Course.any_instance.stubs(:grading_standard_enabled?).returns(true)
end
it 'uses the value from the associated score object, if one exists' do
@ -252,7 +288,7 @@ describe Enrollment do
expect(@enrollment.computed_final_grade).to eq 'A'
end
it 'ignores grading period grades' do
it 'ignores grading period grades when passed no arguments' do
@enrollment.scores.create!(final_score: 80.3, grading_period: period)
expect(@enrollment.computed_final_grade).to eq 'A'
end
@ -262,6 +298,18 @@ describe Enrollment do
score.destroy
expect(@enrollment.computed_final_grade).to eq 'A'
end
it 'computes final grade for a given grading period id' do
@enrollment.scores.create!(final_score: 80.3)
@enrollment.scores.create!(final_score: 70.6, grading_period: period)
final_grade = @enrollment.computed_final_grade(grading_period_id: period.id)
expect(final_grade).to eq 'C-'
end
it 'returns nil if a grading period grade is requested and does not exist' do
final_grade = @enrollment.computed_final_grade(grading_period_id: period.id)
expect(final_grade).to eq nil
end
end
end

View File

@ -23,14 +23,14 @@ describe GradingPeriod do
let(:group_helper) { Factories::GradingPeriodGroupHelper.new }
let(:account) { Account.create! }
let(:course) { Course.create! }
let(:course) { account.courses.create! }
let(:grading_period_group) do
group = account.grading_period_groups.create!(title: "A Group")
group.enrollment_terms << course.enrollment_term
term = course.enrollment_term
group.enrollment_terms << term
group
end
let(:now) { Time.zone.now.change(usec: 0) }
let(:params) do
{
title: 'A Grading Period',
@ -194,7 +194,6 @@ describe GradingPeriod do
end
end
describe "#as_json_with_user_permissions" do
it "includes the close_date in the returned object" do
json = grading_period.as_json_with_user_permissions(User.new)
@ -691,4 +690,39 @@ describe GradingPeriod do
expect(subject.reload.weight).to eql 1.5
end
end
describe 'grading period scores' do
before do
student_in_course(course: course, active_all: true)
teacher_in_course(course: course, active_all: true)
@assignment = course.assignments.create!(due_at: 10.days.from_now(now), points_possible: 10)
@assignment.grade_student(@student, grade: 8, grader: @teacher)
end
it 'creates scores for the grading period upon its creation' do
expect{ grading_period.save! }.to change{ Score.count }.from(1).to(2)
end
it 'updates grading period scores when the grading period end date is changed' do
grading_period.save!
expect do
day_after_assignment_is_due = 1.day.from_now(@assignment.due_at)
grading_period.update!(
end_date: day_after_assignment_is_due,
close_date: day_after_assignment_is_due
)
end.to change{
Score.where(grading_period_id: grading_period).first.current_score
}.from(nil).to(80.0)
end
it 'updates grading period scores when the grading period start date is changed' do
day_before_grading_period_starts = 1.day.ago(grading_period.start_date)
@assignment.update!(due_at: day_before_grading_period_starts)
grading_period.save!
expect{ grading_period.update!(start_date: 1.day.ago(@assignment.due_at)) }.to change{
Score.where(grading_period_id: grading_period).first.current_score
}.from(nil).to(80.0)
end
end
end

View File

@ -619,6 +619,74 @@ describe Submission do
end
end
describe 'computation of scores' do
before(:once) do
@assignment.update!(points_possible: 10)
@submission = Submission.create!(@valid_attributes)
end
let(:scores) do
enrollment = Enrollment.where(user_id: @submission.user_id, course_id: @submission.context).first
enrollment.scores.order(:grading_period_id)
end
let(:grading_period_scores) do
scores.where.not(grading_period_id: nil)
end
it 'recomputes course scores when the submission score changes' do
expect { @submission.update!(score: 5) }.to change {
scores.pluck(:current_score)
}.from([nil]).to([50.0])
end
context 'Multiple Grading Periods' do
before(:once) do
@now = Time.zone.now
course = @submission.context
assignment_outside_of_period = course.assignments.create!(
due_at: 10.days.from_now(@now),
points_possible: 10
)
assignment_outside_of_period.grade_student(@user, grade: 8, grader: @teacher)
@assignment.update!(due_at: @now)
@root_account = course.root_account
@root_account.enable_feature!(:multiple_grading_periods)
group = @root_account.grading_period_groups.create!
group.enrollment_terms << course.enrollment_term
@grading_period = group.grading_periods.create!(
title: 'Current Grading Period',
start_date: 5.days.ago(@now),
end_date: 5.days.from_now(@now)
)
end
it 'updates the course score and grading period score if a submission ' \
'in a grading period is graded' do
expect { @submission.update!(score: 5) }.to change {
scores.pluck(:current_score)
}.from([nil, 80.0]).to([50.0, 65.0])
end
it 'keeps grading period scores updated even if the feature flag is disabled' do
@submission.update!(score: 10)
@root_account.disable_feature!(:multiple_grading_periods)
expect { @submission.update!(score: 5) }.to change {
grading_period_scores.pluck(:current_score)
}.from([100.0]).to([50.0])
end
it 'only updates the course score (not the grading period score) if a submission ' \
'not in a grading period is graded' do
day_after_grading_period_ends = 1.day.from_now(@grading_period.end_date)
@assignment.update!(due_at: day_after_grading_period_ends)
expect { @submission.update!(score: 5) }.to change {
scores.pluck(:current_score)
}.from([nil, 80.0]).to([nil, 65.0])
end
end
end
describe '#can_grade?' do
before(:each) do
@account = Account.new