canvas-lms/lib/due_date_cacher.rb

445 lines
19 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
#
# Copyright (C) 2013 - 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 'anonymity'
replace submissions.late column with .cached_due_date refs CNVS-5805 with efficient calculation of all due dates for any submissions for a given assignment when related records (the assignment, its overrides, related enrollments, and related group memberships) changes. compares this cached due date to the submitted_at or current time when determining lateness. populates the column for existing submissions in a post-deploy data-fixup migration. test-plan: - run lib/data_fixup/initialize_submission_cached_due_date.rb - all submissions' cached_due_dates should be updated over several jobs - enroll a student in a course and create submissions in that course - create a second enrollment in a second section; the cached_due_dates for the user's submissions should recalculate - destroy the second enrollment; the cached_due_dates for the user's submissions should recalculate - create a group assignment - add the student to a group in the assignment's category; the cached_due_dates for the user's submissions should recalculate - remove the student from the group; the cached_due_dates for the user's submissions should recalculate - enroll more students in the course - change an assignment's due date; the cached_due_dates for the assignment's submissions should recalculate - create an override for the assignment; the cached_due_dates for the assignment's submissions should recalculate - change the due date on the override; the cached_due_dates for the assignment's submissions should recalculate - delete the override; the cached_due_dates for the assignment's submissions should recalculate - during any of the above recalculations: - the most lenient applicable override should apply - if the most lenient applicable override is more stringent than the assignment due_at, it should still apply - the assignment due_at should apply if there are no applicable overrides Change-Id: Ibacab27429a76755114dabb1e735d4b3d9bbd2fc Reviewed-on: https://gerrit.instructure.com/21123 Reviewed-by: Brian Palmer <brianp@instructure.com> Product-Review: Jacob Fugal <jacob@instructure.com> QA-Review: Jacob Fugal <jacob@instructure.com> Tested-by: Jacob Fugal <jacob@instructure.com>
2013-06-01 04:07:26 +08:00
class DueDateCacher
include Moderation
thread_mattr_accessor :executing_users, instance_accessor: false
# These methods allow the caller to specify a user to whom due date
# changes should be attributed (currently this is used for creating
# audit events for anonymous or moderated assignments), and are meant
# to be used when DueDateCacher is invoked in a callback or a similar
# place where directly specifying an executing user is impractical.
#
# DueDateCacher.with_executing_user(a_user) do
# # do something to update due dates, like saving an assignment override
# # any DDC calls that occur while an executing user is set will
# # attribute changes to that user
# end
#
# Users are stored on a stack, so nested calls will work as expected.
# A value of nil may also be passed to indicate that no user should be
# credited (in which case audit events will not be recorded).
#
# You may also specify a user explicitly when calling the class methods:
# DueDateCacher.recompute(assignment, update_grades: true, executing_user: a_user)
#
# An explicitly specified user will take precedence over any users specified
# via with_executing_user, but will not otherwise affect the current "stack"
# of executing users.
#
# If you are calling DueDateCacher in a delayed job of your own making (e.g.,
# Assignment#run_if_overrides_changed_later!), you should pass a user
# explicitly rather than relying on the user stored in with_executing_user
# at the time you create the delayed job.
def self.with_executing_user(user)
self.executing_users ||= []
self.executing_users.push(user)
begin
result = yield
ensure
self.executing_users.pop
end
result
end
def self.current_executing_user
self.executing_users ||= []
self.executing_users.last
end
INFER_SUBMISSION_WORKFLOW_STATE_SQL = <<~SQL_FRAGMENT
CASE
WHEN grade IS NOT NULL OR excused IS TRUE THEN
'graded'
WHEN submission_type = 'online_quiz' AND quiz_submission_id IS NOT NULL THEN
'pending_review'
WHEN submission_type IS NOT NULL AND submitted_at IS NOT NULL THEN
'submitted'
ELSE
'unsubmitted'
END
SQL_FRAGMENT
def self.recompute(assignment, update_grades: false, executing_user: nil)
current_caller = caller(1..1).first
Rails.logger.debug "DDC.recompute(#{assignment&.id}) - #{current_caller}"
return unless assignment.persisted? && assignment.active?
# We use a strand here instead of a singleton because a bunch of
# assignment updates with upgrade_grades could end up causing
# score table fights.
change delayed job behavior when grading period is updated If a grading period or grading period group is updated, we were previously creating a delayed-job-per-course for grade recalculation and a delayed-job-per-course for DueDateCacher recalculation, with no limit on how many of those jobs could be run in parallel. Now, we create a delayed-job-per-1000-courses for grade recalculation, and a delayed-job-per-1000-courses for DueDateCacher recalculation, and the number of jobs that can be run in parallel are limited with an n_strand. closes GRADE-805 Test Plan: 1. Verify cached due dates and scores (grading period and overall) are recalulated when: a) Creating, updating, and deleting a Grading Period Set. b) Creating, updating, and deleting a Grading Period. 2. When creating/updating a Grading Period or a Grading Period Group such that a score + due date recalculation occurs, verify: a) Enrollment term delayed jobs (EnrollmentTerm#recompute_scores_for_batch) are being stranded by GradingPeriodGroup, and a job is being created for each 1000 courses that need to be operated on in that term. b) Score recalculation delayed jobs are not being triggered (because the score recalculation is happening in the delayed job triggered in step a above). c) Due date recalculation delayed jobs are not being triggered (because the due date recalculation is happening in the delayed job triggered in step a above). Change-Id: I99610d0559a449ad08b9209646490f7fa1cdf33b Reviewed-on: https://gerrit.instructure.com/138508 Reviewed-by: Shahbaz Javeed <sjaveed@instructure.com> Tested-by: Jenkins Reviewed-by: Derek Bender <djbender@instructure.com> Product-Review: Keith T. Garner <kgarner@instructure.com> QA-Review: Keith T. Garner <kgarner@instructure.com>
2018-01-20 04:56:28 +08:00
opts = {
assignments: [assignment.id],
inst_jobs_opts: {
strand: "cached_due_date:calculator:Course:Assignments:#{assignment.context.global_id}",
max_attempts: 10
fix stale grades when re-assigning students to assignment closes GRADE-819 The specific case was to unassign a graded student from an assignment by removing "Everyone" and adding all students except one to the assignment. Then remove all students and assign "Everyone" to the assignment. This should make the previously unassigned student's grades stale. test plan: * Configure delayed jobs to use 6 simultaneously running workers by editing config/delayed_jobs.yml to look like this: default: workers: - queue: canvas_queue workers: 6 Notice the line that says "workers: 6" * Restart the jobs container so all six new workers are started * Create a published course with one assignment worth 150 points and two students enrolled * For the remainder of these steps, let's assume unique identifer for the course is "course_id" and the unique identifier for the student is "student_id" * Go to the Gradebook and grade the first student at 125 points and wait for their score to be updated in the gradebook. * Verify the score in the Gradebook is 83.33% * In a separate tab, visit the following URL, making sure to replace course_id with the actual course id and student_id with the actual student id: /api/v1/courses/course_id/enrollments?user_id=student_id * Verify that you see the student's current_score as 83.33 * Repeat the following steps multiple times to ensure the problem does not manifest itself: - Modify the assignment: unassign it from the first student and only assign it to the second student only - Go to the gradebook and verify the cells for the first student are not editable any more - Go back to the API URL above and verify the student's current_score now says null - Modify the assignment: re-assign it to "Everyone" - Go to the gradebook and verify the cells for the first student are now editable again - Go back to the API URL above and verify the student's current_score now says 83.33 again. If it ever says null at this point, there is a problem. Change-Id: Ifaaf0609dfe5081697c1939db1b4a4e0a3e05bad Reviewed-on: https://gerrit.instructure.com/141049 Reviewed-by: Keith T. Garner <kgarner@instructure.com> Reviewed-by: Derek Bender <djbender@instructure.com> Tested-by: Jenkins Product-Review: Keith T. Garner <kgarner@instructure.com> QA-Review: Keith T. Garner <kgarner@instructure.com>
2018-02-15 23:28:07 +08:00
},
update_grades: update_grades,
original_caller: current_caller,
executing_user: executing_user
change delayed job behavior when grading period is updated If a grading period or grading period group is updated, we were previously creating a delayed-job-per-course for grade recalculation and a delayed-job-per-course for DueDateCacher recalculation, with no limit on how many of those jobs could be run in parallel. Now, we create a delayed-job-per-1000-courses for grade recalculation, and a delayed-job-per-1000-courses for DueDateCacher recalculation, and the number of jobs that can be run in parallel are limited with an n_strand. closes GRADE-805 Test Plan: 1. Verify cached due dates and scores (grading period and overall) are recalulated when: a) Creating, updating, and deleting a Grading Period Set. b) Creating, updating, and deleting a Grading Period. 2. When creating/updating a Grading Period or a Grading Period Group such that a score + due date recalculation occurs, verify: a) Enrollment term delayed jobs (EnrollmentTerm#recompute_scores_for_batch) are being stranded by GradingPeriodGroup, and a job is being created for each 1000 courses that need to be operated on in that term. b) Score recalculation delayed jobs are not being triggered (because the score recalculation is happening in the delayed job triggered in step a above). c) Due date recalculation delayed jobs are not being triggered (because the due date recalculation is happening in the delayed job triggered in step a above). Change-Id: I99610d0559a449ad08b9209646490f7fa1cdf33b Reviewed-on: https://gerrit.instructure.com/138508 Reviewed-by: Shahbaz Javeed <sjaveed@instructure.com> Tested-by: Jenkins Reviewed-by: Derek Bender <djbender@instructure.com> Product-Review: Keith T. Garner <kgarner@instructure.com> QA-Review: Keith T. Garner <kgarner@instructure.com>
2018-01-20 04:56:28 +08:00
}
recompute_course(assignment.context, **opts)
end
def self.recompute_course(course, assignments: nil, inst_jobs_opts: {}, run_immediately: false, update_grades: false, original_caller: caller(1..1).first, executing_user: nil)
Rails.logger.debug "DDC.recompute_course(#{course.inspect}, #{assignments.inspect}, #{inst_jobs_opts.inspect}) - #{original_caller}"
course = Course.find(course) unless course.is_a?(Course)
inst_jobs_opts[:max_attempts] ||= 10
inst_jobs_opts[:singleton] ||= "cached_due_date:calculator:Course:#{course.global_id}" if assignments.nil? && !inst_jobs_opts[:strand]
change delayed job behavior when grading period is updated If a grading period or grading period group is updated, we were previously creating a delayed-job-per-course for grade recalculation and a delayed-job-per-course for DueDateCacher recalculation, with no limit on how many of those jobs could be run in parallel. Now, we create a delayed-job-per-1000-courses for grade recalculation, and a delayed-job-per-1000-courses for DueDateCacher recalculation, and the number of jobs that can be run in parallel are limited with an n_strand. closes GRADE-805 Test Plan: 1. Verify cached due dates and scores (grading period and overall) are recalulated when: a) Creating, updating, and deleting a Grading Period Set. b) Creating, updating, and deleting a Grading Period. 2. When creating/updating a Grading Period or a Grading Period Group such that a score + due date recalculation occurs, verify: a) Enrollment term delayed jobs (EnrollmentTerm#recompute_scores_for_batch) are being stranded by GradingPeriodGroup, and a job is being created for each 1000 courses that need to be operated on in that term. b) Score recalculation delayed jobs are not being triggered (because the score recalculation is happening in the delayed job triggered in step a above). c) Due date recalculation delayed jobs are not being triggered (because the due date recalculation is happening in the delayed job triggered in step a above). Change-Id: I99610d0559a449ad08b9209646490f7fa1cdf33b Reviewed-on: https://gerrit.instructure.com/138508 Reviewed-by: Shahbaz Javeed <sjaveed@instructure.com> Tested-by: Jenkins Reviewed-by: Derek Bender <djbender@instructure.com> Product-Review: Keith T. Garner <kgarner@instructure.com> QA-Review: Keith T. Garner <kgarner@instructure.com>
2018-01-20 04:56:28 +08:00
assignments_to_recompute = assignments || Assignment.active.where(context: course).pluck(:id)
return if assignments_to_recompute.empty?
executing_user ||= self.current_executing_user
due_date_cacher = new(course, assignments_to_recompute, update_grades: update_grades, original_caller: original_caller, executing_user: executing_user)
change delayed job behavior when grading period is updated If a grading period or grading period group is updated, we were previously creating a delayed-job-per-course for grade recalculation and a delayed-job-per-course for DueDateCacher recalculation, with no limit on how many of those jobs could be run in parallel. Now, we create a delayed-job-per-1000-courses for grade recalculation, and a delayed-job-per-1000-courses for DueDateCacher recalculation, and the number of jobs that can be run in parallel are limited with an n_strand. closes GRADE-805 Test Plan: 1. Verify cached due dates and scores (grading period and overall) are recalulated when: a) Creating, updating, and deleting a Grading Period Set. b) Creating, updating, and deleting a Grading Period. 2. When creating/updating a Grading Period or a Grading Period Group such that a score + due date recalculation occurs, verify: a) Enrollment term delayed jobs (EnrollmentTerm#recompute_scores_for_batch) are being stranded by GradingPeriodGroup, and a job is being created for each 1000 courses that need to be operated on in that term. b) Score recalculation delayed jobs are not being triggered (because the score recalculation is happening in the delayed job triggered in step a above). c) Due date recalculation delayed jobs are not being triggered (because the due date recalculation is happening in the delayed job triggered in step a above). Change-Id: I99610d0559a449ad08b9209646490f7fa1cdf33b Reviewed-on: https://gerrit.instructure.com/138508 Reviewed-by: Shahbaz Javeed <sjaveed@instructure.com> Tested-by: Jenkins Reviewed-by: Derek Bender <djbender@instructure.com> Product-Review: Keith T. Garner <kgarner@instructure.com> QA-Review: Keith T. Garner <kgarner@instructure.com>
2018-01-20 04:56:28 +08:00
if run_immediately
due_date_cacher.recompute
else
due_date_cacher.delay_if_production(**inst_jobs_opts).recompute
change delayed job behavior when grading period is updated If a grading period or grading period group is updated, we were previously creating a delayed-job-per-course for grade recalculation and a delayed-job-per-course for DueDateCacher recalculation, with no limit on how many of those jobs could be run in parallel. Now, we create a delayed-job-per-1000-courses for grade recalculation, and a delayed-job-per-1000-courses for DueDateCacher recalculation, and the number of jobs that can be run in parallel are limited with an n_strand. closes GRADE-805 Test Plan: 1. Verify cached due dates and scores (grading period and overall) are recalulated when: a) Creating, updating, and deleting a Grading Period Set. b) Creating, updating, and deleting a Grading Period. 2. When creating/updating a Grading Period or a Grading Period Group such that a score + due date recalculation occurs, verify: a) Enrollment term delayed jobs (EnrollmentTerm#recompute_scores_for_batch) are being stranded by GradingPeriodGroup, and a job is being created for each 1000 courses that need to be operated on in that term. b) Score recalculation delayed jobs are not being triggered (because the score recalculation is happening in the delayed job triggered in step a above). c) Due date recalculation delayed jobs are not being triggered (because the due date recalculation is happening in the delayed job triggered in step a above). Change-Id: I99610d0559a449ad08b9209646490f7fa1cdf33b Reviewed-on: https://gerrit.instructure.com/138508 Reviewed-by: Shahbaz Javeed <sjaveed@instructure.com> Tested-by: Jenkins Reviewed-by: Derek Bender <djbender@instructure.com> Product-Review: Keith T. Garner <kgarner@instructure.com> QA-Review: Keith T. Garner <kgarner@instructure.com>
2018-01-20 04:56:28 +08:00
end
replace submissions.late column with .cached_due_date refs CNVS-5805 with efficient calculation of all due dates for any submissions for a given assignment when related records (the assignment, its overrides, related enrollments, and related group memberships) changes. compares this cached due date to the submitted_at or current time when determining lateness. populates the column for existing submissions in a post-deploy data-fixup migration. test-plan: - run lib/data_fixup/initialize_submission_cached_due_date.rb - all submissions' cached_due_dates should be updated over several jobs - enroll a student in a course and create submissions in that course - create a second enrollment in a second section; the cached_due_dates for the user's submissions should recalculate - destroy the second enrollment; the cached_due_dates for the user's submissions should recalculate - create a group assignment - add the student to a group in the assignment's category; the cached_due_dates for the user's submissions should recalculate - remove the student from the group; the cached_due_dates for the user's submissions should recalculate - enroll more students in the course - change an assignment's due date; the cached_due_dates for the assignment's submissions should recalculate - create an override for the assignment; the cached_due_dates for the assignment's submissions should recalculate - change the due date on the override; the cached_due_dates for the assignment's submissions should recalculate - delete the override; the cached_due_dates for the assignment's submissions should recalculate - during any of the above recalculations: - the most lenient applicable override should apply - if the most lenient applicable override is more stringent than the assignment due_at, it should still apply - the assignment due_at should apply if there are no applicable overrides Change-Id: Ibacab27429a76755114dabb1e735d4b3d9bbd2fc Reviewed-on: https://gerrit.instructure.com/21123 Reviewed-by: Brian Palmer <brianp@instructure.com> Product-Review: Jacob Fugal <jacob@instructure.com> QA-Review: Jacob Fugal <jacob@instructure.com> Tested-by: Jacob Fugal <jacob@instructure.com>
2013-06-01 04:07:26 +08:00
end
def self.recompute_users_for_course(user_ids, course, assignments = nil, inst_jobs_opts = {})
user_ids = Array(user_ids)
course = Course.find(course) unless course.is_a?(Course)
inst_jobs_opts[:max_attempts] ||= 10
if assignments.nil?
inst_jobs_opts[:singleton] ||= "cached_due_date:calculator:Users:#{course.global_id}:#{Digest::SHA256.hexdigest(user_ids.sort.join(':'))}"
end
assignments ||= Assignment.active.where(context: course).pluck(:id)
return if assignments.empty?
fix stale grades when re-assigning students to assignment closes GRADE-819 The specific case was to unassign a graded student from an assignment by removing "Everyone" and adding all students except one to the assignment. Then remove all students and assign "Everyone" to the assignment. This should make the previously unassigned student's grades stale. test plan: * Configure delayed jobs to use 6 simultaneously running workers by editing config/delayed_jobs.yml to look like this: default: workers: - queue: canvas_queue workers: 6 Notice the line that says "workers: 6" * Restart the jobs container so all six new workers are started * Create a published course with one assignment worth 150 points and two students enrolled * For the remainder of these steps, let's assume unique identifer for the course is "course_id" and the unique identifier for the student is "student_id" * Go to the Gradebook and grade the first student at 125 points and wait for their score to be updated in the gradebook. * Verify the score in the Gradebook is 83.33% * In a separate tab, visit the following URL, making sure to replace course_id with the actual course id and student_id with the actual student id: /api/v1/courses/course_id/enrollments?user_id=student_id * Verify that you see the student's current_score as 83.33 * Repeat the following steps multiple times to ensure the problem does not manifest itself: - Modify the assignment: unassign it from the first student and only assign it to the second student only - Go to the gradebook and verify the cells for the first student are not editable any more - Go back to the API URL above and verify the student's current_score now says null - Modify the assignment: re-assign it to "Everyone" - Go to the gradebook and verify the cells for the first student are now editable again - Go back to the API URL above and verify the student's current_score now says 83.33 again. If it ever says null at this point, there is a problem. Change-Id: Ifaaf0609dfe5081697c1939db1b4a4e0a3e05bad Reviewed-on: https://gerrit.instructure.com/141049 Reviewed-by: Keith T. Garner <kgarner@instructure.com> Reviewed-by: Derek Bender <djbender@instructure.com> Tested-by: Jenkins Product-Review: Keith T. Garner <kgarner@instructure.com> QA-Review: Keith T. Garner <kgarner@instructure.com>
2018-02-15 23:28:07 +08:00
current_caller = caller(1..1).first
update_grades = inst_jobs_opts.delete(:update_grades) || false
executing_user = inst_jobs_opts.delete(:executing_user) || self.current_executing_user
due_date_cacher = new(course, assignments, user_ids, update_grades: update_grades, original_caller: current_caller, executing_user: executing_user)
due_date_cacher.delay_if_production(**inst_jobs_opts).recompute
end
def initialize(course, assignments, user_ids = [], update_grades: false, original_caller: caller(1..1).first, executing_user: nil)
@course = course
@assignment_ids = Array(assignments).map { |a| a.is_a?(Assignment) ? a.id : a }
@assignments_auditable_by_id = if @assignment_ids.present?
Set.new(Assignment.auditable.where(id: @assignment_ids).pluck(:id))
else
Set.new
end
@user_ids = Array(user_ids)
fix stale grades when re-assigning students to assignment closes GRADE-819 The specific case was to unassign a graded student from an assignment by removing "Everyone" and adding all students except one to the assignment. Then remove all students and assign "Everyone" to the assignment. This should make the previously unassigned student's grades stale. test plan: * Configure delayed jobs to use 6 simultaneously running workers by editing config/delayed_jobs.yml to look like this: default: workers: - queue: canvas_queue workers: 6 Notice the line that says "workers: 6" * Restart the jobs container so all six new workers are started * Create a published course with one assignment worth 150 points and two students enrolled * For the remainder of these steps, let's assume unique identifer for the course is "course_id" and the unique identifier for the student is "student_id" * Go to the Gradebook and grade the first student at 125 points and wait for their score to be updated in the gradebook. * Verify the score in the Gradebook is 83.33% * In a separate tab, visit the following URL, making sure to replace course_id with the actual course id and student_id with the actual student id: /api/v1/courses/course_id/enrollments?user_id=student_id * Verify that you see the student's current_score as 83.33 * Repeat the following steps multiple times to ensure the problem does not manifest itself: - Modify the assignment: unassign it from the first student and only assign it to the second student only - Go to the gradebook and verify the cells for the first student are not editable any more - Go back to the API URL above and verify the student's current_score now says null - Modify the assignment: re-assign it to "Everyone" - Go to the gradebook and verify the cells for the first student are now editable again - Go back to the API URL above and verify the student's current_score now says 83.33 again. If it ever says null at this point, there is a problem. Change-Id: Ifaaf0609dfe5081697c1939db1b4a4e0a3e05bad Reviewed-on: https://gerrit.instructure.com/141049 Reviewed-by: Keith T. Garner <kgarner@instructure.com> Reviewed-by: Derek Bender <djbender@instructure.com> Tested-by: Jenkins Product-Review: Keith T. Garner <kgarner@instructure.com> QA-Review: Keith T. Garner <kgarner@instructure.com>
2018-02-15 23:28:07 +08:00
@update_grades = update_grades
@original_caller = original_caller
if executing_user.present?
@executing_user_id = executing_user.is_a?(User) ? executing_user.id : executing_user
end
replace submissions.late column with .cached_due_date refs CNVS-5805 with efficient calculation of all due dates for any submissions for a given assignment when related records (the assignment, its overrides, related enrollments, and related group memberships) changes. compares this cached due date to the submitted_at or current time when determining lateness. populates the column for existing submissions in a post-deploy data-fixup migration. test-plan: - run lib/data_fixup/initialize_submission_cached_due_date.rb - all submissions' cached_due_dates should be updated over several jobs - enroll a student in a course and create submissions in that course - create a second enrollment in a second section; the cached_due_dates for the user's submissions should recalculate - destroy the second enrollment; the cached_due_dates for the user's submissions should recalculate - create a group assignment - add the student to a group in the assignment's category; the cached_due_dates for the user's submissions should recalculate - remove the student from the group; the cached_due_dates for the user's submissions should recalculate - enroll more students in the course - change an assignment's due date; the cached_due_dates for the assignment's submissions should recalculate - create an override for the assignment; the cached_due_dates for the assignment's submissions should recalculate - change the due date on the override; the cached_due_dates for the assignment's submissions should recalculate - delete the override; the cached_due_dates for the assignment's submissions should recalculate - during any of the above recalculations: - the most lenient applicable override should apply - if the most lenient applicable override is more stringent than the assignment due_at, it should still apply - the assignment due_at should apply if there are no applicable overrides Change-Id: Ibacab27429a76755114dabb1e735d4b3d9bbd2fc Reviewed-on: https://gerrit.instructure.com/21123 Reviewed-by: Brian Palmer <brianp@instructure.com> Product-Review: Jacob Fugal <jacob@instructure.com> QA-Review: Jacob Fugal <jacob@instructure.com> Tested-by: Jacob Fugal <jacob@instructure.com>
2013-06-01 04:07:26 +08:00
end
def recompute
Rails.logger.debug "DUE DATE CACHER STARTS: #{Time.zone.now.to_i}"
Rails.logger.debug "DDC#recompute() - original caller: #{@original_caller}"
Rails.logger.debug "DDC#recompute() - current caller: #{caller(1..1).first}"
add anonymous_id to due date cacher to upsert Relies on base58, which is rad because you can't mistake 0 (zero) for uppercase O (oh) and uppercase I (eye) for lowercase l (el). The other added benefit is some systems will interpret a prefixed 0 has indicating a different literal (e.g. hex is `0x` prefixed). This avoids the problem by never using zero. log2(58)*5 = ~29.28 bits (round up to 30 bits of entropy) 58**5 = 656,356,768 combinations For two generations there's a 1 in 656,356,768 chance of collision. In the case of a collision, we'll loop until an unused one is generated. Performance for this probably gets kinda bad once the assignment is half full. However if we have >300,000,000 submissions the app has probably already fallen over. closes: GRADE-893 Test Plan: 1. Given a course 2. Given a student enrolled in the course 3. When creating the first assignment (e.g. 'Alpha') 4. Then all submissions have anonymous_ids: anonymous_ids = Assignment.find_by!(title: 'Alpha'). all_submissions.pluck(:anonymous_id) 5. When Alpha has it's due date changed via the assignment edit page 6. Then none of the anonymous_ids have changed: anonymous_ids.sort! == Assignment.find_by!(title: 'Alpha'). all_submissions.pluck(:anonymous_id).sort! 7. Given a second assignment (e.g. 'Omega') 8. Given the manual deletion of anonymous_ids for the Omega assignment: omega = Assignment.find_by!(title: 'Omega') omega.all_submissions.update_all(anonymous_id: nil) 9. When DueDateCacher is triggered by changing the due date 10.Then all submissions in Omega now have anonymous_ids: omega.all_submissions.reload.pluck(:anonymous_id) 11.When validating a new submission in the console: submission = omega.submissions.build submission.validate # => false 12.Then anonymous_id is present: submission.anonymous_id # e.g. "aB123" Change-Id: I874ba5b0d6025c95af2f832c3cc5d83c3cbd20e7 Reviewed-on: https://gerrit.instructure.com/143314 Tested-by: Jenkins Reviewed-by: Keith T. Garner <kgarner@instructure.com> QA-Review: Indira Pai <ipai@instructure.com> Product-Review: Keith T. Garner <kgarner@instructure.com>
2018-03-13 04:05:42 +08:00
# in a transaction on the correct shard:
@course.shard.activate do
values = []
assignments_by_id = Assignment.find(@assignment_ids).index_by(&:id)
ignore concluded enrollments in due date cacher closes GRADE-269 QA Notes: To run the due date cacher, open a Rails console and run: assignment = Assignment.find(<id of assignment>) DueDateCacher.recompute(assignment) Check cached due dates for submissions in the Rails console. submission = Submission.find_by( user_id: <id of student>, assignment_id: <id of assignment> ) submission.cached_due_date test plan: A. Setup 1. ensure the delayed jobs are not running 2. create a course with one assignment 3. enroll multiple students 4. assign the assignment to everyone B. Student without a Submission 1. enroll a student in the course 2. conclude the student's enrollment 3. manually run the due date cacher 4. confirm the student does not have any submissions C. Changing a Due Date 1. enroll a student in the course 2. manually run the due date cacher 3. conclude the student's enrollment 4. change the due date on the assignment 5. manually run the due date cacher 6. confirm the student submission exists 7. confirm the submission has the previous due date cached D. Unassigning an Assignment 1. enroll a student in the course 2. manually run the due date cacher 3. conclude the student's enrollment 4. create overrides for only the active students 5. make the assignment due only to overrides 6. manually run the due date cacher 7. confirm the student submission exists 8. confirm the submission has the previous due date cached Change-Id: I5e7165c0120e5c87635da1fbbe47501970874653 Reviewed-on: https://gerrit.instructure.com/126270 Tested-by: Jenkins Reviewed-by: Matt Taylor <mtaylor@instructure.com> Reviewed-by: Keith T. Garner <kgarner@instructure.com> QA-Review: KC Naegle <knaegle@instructure.com> Product-Review: Spencer Olson <solson@instructure.com>
2017-09-15 03:19:56 +08:00
effective_due_dates.to_hash.each do |assignment_id, student_due_dates|
add anonymous_id to due date cacher to upsert Relies on base58, which is rad because you can't mistake 0 (zero) for uppercase O (oh) and uppercase I (eye) for lowercase l (el). The other added benefit is some systems will interpret a prefixed 0 has indicating a different literal (e.g. hex is `0x` prefixed). This avoids the problem by never using zero. log2(58)*5 = ~29.28 bits (round up to 30 bits of entropy) 58**5 = 656,356,768 combinations For two generations there's a 1 in 656,356,768 chance of collision. In the case of a collision, we'll loop until an unused one is generated. Performance for this probably gets kinda bad once the assignment is half full. However if we have >300,000,000 submissions the app has probably already fallen over. closes: GRADE-893 Test Plan: 1. Given a course 2. Given a student enrolled in the course 3. When creating the first assignment (e.g. 'Alpha') 4. Then all submissions have anonymous_ids: anonymous_ids = Assignment.find_by!(title: 'Alpha'). all_submissions.pluck(:anonymous_id) 5. When Alpha has it's due date changed via the assignment edit page 6. Then none of the anonymous_ids have changed: anonymous_ids.sort! == Assignment.find_by!(title: 'Alpha'). all_submissions.pluck(:anonymous_id).sort! 7. Given a second assignment (e.g. 'Omega') 8. Given the manual deletion of anonymous_ids for the Omega assignment: omega = Assignment.find_by!(title: 'Omega') omega.all_submissions.update_all(anonymous_id: nil) 9. When DueDateCacher is triggered by changing the due date 10.Then all submissions in Omega now have anonymous_ids: omega.all_submissions.reload.pluck(:anonymous_id) 11.When validating a new submission in the console: submission = omega.submissions.build submission.validate # => false 12.Then anonymous_id is present: submission.anonymous_id # e.g. "aB123" Change-Id: I874ba5b0d6025c95af2f832c3cc5d83c3cbd20e7 Reviewed-on: https://gerrit.instructure.com/143314 Tested-by: Jenkins Reviewed-by: Keith T. Garner <kgarner@instructure.com> QA-Review: Indira Pai <ipai@instructure.com> Product-Review: Keith T. Garner <kgarner@instructure.com>
2018-03-13 04:05:42 +08:00
students_without_priors = student_due_dates.keys - enrollment_counts.prior_student_ids
existing_anonymous_ids = existing_anonymous_ids_by_assignment_id[assignment_id]
add anonymous_id to due date cacher to upsert Relies on base58, which is rad because you can't mistake 0 (zero) for uppercase O (oh) and uppercase I (eye) for lowercase l (el). The other added benefit is some systems will interpret a prefixed 0 has indicating a different literal (e.g. hex is `0x` prefixed). This avoids the problem by never using zero. log2(58)*5 = ~29.28 bits (round up to 30 bits of entropy) 58**5 = 656,356,768 combinations For two generations there's a 1 in 656,356,768 chance of collision. In the case of a collision, we'll loop until an unused one is generated. Performance for this probably gets kinda bad once the assignment is half full. However if we have >300,000,000 submissions the app has probably already fallen over. closes: GRADE-893 Test Plan: 1. Given a course 2. Given a student enrolled in the course 3. When creating the first assignment (e.g. 'Alpha') 4. Then all submissions have anonymous_ids: anonymous_ids = Assignment.find_by!(title: 'Alpha'). all_submissions.pluck(:anonymous_id) 5. When Alpha has it's due date changed via the assignment edit page 6. Then none of the anonymous_ids have changed: anonymous_ids.sort! == Assignment.find_by!(title: 'Alpha'). all_submissions.pluck(:anonymous_id).sort! 7. Given a second assignment (e.g. 'Omega') 8. Given the manual deletion of anonymous_ids for the Omega assignment: omega = Assignment.find_by!(title: 'Omega') omega.all_submissions.update_all(anonymous_id: nil) 9. When DueDateCacher is triggered by changing the due date 10.Then all submissions in Omega now have anonymous_ids: omega.all_submissions.reload.pluck(:anonymous_id) 11.When validating a new submission in the console: submission = omega.submissions.build submission.validate # => false 12.Then anonymous_id is present: submission.anonymous_id # e.g. "aB123" Change-Id: I874ba5b0d6025c95af2f832c3cc5d83c3cbd20e7 Reviewed-on: https://gerrit.instructure.com/143314 Tested-by: Jenkins Reviewed-by: Keith T. Garner <kgarner@instructure.com> QA-Review: Indira Pai <ipai@instructure.com> Product-Review: Keith T. Garner <kgarner@instructure.com>
2018-03-13 04:05:42 +08:00
create_moderation_selections_for_assignment(assignments_by_id[assignment_id], student_due_dates.keys, @user_ids)
quiz_lti = quiz_lti_assignments.include?(assignment_id)
cache quiz_lti on submissions This caches/denormalizes the quiz_lti? information from assignments onto submissions. This makes queries for missing/late Quizzes.Next quizzes to be easier without having to join three tables. (Future ticket coming to make this happen.) closes GRADE-2231 test plan: - Have a course with a student and a Quizzes.Next assignment and another non-Q.N assignment. (You can fake the quizzes next assignment via the rails console) - On the rails console check the submission object for each assignment. Confirm that the quizzes.next assignment has cached_quiz_lti set to true and that the other assignment has it set to false - Create another assignment with on paper. Save. - Switch it to a Quizzes.Next assignment. - On the rails console check the submission object for each assignment. Confirm that the new quizzes.next assignment has cached_quiz_lti set to true - For the new assignment, in the rails console, find the ContentTag and .destroy it - On the rails console check the submission object for each assignment. Confirm that the new quizzes.next assignment has cached_quiz_lti set to false Change-Id: I4bfc1d3a282863dde9de9658b335b9a24b2341e5 Reviewed-on: https://gerrit.instructure.com/197811 QA-Review: James Butters <jbutters@instructure.com> Tested-by: Jenkins Reviewed-by: Derek Bender <djbender@instructure.com> Reviewed-by: Adrian Packel <apackel@instructure.com> Reviewed-by: James Williams <jamesw@instructure.com> Product-Review: Keith Garner <kgarner@instructure.com>
2019-06-15 07:13:39 +08:00
add anonymous_id to due date cacher to upsert Relies on base58, which is rad because you can't mistake 0 (zero) for uppercase O (oh) and uppercase I (eye) for lowercase l (el). The other added benefit is some systems will interpret a prefixed 0 has indicating a different literal (e.g. hex is `0x` prefixed). This avoids the problem by never using zero. log2(58)*5 = ~29.28 bits (round up to 30 bits of entropy) 58**5 = 656,356,768 combinations For two generations there's a 1 in 656,356,768 chance of collision. In the case of a collision, we'll loop until an unused one is generated. Performance for this probably gets kinda bad once the assignment is half full. However if we have >300,000,000 submissions the app has probably already fallen over. closes: GRADE-893 Test Plan: 1. Given a course 2. Given a student enrolled in the course 3. When creating the first assignment (e.g. 'Alpha') 4. Then all submissions have anonymous_ids: anonymous_ids = Assignment.find_by!(title: 'Alpha'). all_submissions.pluck(:anonymous_id) 5. When Alpha has it's due date changed via the assignment edit page 6. Then none of the anonymous_ids have changed: anonymous_ids.sort! == Assignment.find_by!(title: 'Alpha'). all_submissions.pluck(:anonymous_id).sort! 7. Given a second assignment (e.g. 'Omega') 8. Given the manual deletion of anonymous_ids for the Omega assignment: omega = Assignment.find_by!(title: 'Omega') omega.all_submissions.update_all(anonymous_id: nil) 9. When DueDateCacher is triggered by changing the due date 10.Then all submissions in Omega now have anonymous_ids: omega.all_submissions.reload.pluck(:anonymous_id) 11.When validating a new submission in the console: submission = omega.submissions.build submission.validate # => false 12.Then anonymous_id is present: submission.anonymous_id # e.g. "aB123" Change-Id: I874ba5b0d6025c95af2f832c3cc5d83c3cbd20e7 Reviewed-on: https://gerrit.instructure.com/143314 Tested-by: Jenkins Reviewed-by: Keith T. Garner <kgarner@instructure.com> QA-Review: Indira Pai <ipai@instructure.com> Product-Review: Keith T. Garner <kgarner@instructure.com>
2018-03-13 04:05:42 +08:00
students_without_priors.each do |student_id|
ignore concluded enrollments in due date cacher closes GRADE-269 QA Notes: To run the due date cacher, open a Rails console and run: assignment = Assignment.find(<id of assignment>) DueDateCacher.recompute(assignment) Check cached due dates for submissions in the Rails console. submission = Submission.find_by( user_id: <id of student>, assignment_id: <id of assignment> ) submission.cached_due_date test plan: A. Setup 1. ensure the delayed jobs are not running 2. create a course with one assignment 3. enroll multiple students 4. assign the assignment to everyone B. Student without a Submission 1. enroll a student in the course 2. conclude the student's enrollment 3. manually run the due date cacher 4. confirm the student does not have any submissions C. Changing a Due Date 1. enroll a student in the course 2. manually run the due date cacher 3. conclude the student's enrollment 4. change the due date on the assignment 5. manually run the due date cacher 6. confirm the student submission exists 7. confirm the submission has the previous due date cached D. Unassigning an Assignment 1. enroll a student in the course 2. manually run the due date cacher 3. conclude the student's enrollment 4. create overrides for only the active students 5. make the assignment due only to overrides 6. manually run the due date cacher 7. confirm the student submission exists 8. confirm the submission has the previous due date cached Change-Id: I5e7165c0120e5c87635da1fbbe47501970874653 Reviewed-on: https://gerrit.instructure.com/126270 Tested-by: Jenkins Reviewed-by: Matt Taylor <mtaylor@instructure.com> Reviewed-by: Keith T. Garner <kgarner@instructure.com> QA-Review: KC Naegle <knaegle@instructure.com> Product-Review: Spencer Olson <solson@instructure.com>
2017-09-15 03:19:56 +08:00
submission_info = student_due_dates[student_id]
due_date = submission_info[:due_at] ? "'#{submission_info[:due_at].iso8601}'::timestamptz" : 'NULL'
grading_period_id = submission_info[:grading_period_id] || 'NULL'
add anonymous_id to due date cacher to upsert Relies on base58, which is rad because you can't mistake 0 (zero) for uppercase O (oh) and uppercase I (eye) for lowercase l (el). The other added benefit is some systems will interpret a prefixed 0 has indicating a different literal (e.g. hex is `0x` prefixed). This avoids the problem by never using zero. log2(58)*5 = ~29.28 bits (round up to 30 bits of entropy) 58**5 = 656,356,768 combinations For two generations there's a 1 in 656,356,768 chance of collision. In the case of a collision, we'll loop until an unused one is generated. Performance for this probably gets kinda bad once the assignment is half full. However if we have >300,000,000 submissions the app has probably already fallen over. closes: GRADE-893 Test Plan: 1. Given a course 2. Given a student enrolled in the course 3. When creating the first assignment (e.g. 'Alpha') 4. Then all submissions have anonymous_ids: anonymous_ids = Assignment.find_by!(title: 'Alpha'). all_submissions.pluck(:anonymous_id) 5. When Alpha has it's due date changed via the assignment edit page 6. Then none of the anonymous_ids have changed: anonymous_ids.sort! == Assignment.find_by!(title: 'Alpha'). all_submissions.pluck(:anonymous_id).sort! 7. Given a second assignment (e.g. 'Omega') 8. Given the manual deletion of anonymous_ids for the Omega assignment: omega = Assignment.find_by!(title: 'Omega') omega.all_submissions.update_all(anonymous_id: nil) 9. When DueDateCacher is triggered by changing the due date 10.Then all submissions in Omega now have anonymous_ids: omega.all_submissions.reload.pluck(:anonymous_id) 11.When validating a new submission in the console: submission = omega.submissions.build submission.validate # => false 12.Then anonymous_id is present: submission.anonymous_id # e.g. "aB123" Change-Id: I874ba5b0d6025c95af2f832c3cc5d83c3cbd20e7 Reviewed-on: https://gerrit.instructure.com/143314 Tested-by: Jenkins Reviewed-by: Keith T. Garner <kgarner@instructure.com> QA-Review: Indira Pai <ipai@instructure.com> Product-Review: Keith T. Garner <kgarner@instructure.com>
2018-03-13 04:05:42 +08:00
anonymous_id = Anonymity.generate_id(existing_ids: existing_anonymous_ids)
add anonymous_id to due date cacher to upsert Relies on base58, which is rad because you can't mistake 0 (zero) for uppercase O (oh) and uppercase I (eye) for lowercase l (el). The other added benefit is some systems will interpret a prefixed 0 has indicating a different literal (e.g. hex is `0x` prefixed). This avoids the problem by never using zero. log2(58)*5 = ~29.28 bits (round up to 30 bits of entropy) 58**5 = 656,356,768 combinations For two generations there's a 1 in 656,356,768 chance of collision. In the case of a collision, we'll loop until an unused one is generated. Performance for this probably gets kinda bad once the assignment is half full. However if we have >300,000,000 submissions the app has probably already fallen over. closes: GRADE-893 Test Plan: 1. Given a course 2. Given a student enrolled in the course 3. When creating the first assignment (e.g. 'Alpha') 4. Then all submissions have anonymous_ids: anonymous_ids = Assignment.find_by!(title: 'Alpha'). all_submissions.pluck(:anonymous_id) 5. When Alpha has it's due date changed via the assignment edit page 6. Then none of the anonymous_ids have changed: anonymous_ids.sort! == Assignment.find_by!(title: 'Alpha'). all_submissions.pluck(:anonymous_id).sort! 7. Given a second assignment (e.g. 'Omega') 8. Given the manual deletion of anonymous_ids for the Omega assignment: omega = Assignment.find_by!(title: 'Omega') omega.all_submissions.update_all(anonymous_id: nil) 9. When DueDateCacher is triggered by changing the due date 10.Then all submissions in Omega now have anonymous_ids: omega.all_submissions.reload.pluck(:anonymous_id) 11.When validating a new submission in the console: submission = omega.submissions.build submission.validate # => false 12.Then anonymous_id is present: submission.anonymous_id # e.g. "aB123" Change-Id: I874ba5b0d6025c95af2f832c3cc5d83c3cbd20e7 Reviewed-on: https://gerrit.instructure.com/143314 Tested-by: Jenkins Reviewed-by: Keith T. Garner <kgarner@instructure.com> QA-Review: Indira Pai <ipai@instructure.com> Product-Review: Keith T. Garner <kgarner@instructure.com>
2018-03-13 04:05:42 +08:00
existing_anonymous_ids << anonymous_id
sql_ready_anonymous_id = Submission.connection.quote(anonymous_id)
values << [assignment_id, student_id, due_date, grading_period_id, sql_ready_anonymous_id, quiz_lti, @course.root_account_id]
end
end
add anonymous_id to due date cacher to upsert Relies on base58, which is rad because you can't mistake 0 (zero) for uppercase O (oh) and uppercase I (eye) for lowercase l (el). The other added benefit is some systems will interpret a prefixed 0 has indicating a different literal (e.g. hex is `0x` prefixed). This avoids the problem by never using zero. log2(58)*5 = ~29.28 bits (round up to 30 bits of entropy) 58**5 = 656,356,768 combinations For two generations there's a 1 in 656,356,768 chance of collision. In the case of a collision, we'll loop until an unused one is generated. Performance for this probably gets kinda bad once the assignment is half full. However if we have >300,000,000 submissions the app has probably already fallen over. closes: GRADE-893 Test Plan: 1. Given a course 2. Given a student enrolled in the course 3. When creating the first assignment (e.g. 'Alpha') 4. Then all submissions have anonymous_ids: anonymous_ids = Assignment.find_by!(title: 'Alpha'). all_submissions.pluck(:anonymous_id) 5. When Alpha has it's due date changed via the assignment edit page 6. Then none of the anonymous_ids have changed: anonymous_ids.sort! == Assignment.find_by!(title: 'Alpha'). all_submissions.pluck(:anonymous_id).sort! 7. Given a second assignment (e.g. 'Omega') 8. Given the manual deletion of anonymous_ids for the Omega assignment: omega = Assignment.find_by!(title: 'Omega') omega.all_submissions.update_all(anonymous_id: nil) 9. When DueDateCacher is triggered by changing the due date 10.Then all submissions in Omega now have anonymous_ids: omega.all_submissions.reload.pluck(:anonymous_id) 11.When validating a new submission in the console: submission = omega.submissions.build submission.validate # => false 12.Then anonymous_id is present: submission.anonymous_id # e.g. "aB123" Change-Id: I874ba5b0d6025c95af2f832c3cc5d83c3cbd20e7 Reviewed-on: https://gerrit.instructure.com/143314 Tested-by: Jenkins Reviewed-by: Keith T. Garner <kgarner@instructure.com> QA-Review: Indira Pai <ipai@instructure.com> Product-Review: Keith T. Garner <kgarner@instructure.com>
2018-03-13 04:05:42 +08:00
assignments_to_delete_all_submissions_for = []
# Delete submissions for students who don't have visibility to this assignment anymore
@assignment_ids.each do |assignment_id|
assigned_student_ids = effective_due_dates.find_effective_due_dates_for_assignment(assignment_id).keys
if @user_ids.blank? && assigned_student_ids.blank? && enrollment_counts.prior_student_ids.blank?
assignments_to_delete_all_submissions_for << assignment_id
else
# Delete the users we KNOW we need to delete in batches (it makes the database happier this way)
deletable_student_ids =
enrollment_counts.accepted_student_ids - assigned_student_ids - enrollment_counts.prior_student_ids
deletable_student_ids.each_slice(1000) do |deletable_student_ids_chunk|
# using this approach instead of using .in_batches because we want to limit the IDs in the IN clause to 1k
Submission.active.where(assignment_id: assignment_id, user_id: deletable_student_ids_chunk)
.update_all(workflow_state: :deleted, updated_at: Time.zone.now)
end
User.clear_cache_keys(deletable_student_ids, :submissions)
end
end
assignments_to_delete_all_submissions_for.each_slice(50) do |assignment_slice|
subs = Submission.active.where(assignment_id: assignment_slice).limit(1_000)
while subs.update_all(workflow_state: :deleted, updated_at: Time.zone.now) > 0; end
end
# Get any stragglers that might have had their enrollment removed from the course
# 100 students at a time for 10 assignments each == slice of up to 1K submissions
enrollment_counts.deleted_student_ids.each_slice(100) do |student_slice|
@assignment_ids.each_slice(10) do |assignment_ids_slice|
Submission.active
.where(assignment_id: assignment_ids_slice, user_id: student_slice)
.update_all(workflow_state: :deleted, updated_at: Time.zone.now)
end
User.clear_cache_keys(student_slice, :submissions)
end
add anonymous_id to due date cacher to upsert Relies on base58, which is rad because you can't mistake 0 (zero) for uppercase O (oh) and uppercase I (eye) for lowercase l (el). The other added benefit is some systems will interpret a prefixed 0 has indicating a different literal (e.g. hex is `0x` prefixed). This avoids the problem by never using zero. log2(58)*5 = ~29.28 bits (round up to 30 bits of entropy) 58**5 = 656,356,768 combinations For two generations there's a 1 in 656,356,768 chance of collision. In the case of a collision, we'll loop until an unused one is generated. Performance for this probably gets kinda bad once the assignment is half full. However if we have >300,000,000 submissions the app has probably already fallen over. closes: GRADE-893 Test Plan: 1. Given a course 2. Given a student enrolled in the course 3. When creating the first assignment (e.g. 'Alpha') 4. Then all submissions have anonymous_ids: anonymous_ids = Assignment.find_by!(title: 'Alpha'). all_submissions.pluck(:anonymous_id) 5. When Alpha has it's due date changed via the assignment edit page 6. Then none of the anonymous_ids have changed: anonymous_ids.sort! == Assignment.find_by!(title: 'Alpha'). all_submissions.pluck(:anonymous_id).sort! 7. Given a second assignment (e.g. 'Omega') 8. Given the manual deletion of anonymous_ids for the Omega assignment: omega = Assignment.find_by!(title: 'Omega') omega.all_submissions.update_all(anonymous_id: nil) 9. When DueDateCacher is triggered by changing the due date 10.Then all submissions in Omega now have anonymous_ids: omega.all_submissions.reload.pluck(:anonymous_id) 11.When validating a new submission in the console: submission = omega.submissions.build submission.validate # => false 12.Then anonymous_id is present: submission.anonymous_id # e.g. "aB123" Change-Id: I874ba5b0d6025c95af2f832c3cc5d83c3cbd20e7 Reviewed-on: https://gerrit.instructure.com/143314 Tested-by: Jenkins Reviewed-by: Keith T. Garner <kgarner@instructure.com> QA-Review: Indira Pai <ipai@instructure.com> Product-Review: Keith T. Garner <kgarner@instructure.com>
2018-03-13 04:05:42 +08:00
return if values.empty?
values = values.sort_by(&:first)
values.each_slice(1000) do |batch|
auditable_entries = []
cached_due_dates_by_submission = {}
if record_due_date_changed_events?
auditable_entries = batch.select { |entry| @assignments_auditable_by_id.include?(entry.first) }
cached_due_dates_by_submission = current_cached_due_dates(auditable_entries)
end
# prepare values for SQL interpolation
batch_values = batch.map { |entry| "(#{entry.join(',')})" }
perform_submission_upsert(batch_values)
next unless record_due_date_changed_events? && auditable_entries.present?
record_due_date_changes_for_auditable_assignments!(
entries: auditable_entries,
previous_cached_dates: cached_due_dates_by_submission
)
end
User.clear_cache_keys(values.map { |v| v[1] }, :submissions)
end
re-apply late policies to any late submissions as needed the "as needed" refers to two cases: * when late policy changes * when an assignment's points_possible changes closes CNVS-36656 test plan: * Create a course with two assignments, two grading periods and two students enrolled * Ensure one assignment is due in a closed grading period and the other in an open grading period * Submit homework from both students so for each assignment one student submits the homework on time and the other submits it late * Go to gradebook and grade the students * Add a late policy to the course using the Rails console: course = Course.find(my_course_id) late_policy = LatePolicy.create( course: course, late_submission_deduction_enabled: true, late_submission_deduction: 50.0 ) * Reload the gradebook and you should see the score of the late submission for the assignment in the open grading period to have changed (lowered) * Verify that none of the other submissions had their scores changed * Now edit the assignment in the open grading period and change its points possible to a higher number and save this change * Reload the gradebook and you should see the score of the late submission for the assignment in the open grading period to have changed again * Verify that none of the other submissions had their scores changed * Now try this using three quiz submissions (early and late and just 45 seconds past the deadline). * Verify in the gradebook that late policy deductions are applied only to quiz submissions that are later than 60 seconds after the quiz due date Change-Id: I58ed3e3d0665870cf46d1b1e3ddf00f5f2f7008c Reviewed-on: https://gerrit.instructure.com/110598 Tested-by: Jenkins Reviewed-by: Jeremy Neander <jneander@instructure.com> Reviewed-by: Derek Bender <djbender@instructure.com> QA-Review: KC Naegle <knaegle@instructure.com> Product-Review: Keith T. Garner <kgarner@instructure.com>
2017-05-03 07:00:21 +08:00
fix stale grades when re-assigning students to assignment closes GRADE-819 The specific case was to unassign a graded student from an assignment by removing "Everyone" and adding all students except one to the assignment. Then remove all students and assign "Everyone" to the assignment. This should make the previously unassigned student's grades stale. test plan: * Configure delayed jobs to use 6 simultaneously running workers by editing config/delayed_jobs.yml to look like this: default: workers: - queue: canvas_queue workers: 6 Notice the line that says "workers: 6" * Restart the jobs container so all six new workers are started * Create a published course with one assignment worth 150 points and two students enrolled * For the remainder of these steps, let's assume unique identifer for the course is "course_id" and the unique identifier for the student is "student_id" * Go to the Gradebook and grade the first student at 125 points and wait for their score to be updated in the gradebook. * Verify the score in the Gradebook is 83.33% * In a separate tab, visit the following URL, making sure to replace course_id with the actual course id and student_id with the actual student id: /api/v1/courses/course_id/enrollments?user_id=student_id * Verify that you see the student's current_score as 83.33 * Repeat the following steps multiple times to ensure the problem does not manifest itself: - Modify the assignment: unassign it from the first student and only assign it to the second student only - Go to the gradebook and verify the cells for the first student are not editable any more - Go back to the API URL above and verify the student's current_score now says null - Modify the assignment: re-assign it to "Everyone" - Go to the gradebook and verify the cells for the first student are now editable again - Go back to the API URL above and verify the student's current_score now says 83.33 again. If it ever says null at this point, there is a problem. Change-Id: Ifaaf0609dfe5081697c1939db1b4a4e0a3e05bad Reviewed-on: https://gerrit.instructure.com/141049 Reviewed-by: Keith T. Garner <kgarner@instructure.com> Reviewed-by: Derek Bender <djbender@instructure.com> Tested-by: Jenkins Product-Review: Keith T. Garner <kgarner@instructure.com> QA-Review: Keith T. Garner <kgarner@instructure.com>
2018-02-15 23:28:07 +08:00
if @update_grades
@course.recompute_student_scores_without_send_later(@user_ids)
end
if @assignment_ids.size == 1
re-apply late policies to any late submissions as needed the "as needed" refers to two cases: * when late policy changes * when an assignment's points_possible changes closes CNVS-36656 test plan: * Create a course with two assignments, two grading periods and two students enrolled * Ensure one assignment is due in a closed grading period and the other in an open grading period * Submit homework from both students so for each assignment one student submits the homework on time and the other submits it late * Go to gradebook and grade the students * Add a late policy to the course using the Rails console: course = Course.find(my_course_id) late_policy = LatePolicy.create( course: course, late_submission_deduction_enabled: true, late_submission_deduction: 50.0 ) * Reload the gradebook and you should see the score of the late submission for the assignment in the open grading period to have changed (lowered) * Verify that none of the other submissions had their scores changed * Now edit the assignment in the open grading period and change its points possible to a higher number and save this change * Reload the gradebook and you should see the score of the late submission for the assignment in the open grading period to have changed again * Verify that none of the other submissions had their scores changed * Now try this using three quiz submissions (early and late and just 45 seconds past the deadline). * Verify in the gradebook that late policy deductions are applied only to quiz submissions that are later than 60 seconds after the quiz due date Change-Id: I58ed3e3d0665870cf46d1b1e3ddf00f5f2f7008c Reviewed-on: https://gerrit.instructure.com/110598 Tested-by: Jenkins Reviewed-by: Jeremy Neander <jneander@instructure.com> Reviewed-by: Derek Bender <djbender@instructure.com> QA-Review: KC Naegle <knaegle@instructure.com> Product-Review: Keith T. Garner <kgarner@instructure.com>
2017-05-03 07:00:21 +08:00
# Only changes to LatePolicy or (sometimes) Assignment records can result in a re-calculation
# of student scores. No changes to the Course record can trigger such re-calculations so
# let's ensure this is triggered only when DueDateCacher is called for a Assignment-level
# changes and not for Course-level changes
assignment = Assignment.find(@assignment_ids.first)
re-apply late policies to any late submissions as needed the "as needed" refers to two cases: * when late policy changes * when an assignment's points_possible changes closes CNVS-36656 test plan: * Create a course with two assignments, two grading periods and two students enrolled * Ensure one assignment is due in a closed grading period and the other in an open grading period * Submit homework from both students so for each assignment one student submits the homework on time and the other submits it late * Go to gradebook and grade the students * Add a late policy to the course using the Rails console: course = Course.find(my_course_id) late_policy = LatePolicy.create( course: course, late_submission_deduction_enabled: true, late_submission_deduction: 50.0 ) * Reload the gradebook and you should see the score of the late submission for the assignment in the open grading period to have changed (lowered) * Verify that none of the other submissions had their scores changed * Now edit the assignment in the open grading period and change its points possible to a higher number and save this change * Reload the gradebook and you should see the score of the late submission for the assignment in the open grading period to have changed again * Verify that none of the other submissions had their scores changed * Now try this using three quiz submissions (early and late and just 45 seconds past the deadline). * Verify in the gradebook that late policy deductions are applied only to quiz submissions that are later than 60 seconds after the quiz due date Change-Id: I58ed3e3d0665870cf46d1b1e3ddf00f5f2f7008c Reviewed-on: https://gerrit.instructure.com/110598 Tested-by: Jenkins Reviewed-by: Jeremy Neander <jneander@instructure.com> Reviewed-by: Derek Bender <djbender@instructure.com> QA-Review: KC Naegle <knaegle@instructure.com> Product-Review: Keith T. Garner <kgarner@instructure.com>
2017-05-03 07:00:21 +08:00
LatePolicyApplicator.for_assignment(assignment)
end
replace submissions.late column with .cached_due_date refs CNVS-5805 with efficient calculation of all due dates for any submissions for a given assignment when related records (the assignment, its overrides, related enrollments, and related group memberships) changes. compares this cached due date to the submitted_at or current time when determining lateness. populates the column for existing submissions in a post-deploy data-fixup migration. test-plan: - run lib/data_fixup/initialize_submission_cached_due_date.rb - all submissions' cached_due_dates should be updated over several jobs - enroll a student in a course and create submissions in that course - create a second enrollment in a second section; the cached_due_dates for the user's submissions should recalculate - destroy the second enrollment; the cached_due_dates for the user's submissions should recalculate - create a group assignment - add the student to a group in the assignment's category; the cached_due_dates for the user's submissions should recalculate - remove the student from the group; the cached_due_dates for the user's submissions should recalculate - enroll more students in the course - change an assignment's due date; the cached_due_dates for the assignment's submissions should recalculate - create an override for the assignment; the cached_due_dates for the assignment's submissions should recalculate - change the due date on the override; the cached_due_dates for the assignment's submissions should recalculate - delete the override; the cached_due_dates for the assignment's submissions should recalculate - during any of the above recalculations: - the most lenient applicable override should apply - if the most lenient applicable override is more stringent than the assignment due_at, it should still apply - the assignment due_at should apply if there are no applicable overrides Change-Id: Ibacab27429a76755114dabb1e735d4b3d9bbd2fc Reviewed-on: https://gerrit.instructure.com/21123 Reviewed-by: Brian Palmer <brianp@instructure.com> Product-Review: Jacob Fugal <jacob@instructure.com> QA-Review: Jacob Fugal <jacob@instructure.com> Tested-by: Jacob Fugal <jacob@instructure.com>
2013-06-01 04:07:26 +08:00
end
private
EnrollmentCounts = Struct.new(:accepted_student_ids, :prior_student_ids, :deleted_student_ids)
def enrollment_counts
@enrollment_counts ||= begin
counts = EnrollmentCounts.new([], [], [])
GuardRail.activate(:secondary) do
# The various workflow states below try to mimic similarly named scopes off of course
scope = Enrollment.select(
:user_id,
"count(nullif(workflow_state not in ('rejected', 'deleted', 'completed'), false)) as accepted_count",
"count(nullif(workflow_state in ('completed'), false)) as prior_count",
"count(nullif(workflow_state in ('rejected', 'deleted'), false)) as deleted_count"
)
.where(course_id: @course, type: ['StudentEnrollment', 'StudentViewEnrollment'])
.group(:user_id)
scope = scope.where(user_id: @user_ids) if @user_ids.present?
scope.find_each do |record|
if record.accepted_count == 0 && record.deleted_count > 0
counts.deleted_student_ids << record.user_id
elsif record.accepted_count == 0 && record.prior_count > 0
counts.prior_student_ids << record.user_id
elsif record.accepted_count > 0
counts.accepted_student_ids << record.user_id
else
raise "Unknown enrollment state: #{record.accepted_count}, #{record.prior_count}, #{record.deleted_count}"
end
end
end
counts
end
ignore concluded enrollments in due date cacher closes GRADE-269 QA Notes: To run the due date cacher, open a Rails console and run: assignment = Assignment.find(<id of assignment>) DueDateCacher.recompute(assignment) Check cached due dates for submissions in the Rails console. submission = Submission.find_by( user_id: <id of student>, assignment_id: <id of assignment> ) submission.cached_due_date test plan: A. Setup 1. ensure the delayed jobs are not running 2. create a course with one assignment 3. enroll multiple students 4. assign the assignment to everyone B. Student without a Submission 1. enroll a student in the course 2. conclude the student's enrollment 3. manually run the due date cacher 4. confirm the student does not have any submissions C. Changing a Due Date 1. enroll a student in the course 2. manually run the due date cacher 3. conclude the student's enrollment 4. change the due date on the assignment 5. manually run the due date cacher 6. confirm the student submission exists 7. confirm the submission has the previous due date cached D. Unassigning an Assignment 1. enroll a student in the course 2. manually run the due date cacher 3. conclude the student's enrollment 4. create overrides for only the active students 5. make the assignment due only to overrides 6. manually run the due date cacher 7. confirm the student submission exists 8. confirm the submission has the previous due date cached Change-Id: I5e7165c0120e5c87635da1fbbe47501970874653 Reviewed-on: https://gerrit.instructure.com/126270 Tested-by: Jenkins Reviewed-by: Matt Taylor <mtaylor@instructure.com> Reviewed-by: Keith T. Garner <kgarner@instructure.com> QA-Review: KC Naegle <knaegle@instructure.com> Product-Review: Spencer Olson <solson@instructure.com>
2017-09-15 03:19:56 +08:00
end
def effective_due_dates
@effective_due_dates ||= begin
edd = EffectiveDueDates.for_course(@course, @assignment_ids)
edd.filter_students_to(@user_ids) if @user_ids.present?
edd
end
end
def current_cached_due_dates(entries)
return {} if entries.empty?
entries_for_query = assignment_and_student_id_values(entries: entries)
submissions_with_due_dates = Submission.where("(assignment_id, user_id) IN (#{entries_for_query.join(',')})")
.where.not(cached_due_date: nil)
.pluck(:id, :cached_due_date)
submissions_with_due_dates.each_with_object({}) do |(submission_id, cached_due_date), map|
map[submission_id] = cached_due_date
end
end
def record_due_date_changes_for_auditable_assignments!(entries:, previous_cached_dates:)
entries_for_query = assignment_and_student_id_values(entries: entries)
updated_submissions = Submission.where("(assignment_id, user_id) IN (#{entries_for_query.join(',')})")
.pluck(:id, :assignment_id, :cached_due_date)
timestamp = Time.zone.now
records_to_insert = updated_submissions.each_with_object([]) do |(submission_id, assignment_id, new_due_date), records|
old_due_date = previous_cached_dates.fetch(submission_id, nil)
next if new_due_date == old_due_date
payload = { due_at: [old_due_date&.iso8601, new_due_date&.iso8601] }
records << {
assignment_id: assignment_id,
submission_id: submission_id,
user_id: @executing_user_id,
event_type: 'submission_updated',
payload: payload.to_json,
created_at: timestamp,
updated_at: timestamp
}
end
AnonymousOrModerationEvent.bulk_insert(records_to_insert)
end
def assignment_and_student_id_values(entries:)
entries.map { |(assignment_id, student_id)| "(#{assignment_id}, #{student_id})" }
end
def record_due_date_changed_events?
# Only audit if we have a user and at least one auditable assignment
@record_due_date_changed_events ||= @executing_user_id.present? && @assignments_auditable_by_id.present?
end
def quiz_lti_assignments
# We only care about quiz LTIs, so we'll only snag those. In fact,
# we only care if the assignment *is* a quiz, LTI, so we'll just
# keep a set of those assignment ids.
@quiz_lti_assignments ||=
ContentTag.joins("INNER JOIN #{ContextExternalTool.quoted_table_name} ON content_tags.content_type='ContextExternalTool' AND context_external_tools.id = content_tags.content_id")
.merge(ContextExternalTool.quiz_lti)
.where(context_type: 'Assignment'). #
# We're doing the following direct postgres any() rather than .where(context_id: @assignment_ids) on advice
# from our DBAs that the any is considerably faster in the postgres planner than the "IN ()" statement that
# AR would have generated.
where("content_tags.context_id = any('{?}'::int8[])", @assignment_ids)
.where.not(workflow_state: 'deleted').distinct.pluck(:context_id).to_set
end
def existing_anonymous_ids_by_assignment_id
@existing_anonymous_ids_by_assignment_id ||=
Submission
.anonymized
.for_assignment(effective_due_dates.to_hash.keys)
.pluck(:assignment_id, :anonymous_id)
.each_with_object(Hash.new { |h, k| h[k] = [] }) { |data, h| h[data.first] << data.last }
end
def perform_submission_upsert(batch_values)
# Construct upsert statement to update existing Submissions or create them if needed.
query = <<~SQL.squish
UPDATE #{Submission.quoted_table_name}
SET
cached_due_date = vals.due_date::timestamptz,
grading_period_id = vals.grading_period_id::integer,
workflow_state = COALESCE(NULLIF(workflow_state, 'deleted'), (
#{INFER_SUBMISSION_WORKFLOW_STATE_SQL}
)),
anonymous_id = COALESCE(submissions.anonymous_id, vals.anonymous_id),
cached_quiz_lti = vals.cached_quiz_lti,
updated_at = now() AT TIME ZONE 'UTC'
FROM (VALUES #{batch_values.join(',')})
AS vals(assignment_id, student_id, due_date, grading_period_id, anonymous_id, cached_quiz_lti, root_account_id)
WHERE submissions.user_id = vals.student_id AND
submissions.assignment_id = vals.assignment_id AND
(
(submissions.cached_due_date IS DISTINCT FROM vals.due_date::timestamptz) OR
(submissions.grading_period_id IS DISTINCT FROM vals.grading_period_id::integer) OR
(submissions.workflow_state <> COALESCE(NULLIF(submissions.workflow_state, 'deleted'),
(#{INFER_SUBMISSION_WORKFLOW_STATE_SQL})
)) OR
(submissions.anonymous_id IS DISTINCT FROM COALESCE(submissions.anonymous_id, vals.anonymous_id)) OR
(submissions.cached_quiz_lti IS DISTINCT FROM vals.cached_quiz_lti)
);
INSERT INTO #{Submission.quoted_table_name}
(assignment_id, user_id, workflow_state, created_at, updated_at, course_id,
cached_due_date, grading_period_id, anonymous_id, cached_quiz_lti, root_account_id)
SELECT
assignments.id, vals.student_id, 'unsubmitted',
now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC',
assignments.context_id, vals.due_date::timestamptz, vals.grading_period_id::integer,
vals.anonymous_id,
vals.cached_quiz_lti,
vals.root_account_id
FROM (VALUES #{batch_values.join(',')})
AS vals(assignment_id, student_id, due_date, grading_period_id, anonymous_id, cached_quiz_lti, root_account_id)
INNER JOIN #{Assignment.quoted_table_name} assignments
ON assignments.id = vals.assignment_id
LEFT OUTER JOIN #{Submission.quoted_table_name} submissions
ON submissions.assignment_id = assignments.id
AND submissions.user_id = vals.student_id
WHERE submissions.id IS NULL;
SQL
begin
Submission.transaction do
Submission.connection.execute(query)
end
rescue ActiveRecord::RecordNotUnique => e
Canvas::Errors.capture_exception(:due_date_cacher, e, :warn)
raise Delayed::RetriableError, "Unique record violation when creating new submissions"
rescue ActiveRecord::Deadlocked => e
Canvas::Errors.capture_exception(:due_date_cacher, e, :warn)
raise Delayed::RetriableError, "Deadlock when upserting submissions"
end
end
replace submissions.late column with .cached_due_date refs CNVS-5805 with efficient calculation of all due dates for any submissions for a given assignment when related records (the assignment, its overrides, related enrollments, and related group memberships) changes. compares this cached due date to the submitted_at or current time when determining lateness. populates the column for existing submissions in a post-deploy data-fixup migration. test-plan: - run lib/data_fixup/initialize_submission_cached_due_date.rb - all submissions' cached_due_dates should be updated over several jobs - enroll a student in a course and create submissions in that course - create a second enrollment in a second section; the cached_due_dates for the user's submissions should recalculate - destroy the second enrollment; the cached_due_dates for the user's submissions should recalculate - create a group assignment - add the student to a group in the assignment's category; the cached_due_dates for the user's submissions should recalculate - remove the student from the group; the cached_due_dates for the user's submissions should recalculate - enroll more students in the course - change an assignment's due date; the cached_due_dates for the assignment's submissions should recalculate - create an override for the assignment; the cached_due_dates for the assignment's submissions should recalculate - change the due date on the override; the cached_due_dates for the assignment's submissions should recalculate - delete the override; the cached_due_dates for the assignment's submissions should recalculate - during any of the above recalculations: - the most lenient applicable override should apply - if the most lenient applicable override is more stringent than the assignment due_at, it should still apply - the assignment due_at should apply if there are no applicable overrides Change-Id: Ibacab27429a76755114dabb1e735d4b3d9bbd2fc Reviewed-on: https://gerrit.instructure.com/21123 Reviewed-by: Brian Palmer <brianp@instructure.com> Product-Review: Jacob Fugal <jacob@instructure.com> QA-Review: Jacob Fugal <jacob@instructure.com> Tested-by: Jacob Fugal <jacob@instructure.com>
2013-06-01 04:07:26 +08:00
end