apply deductions for missing submissions periodically

closes CNVS-36153

Test Plan

NOTE: If you don't feel like waiting 5 minutes for the missing
deductions job to run open config/initializers/period_jobs.rb
and change */5 * * * * to */1 * * * * on line 183. This will
make the job run once per minute. This change will need to be
made prior to starting the rails server.

1. Create a course with some students in it
2. Create some quiz and non-quiz assignments
  a. Mix grading types and submission types
  b. Make sure at least one of the assignments is complete/incomplete
     and has some kind of online submission type
  c. Set the due dates for some of the assignments/quizzes to be in
     the past within the last 24 hours, some further in the past,
     some in the future. Make sure at least one complete/incomplete
     assignment has a due date within the last 24 hours and one
     other grading type within the last 24 hours.
  d. Create at least one assignment due within the last 24 hours and
  	 manually grade one of the students in your course, but not the
  	 others.
  d. Create at least one assignment due within the last 24 hours and:
     1. Mark one of its submissions as missing via the rails
     	console with:
     	Assignment.find(4).submissions.first.update(
     		late_policy_status: 'missing'
     	)
     2. Mark one if its submissions as late via the rails console:
       Assignment.find(4).submissions.second.update(
     		late_policy_status: 'late'
     	)
  f. Wait for the missing deductions job to run in the console you'll
     see output that starts with:
  	"periodic: MissingPolicyApplicator.apply_missing_deductions...."
  g. Visit Gradezilla.
  h. Verify that none of the assignments were automatically graded.
3. Create a LatePolicy for your course via the rails console like so:
   LatePolicy.create(
   	course_id: <COURSE_ID>,
   	missing_submission_deduction_enabled: true,
   	missing_submission_deduction: 90
   )
4. Wait for the job to run again.
5. Visit Gradezilla
6. Verify that grades have been correctly assigned based on missing
   policy. If you created the LatePolicy with the command given in
   step 3 you should see each assignment/quiz given a score of 10%
   of total points. Complete / Incomplete assignments should always
   be given a score of zero and grade of Incomplete. Verify that
   the Total score for the course has been updated.
7. The submission where you set late_policy_status to late should
   not have been graded but the other submissions for that
   assignment should have been.
8. Verify that the manually graded assignment did not have its score
   overridden by the missing policy.
9. Grading periods
  a. Create an assignment in an open grading period that is due
     within the last 24 hours.
  b. Verify that it gets automatically graded when the job runs.
  c. Create an assignment in a closed grading period due within
     the last 24 hours.
  d. Verify that it is not automatically graded when the job runs.
10. Due date overrides
  a. Create assignments with overrides for individual students.
  b. Verify that if the override is due within the last 24 hours
     the submission gets automatically graded.
  c. Verify that if the override is not due within the last 24 hours
     it is not automatically graded.
11. Create a couple more courses and run through some of the above
    scenarios to verify that all courses with a missing policy have
    their missing policy applied correctly.

Change-Id: I39fd9e4c641df83a3bb222fc4b4f48d1c35a767b
Reviewed-on: https://gerrit.instructure.com/109727
Reviewed-by: Neil Gupta <ngupta@instructure.com>
Tested-by: Jenkins
Reviewed-by: Shahbaz Javeed <sjaveed@instructure.com>
QA-Review: Shahbaz Javeed <sjaveed@instructure.com>
Product-Review: Keith T. Garner <kgarner@instructure.com>
This commit is contained in:
Brian Park 2017-04-25 14:12:05 -07:00 committed by Keith T. Garner
parent 25e7be975f
commit 54b9ec859b
6 changed files with 291 additions and 18 deletions

View File

@ -42,6 +42,16 @@ class LatePolicy < ActiveRecord::Base
possible * [late_percent_deduct, maximum_deduct].min / 100 possible * [late_percent_deduct, maximum_deduct].min / 100
end end
def missing_points_deducted(assignment)
return assignment.points_possible.to_f if assignment.grading_type == 'pass_fail'
assignment.points_possible.to_f * missing_submission_deduction.to_f / 100
end
def points_for_missing(assignment)
return 0 if assignment.grading_type == 'pass_fail'
assignment.points_possible.to_f * (100 - missing_submission_deduction.to_f) / 100
end
private private
def interval_seconds def interval_seconds

View File

@ -123,11 +123,11 @@ Rails.configuration.after_initialize do
end end
end end
Delayed::Periodic.cron 'Alerts::DelayedAlertSender.process', '30 11 * * *', :priority => Delayed::LOW_PRIORITY do Delayed::Periodic.cron 'Alerts::DelayedAlertSender.process', '30 11 * * *', priority: Delayed::LOW_PRIORITY do
with_each_shard_by_database(Alerts::DelayedAlertSender, :process) with_each_shard_by_database(Alerts::DelayedAlertSender, :process)
end end
Delayed::Periodic.cron 'Attachment.do_notifications', '*/10 * * * *', :priority => Delayed::LOW_PRIORITY do Delayed::Periodic.cron 'Attachment.do_notifications', '*/10 * * * *', priority: Delayed::LOW_PRIORITY do
with_each_shard_by_database(Attachment, :do_notifications) with_each_shard_by_database(Attachment, :do_notifications)
end end
@ -176,7 +176,11 @@ Rails.configuration.after_initialize do
end end
end end
Delayed::Periodic.cron 'EnrollmentState.recalculate_expired_states', '*/5 * * * *', :priority => Delayed::LOW_PRIORITY do Delayed::Periodic.cron 'EnrollmentState.recalculate_expired_states', '*/5 * * * *', priority: Delayed::LOW_PRIORITY do
with_each_shard_by_database(EnrollmentState, :recalculate_expired_states) with_each_shard_by_database(EnrollmentState, :recalculate_expired_states)
end end
Delayed::Periodic.cron 'MissingPolicyApplicator.apply_missing_deductions', '*/5 * * * *', priority: Delayed::LOW_PRIORITY do
with_each_shard_by_database(MissingPolicyApplicator, :apply_missing_deductions)
end
end end

View File

@ -0,0 +1,59 @@
#
# Copyright (C) 2017 - 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/>.
class MissingPolicyApplicator
def self.apply_missing_deductions
MissingPolicyApplicator.new.apply_missing_deductions
end
def apply_missing_deductions
recently_missing_submissions.find_in_batches do |submissions|
filtered_submissions = submissions.reject { |s| s.grading_period&.closed? }
filtered_submissions.group_by(&:assignment).each(&method(:apply_missing_deduction))
end
end
private
def recently_missing_submissions
now = Time.zone.now
Submission
.joins(assignment: {course: :late_policy})
.eager_load(:grading_period, assignment: { course: :late_policy })
.missing
.where(score: nil, grade: nil, cached_due_date: 1.day.ago(now)..now,
late_policies: { missing_submission_deduction_enabled: true })
end
# Given submissions must all be for the same assignment
def apply_missing_deduction(assignment, submissions)
score = assignment.course.late_policy.points_for_missing(assignment)
grade = assignment.score_to_grade(score)
# We are using update_all here for performance reasons
# rubocop:disable SkipsModelValidations
Submission.where(id: submissions).update_all(
score: score,
grade: grade,
published_score: score,
published_grade: grade
)
# rubocop:enable SkipsModelValidations
assignment.course.recompute_student_scores(submissions.map(&:user_id).uniq)
end
end

View File

@ -29,14 +29,15 @@ module Factories
private private
def late_policy_params(course: nil, deduct: 0, every: :hour, down_to: 0) def late_policy_params(course: nil, deduct: 0, every: :hour, down_to: 0, missing_submission_deduction: 100)
{ {
course_id: course && course[:id], course_id: course && course[:id],
late_submission_deduction_enabled: deduct.positive?, late_submission_deduction_enabled: deduct.positive?,
late_submission_deduction: deduct, late_submission_deduction: deduct,
late_submission_interval: every, late_submission_interval: every,
late_submission_minimum_percent_enabled: down_to.positive?, late_submission_minimum_percent_enabled: down_to.positive?,
late_submission_minimum_percent: down_to late_submission_minimum_percent: down_to,
missing_submission_deduction: missing_submission_deduction
} }
end end
end end

View File

@ -0,0 +1,160 @@
#
# Copyright (C) 2017 - 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 'spec_helper'
describe MissingPolicyApplicator do
describe '.apply_missing_deductions' do
it 'invokes #apply_missing_deductions' do
dbl = instance_double('MissingPolicyApplicator')
allow(described_class).to receive(:new).and_return(dbl)
expect(dbl).to receive(:apply_missing_deductions)
described_class.apply_missing_deductions
end
end
describe '#apply_missing_deductions' do
let(:now) { Time.zone.now }
let :late_policy_missing_enabled do
LatePolicy.create!(
course_id: @course.id,
missing_submission_deduction_enabled: true,
missing_submission_deduction: 75
)
end
let :late_policy_missing_disabled do
LatePolicy.create!(
course_id: @course.id,
missing_submission_deduction_enabled: false,
missing_submission_deduction: 75
)
end
let :create_recent_assignment do
@course.assignments.create!(
assignment_valid_attributes.merge(grading_type: 'letter_grade', due_at: 1.hour.ago(now))
)
end
let :assignment_old do
@course.assignments.create!(
assignment_valid_attributes.merge(grading_type: 'letter_grade', due_at: 25.hours.ago(now))
)
end
let :create_pass_fail_assignment do
@course.assignments.create!(
assignment_valid_attributes.merge(grading_type: 'pass_fail', due_at: 1.hour.ago(now))
)
end
let(:grading_period_group) do
group = @course.account.grading_period_groups.create!(title: "A Group")
term = @course.enrollment_term
group.enrollment_terms << term
group
end
let(:grading_period_closed) do
grading_period_group.grading_periods.create!(
title: 'A Grading Period',
start_date: 10.days.ago(now),
end_date: 30.minutes.ago(now),
close_date: 30.minutes.ago(now)
)
end
let(:applicator) { described_class.new }
let!(:student) { student_in_course(active_all: true, user_name: 'a student').user }
before(:once) do
course_with_teacher(active_all: true)
end
it 'applies deductions to assignments in a course with a LatePolicy with missing submission deductions enabled' do
late_policy_missing_enabled
create_recent_assignment
applicator.apply_missing_deductions
submission = @course.submissions.first
expect(submission.score).to be 0.375
expect(submission.grade).to eql 'F'
end
it 'does not apply deductions to assignments in a course with missing submission deductions disabled' do
late_policy_missing_disabled
create_recent_assignment
applicator.apply_missing_deductions
submission = @course.submissions.first
expect(submission.score).to be nil
expect(submission.grade).to be nil
end
it 'does not apply deductions to assignments that went missing over 24 hours ago' do
late_policy_missing_enabled
assignment_old
applicator.apply_missing_deductions
submission = @course.submissions.first
expect(submission.score).to be nil
expect(submission.grade).to be nil
end
it 'does not apply deductions to assignments in a course without a LatePolicy' do
create_recent_assignment
applicator.apply_missing_deductions
submission = @course.submissions.first
expect(submission.score).to be nil
expect(submission.grade).to be nil
end
it 'assigns a score of zero to Complete / Incomplete assignments' do
late_policy_missing_enabled
create_pass_fail_assignment
applicator.apply_missing_deductions
submission = @course.submissions.first
expect(submission.score).to be 0.0
expect(submission.grade).to eql 'incomplete'
end
it 'does not apply deductions to submission in closed grading periods' do
grading_period_closed
late_policy_missing_enabled
create_recent_assignment
applicator.apply_missing_deductions
submission = @course.submissions.first
expect(submission.score).to be nil
expect(submission.grade).to be nil
end
it 'recomputes student scores for affected students' do
late_policy_missing_enabled
create_recent_assignment
enrollment = student.enrollments.find_by(course_id: @course.id)
enrollment.scores.first_or_create.update(grading_period_id: nil, final_score: 100, current_score: 100)
expect { applicator.apply_missing_deductions }.to change(enrollment, :computed_final_score)
end
end
end

View File

@ -76,37 +76,37 @@ describe LatePolicy do
describe 'rounding' do describe 'rounding' do
it 'only keeps 2 digits after the decimal for late_submission_minimum_percent' do it 'only keeps 2 digits after the decimal for late_submission_minimum_percent' do
policy = LatePolicy.new(late_submission_minimum_percent: 100.223) policy = LatePolicy.new(late_submission_minimum_percent: 25.223)
expect(policy.late_submission_minimum_percent).to eql BigDecimal.new('100.22') expect(policy.late_submission_minimum_percent).to eql BigDecimal.new('25.22')
end end
it 'rounds late_submission_minimum_percent' do it 'rounds late_submission_minimum_percent' do
policy = LatePolicy.new(late_submission_minimum_percent: 100.225) policy = LatePolicy.new(late_submission_minimum_percent: 25.225)
expect(policy.late_submission_minimum_percent).to eql BigDecimal.new('100.23') expect(policy.late_submission_minimum_percent).to eql BigDecimal.new('25.23')
end end
it 'only keeps 2 digits after the decimal for missing_submission_deduction' do it 'only keeps 2 digits after the decimal for missing_submission_deduction' do
policy = LatePolicy.new(missing_submission_deduction: 100.223) policy = LatePolicy.new(missing_submission_deduction: 25.223)
expect(policy.missing_submission_deduction).to eql BigDecimal.new('100.22') expect(policy.missing_submission_deduction).to eql BigDecimal.new('25.22')
end end
it 'rounds missing_submission_deduction' do it 'rounds missing_submission_deduction' do
policy = LatePolicy.new(missing_submission_deduction: 100.225) policy = LatePolicy.new(missing_submission_deduction: 25.225)
expect(policy.missing_submission_deduction).to eql BigDecimal.new('100.23') expect(policy.missing_submission_deduction).to eql BigDecimal.new('25.23')
end end
it 'only keeps 2 digits after the decimal for late_submission_deduction' do it 'only keeps 2 digits after the decimal for late_submission_deduction' do
policy = LatePolicy.new(late_submission_deduction: 100.223) policy = LatePolicy.new(late_submission_deduction: 25.223)
expect(policy.late_submission_deduction).to eql BigDecimal.new('100.22') expect(policy.late_submission_deduction).to eql BigDecimal.new('25.22')
end end
it 'rounds late_submission_deduction' do it 'rounds late_submission_deduction' do
policy = LatePolicy.new(late_submission_deduction: 100.225) policy = LatePolicy.new(late_submission_deduction: 25.225)
expect(policy.late_submission_deduction).to eql BigDecimal.new('100.23') expect(policy.late_submission_deduction).to eql BigDecimal.new('25.23')
end end
end end
describe 'deduction' do describe '#points_deducted' do
it 'ignores disabled late submission deduction' do it 'ignores disabled late submission deduction' do
expect(late_policy_model.points_deducted(score: 500, possible: 1000, late_for: 1.day)).to eq 0 expect(late_policy_model.points_deducted(score: 500, possible: 1000, late_for: 1.day)).to eq 0
end end
@ -144,6 +144,45 @@ describe LatePolicy do
end end
end end
describe '#points_for_missing' do
it 'returns 0 when assignment grading_type is pass_fail' do
policy = late_policy_model
assignment = instance_double('Assignment')
allow(assignment).to receive(:grading_type).and_return('pass_fail')
expect(policy.points_for_missing(assignment)).to eq(0)
end
it 'computes expected value' do
policy = late_policy_model(missing_submission_deduction: 60)
assignment = instance_double('Assignment')
allow(assignment).to receive(:grading_type).and_return('foo')
allow(assignment).to receive(:points_possible).and_return(100)
expect(policy.points_for_missing(assignment)).to eq(40)
end
end
describe '#missing_points_deducted' do
it 'returns points_possible when assignment grading_type is pass_fail' do
policy = late_policy_model
assignment = instance_double('Assignment')
allow(assignment).to receive(:grading_type).and_return('pass_fail')
allow(assignment).to receive(:points_possible).and_return(100)
expect(policy.missing_points_deducted(assignment)).to eq(100)
end
it 'computes expected value' do
policy = late_policy_model(missing_submission_deduction: 60)
assignment = instance_double('Assignment')
allow(assignment).to receive(:grading_type).and_return('foo')
allow(assignment).to receive(:points_possible).and_return(100)
expect(policy.missing_points_deducted(assignment)).to eq(60)
end
end
describe '#update_late_submissions' do describe '#update_late_submissions' do
before :once do before :once do
@course = Course.create(name: 'Late Policy Course') @course = Course.create(name: 'Late Policy Course')