canvas-lms/lib/submission_lifecycle_manage...

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

530 lines
23 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"
class SubmissionLifecycleManager
include Moderation
MAX_RUNNING_JOBS = 10
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 SubmissionLifecycleManager is invoked in a callback or a similar
# place where directly specifying an executing user is impractical.
#
# SubmissionLifecycleManager.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:
# SubmissionLifecycleManager.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 SubmissionLifecycleManager 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
def self.infer_submission_workflow_state_sql
<<~SQL_FRAGMENT
CASE
WHEN submission_type = 'online_quiz' AND quiz_submission_id IS NOT NULL AND (
SELECT EXISTS (
SELECT
*
FROM
#{Quizzes::QuizSubmission.quoted_table_name} qs
WHERE
quiz_submission_id = qs.id
AND workflow_state = 'pending_review'
)
) THEN
'pending_review'
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
end
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: {
singleton: "cached_due_date:calculator:Assignment:#{assignment.global_id}:UpdateGrades:#{update_grades ? 1 : 0}",
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:,
original_caller: current_caller,
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
fix import past date nq assigning late policy on occasion, a course import for new quizzes would trigger a late policy even if the dates were shifted or removed during the migration. this change will disable the late policy applicator if there are any date shift options set durig the import. fixes EVAL-2873 flag=none test plan: - all steps done as a teacher - create two courses (Course A and Course B) with a student in each course - in each course, create a Late Policy for the course and set the % to 50 - create a new quiz *with a positive points value* and with a due date in the past in Course A - go to the /courses/<course-b-id>/content_migrations page - complete the steps to import migration - Select "Copy a Canvas Course" in Content Type dropdown - Select Course A in the search box - Select "Select specific content" radio button - Click the "Adjust dates" checkbox and select "Remove Dates" radio button - Press the "Import" button - once the migration is created, press the "Select Content" button and select the quiz - once the import is complete, go to Gradebook and verify the course was imported but the student did not receive a late policy score Change-Id: I2f08316a970fe9d1cceafdf2293d36fc73135fb2 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/312073 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Spencer Olson <solson@instructure.com> Reviewed-by: Cameron Ray <cameron.ray@instructure.com> QA-Review: Cameron Ray <cameron.ray@instructure.com> Product-Review: Cameron Ray <cameron.ray@instructure.com>
2023-02-28 06:51:04 +08:00
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, skip_late_policy_applicator: false)
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}:UpdateGrades:#{update_grades ? 1 : 0}" if assignments.nil?
inst_jobs_opts[:strand] ||= "cached_due_date:calculator:Course:#{course.global_id}"
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 || AbstractAssignment.active.where(context: course).pluck(:id)
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
return if assignments_to_recompute.empty?
executing_user ||= current_executing_user
submission_lifecycle_manager = new(course, assignments_to_recompute, update_grades:, original_caller:, executing_user:, skip_late_policy_applicator:)
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
submission_lifecycle_manager.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
else
submission_lifecycle_manager.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 = {})
opts = inst_jobs_opts.extract!(:update_grades, :executing_user, :sis_import, :require_singleton).reverse_merge(require_singleton: assignments.nil?)
user_ids = Array(user_ids)
course = Course.find(course) unless course.is_a?(Course)
update_grades = opts[:update_grades] || false
inst_jobs_opts[:max_attempts] ||= 10
inst_jobs_opts[:strand] ||= "cached_due_date:calculator:Course:#{course.global_id}"
if opts[:require_singleton]
inst_jobs_opts[:singleton] ||= "cached_due_date:calculator:Course:#{course.global_id}:Users:#{Digest::SHA256.hexdigest(user_ids.sort.join(":"))}:UpdateGrades:#{update_grades ? 1 : 0}"
end
assignments ||= AbstractAssignment.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
executing_user = opts[:executing_user] || current_executing_user
if opts[:sis_import]
running_jobs_count = Delayed::Job.running.where(shard_id: course.shard.id, tag: "SubmissionLifecycleManager#recompute_for_sis_import").count
if running_jobs_count >= MAX_RUNNING_JOBS
# there are too many sis recompute jobs running concurrently now. let's check again in a bit to see if we can run.
return delay_if_production(
**inst_jobs_opts,
run_at: 10.seconds.from_now
).recompute_users_for_course(user_ids, course, assignments, opts)
else
submission_lifecycle_manager = new(course, assignments, user_ids, update_grades:, original_caller: current_caller, executing_user:)
return submission_lifecycle_manager.delay_if_production(**inst_jobs_opts).recompute_for_sis_import
end
end
submission_lifecycle_manager = new(course, assignments, user_ids, update_grades:, original_caller: current_caller, executing_user:)
submission_lifecycle_manager.delay_if_production(**inst_jobs_opts).recompute
end
fix import past date nq assigning late policy on occasion, a course import for new quizzes would trigger a late policy even if the dates were shifted or removed during the migration. this change will disable the late policy applicator if there are any date shift options set durig the import. fixes EVAL-2873 flag=none test plan: - all steps done as a teacher - create two courses (Course A and Course B) with a student in each course - in each course, create a Late Policy for the course and set the % to 50 - create a new quiz *with a positive points value* and with a due date in the past in Course A - go to the /courses/<course-b-id>/content_migrations page - complete the steps to import migration - Select "Copy a Canvas Course" in Content Type dropdown - Select Course A in the search box - Select "Select specific content" radio button - Click the "Adjust dates" checkbox and select "Remove Dates" radio button - Press the "Import" button - once the migration is created, press the "Select Content" button and select the quiz - once the import is complete, go to Gradebook and verify the course was imported but the student did not receive a late policy score Change-Id: I2f08316a970fe9d1cceafdf2293d36fc73135fb2 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/312073 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Spencer Olson <solson@instructure.com> Reviewed-by: Cameron Ray <cameron.ray@instructure.com> QA-Review: Cameron Ray <cameron.ray@instructure.com> Product-Review: Cameron Ray <cameron.ray@instructure.com>
2023-02-28 06:51:04 +08:00
def initialize(course, assignments, user_ids = [], update_grades: false, original_caller: caller(1..1).first, executing_user: nil, skip_late_policy_applicator: false)
@course = course
@assignment_ids = Array(assignments).map { |a| a.is_a?(AbstractAssignment) ? a.id : a }
# ensure we're dealing with local IDs to avoid headaches downstream
if @assignment_ids.present?
@course.shard.activate do
if @assignment_ids.any? { |id| AbstractAssignment.global_id?(id) }
@assignment_ids = AbstractAssignment.where(id: @assignment_ids).pluck(:id)
end
@assignments_auditable_by_id = Set.new(AbstractAssignment.auditable.where(id: @assignment_ids).pluck(:id))
end
else
@assignments_auditable_by_id = 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
fix import past date nq assigning late policy on occasion, a course import for new quizzes would trigger a late policy even if the dates were shifted or removed during the migration. this change will disable the late policy applicator if there are any date shift options set durig the import. fixes EVAL-2873 flag=none test plan: - all steps done as a teacher - create two courses (Course A and Course B) with a student in each course - in each course, create a Late Policy for the course and set the % to 50 - create a new quiz *with a positive points value* and with a due date in the past in Course A - go to the /courses/<course-b-id>/content_migrations page - complete the steps to import migration - Select "Copy a Canvas Course" in Content Type dropdown - Select Course A in the search box - Select "Select specific content" radio button - Click the "Adjust dates" checkbox and select "Remove Dates" radio button - Press the "Import" button - once the migration is created, press the "Select Content" button and select the quiz - once the import is complete, go to Gradebook and verify the course was imported but the student did not receive a late policy score Change-Id: I2f08316a970fe9d1cceafdf2293d36fc73135fb2 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/312073 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Spencer Olson <solson@instructure.com> Reviewed-by: Cameron Ray <cameron.ray@instructure.com> QA-Review: Cameron Ray <cameron.ray@instructure.com> Product-Review: Cameron Ray <cameron.ray@instructure.com>
2023-02-28 06:51:04 +08:00
@skip_late_policy_applicator = skip_late_policy_applicator
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
# exists so that we can identify (and limit) jobs running specifically for sis imports
# Delayed::Job.where(tag: "SubmissionLifecycleManager#recompute_for_sis_import")
def recompute_for_sis_import
recompute
end
def recompute
Rails.logger.debug "SUBMISSION LIFECYCLE MANAGER STARTS: #{Time.zone.now.to_i}"
Rails.logger.debug "SLM#recompute() - original caller: #{@original_caller}"
Rails.logger.debug "SLM#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 = AbstractAssignment.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|
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
fix speedgrader anonymous moderated bug flag=none closes EVAL-2131 closes EVAL-2332 Test Plan 1: * Prerequisites: Published course with active teachers and students, and the anonymous grading feature option enabled. 1. Create an assignment. Enable moderated and anonymous grading. 2. Submit some content as a student. 3. Enter some grades as the graders. 4. De-activate the student that submitted. 5. Go to Gradebook and in the student header menu select to view 'Inactive' students. 6. Go to SpeedGrader and verify the deactivated student and their submission shows up. Take note of the student's anonymous_id in the URL query params. 7. Go to the Moderate page for the assignment as the moderator, and verify the anonymous name for the student (i.e. "Student 1") matches their anonymous name in SpeedGrader. You can verify this by hovering over the student link on the Moderate page and seeing what their anonymous_id is, and comparing it to the anonymous_id you noted in step 6. Test Plan 2: Legacy Support for Concluded Students 1. Switch to the 'master' branch. 2. Conclude a student that has only a single active enrollment (this student must not have other enrollments, not even soft-deleted ones). 3. Create an assignment that is due for everyone. 4. In a rails console, verify no submission was created for the concluded student: user = User.find(<concluded student id>) Assignment.find(<assignment id>).submissions.where(user: user).count => should be 0 5. Switch to the feature branch for this commit 6. As a teacher, go to Gradebook and in the student header choose to view 'Concluded' students. 7. Go to SpeedGrader for the assignment you made in step 3. 8. Verify the concluded student that does not have a submission does not show up in SpeedGrader. Test Plan 3: 1. Conclude a student in a course. It's important that the student is concluded before any of the following steps. It's also important that the student only has a single enrollment in the course (including soft-deleted enrollments). You can make sure the student only has 1 enrollment in the course with: # should return 1 Enrollment.where(course_id: <course id>, user_id: <student id>).count 2. Create an assignment. 3. In the gradebook, select the option to show concluded enrollments. 4. Launch the SpeedGrader for the assignment. 5. Enable the Hide student names in the SpeedGrader settings. 6. Refresh the page and verify SpeedGrader loads as expected. And verify you can navigate to the concluded student in SpeedGrader. Change-Id: I265ee97953d1a9ebd098911ed4388b0e1da49af7 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/282581 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> QA-Review: Dustin Cowles <dustin.cowles@instructure.com> Product-Review: Jody Sailor Reviewed-by: Dustin Cowles <dustin.cowles@instructure.com> Reviewed-by: Kai Bjorkman <kbjorkman@instructure.com>
2022-01-12 05:56:41 +08:00
student_due_dates.each_key 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] ? "'#{ActiveRecord::Base.connection.quoted_date(submission_info[:due_at].change(usec: 0))}'::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:, 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
nq_restore_pending_flag_enabled = Account.site_admin.feature_enabled?(:new_quiz_deleted_workflow_restore_pending_review_state)
# 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
if nq_restore_pending_flag_enabled
handle_lti_deleted_submissions(batch)
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.pluck(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
fix import past date nq assigning late policy on occasion, a course import for new quizzes would trigger a late policy even if the dates were shifted or removed during the migration. this change will disable the late policy applicator if there are any date shift options set durig the import. fixes EVAL-2873 flag=none test plan: - all steps done as a teacher - create two courses (Course A and Course B) with a student in each course - in each course, create a Late Policy for the course and set the % to 50 - create a new quiz *with a positive points value* and with a due date in the past in Course A - go to the /courses/<course-b-id>/content_migrations page - complete the steps to import migration - Select "Copy a Canvas Course" in Content Type dropdown - Select Course A in the search box - Select "Select specific content" radio button - Click the "Adjust dates" checkbox and select "Remove Dates" radio button - Press the "Import" button - once the migration is created, press the "Select Content" button and select the quiz - once the import is complete, go to Gradebook and verify the course was imported but the student did not receive a late policy score Change-Id: I2f08316a970fe9d1cceafdf2293d36fc73135fb2 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/312073 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Spencer Olson <solson@instructure.com> Reviewed-by: Cameron Ray <cameron.ray@instructure.com> QA-Review: Cameron Ray <cameron.ray@instructure.com> Product-Review: Cameron Ray <cameron.ray@instructure.com>
2023-02-28 06:51:04 +08:00
if @assignment_ids.size == 1 && !@skip_late_policy_applicator
# Only changes to LatePolicy or (sometimes) AbstractAssignment records can result in a re-calculation
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
# of student scores. No changes to the Course record can trigger such re-calculations so
# let's ensure this is triggered only when SubmissionLifecycleManager is called for a Assignment-level
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
# changes and not for Course-level changes
assignment = @course.shard.activate { AbstractAssignment.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,
fix speedgrader anonymous moderated bug flag=none closes EVAL-2131 closes EVAL-2332 Test Plan 1: * Prerequisites: Published course with active teachers and students, and the anonymous grading feature option enabled. 1. Create an assignment. Enable moderated and anonymous grading. 2. Submit some content as a student. 3. Enter some grades as the graders. 4. De-activate the student that submitted. 5. Go to Gradebook and in the student header menu select to view 'Inactive' students. 6. Go to SpeedGrader and verify the deactivated student and their submission shows up. Take note of the student's anonymous_id in the URL query params. 7. Go to the Moderate page for the assignment as the moderator, and verify the anonymous name for the student (i.e. "Student 1") matches their anonymous name in SpeedGrader. You can verify this by hovering over the student link on the Moderate page and seeing what their anonymous_id is, and comparing it to the anonymous_id you noted in step 6. Test Plan 2: Legacy Support for Concluded Students 1. Switch to the 'master' branch. 2. Conclude a student that has only a single active enrollment (this student must not have other enrollments, not even soft-deleted ones). 3. Create an assignment that is due for everyone. 4. In a rails console, verify no submission was created for the concluded student: user = User.find(<concluded student id>) Assignment.find(<assignment id>).submissions.where(user: user).count => should be 0 5. Switch to the feature branch for this commit 6. As a teacher, go to Gradebook and in the student header choose to view 'Concluded' students. 7. Go to SpeedGrader for the assignment you made in step 3. 8. Verify the concluded student that does not have a submission does not show up in SpeedGrader. Test Plan 3: 1. Conclude a student in a course. It's important that the student is concluded before any of the following steps. It's also important that the student only has a single enrollment in the course (including soft-deleted enrollments). You can make sure the student only has 1 enrollment in the course with: # should return 1 Enrollment.where(course_id: <course id>, user_id: <student id>).count 2. Create an assignment. 3. In the gradebook, select the option to show concluded enrollments. 4. Launch the SpeedGrader for the assignment. 5. Enable the Hide student names in the SpeedGrader settings. 6. Refresh the page and verify SpeedGrader loads as expected. And verify you can navigate to the concluded student in SpeedGrader. Change-Id: I265ee97953d1a9ebd098911ed4388b0e1da49af7 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/282581 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> QA-Review: Dustin Cowles <dustin.cowles@instructure.com> Product-Review: Jody Sailor Reviewed-by: Dustin Cowles <dustin.cowles@instructure.com> Reviewed-by: Kai Bjorkman <kbjorkman@instructure.com>
2022-01-12 05:56:41 +08:00
"count(nullif(workflow_state not in ('rejected', 'deleted'), 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|
fix speedgrader anonymous moderated bug flag=none closes EVAL-2131 closes EVAL-2332 Test Plan 1: * Prerequisites: Published course with active teachers and students, and the anonymous grading feature option enabled. 1. Create an assignment. Enable moderated and anonymous grading. 2. Submit some content as a student. 3. Enter some grades as the graders. 4. De-activate the student that submitted. 5. Go to Gradebook and in the student header menu select to view 'Inactive' students. 6. Go to SpeedGrader and verify the deactivated student and their submission shows up. Take note of the student's anonymous_id in the URL query params. 7. Go to the Moderate page for the assignment as the moderator, and verify the anonymous name for the student (i.e. "Student 1") matches their anonymous name in SpeedGrader. You can verify this by hovering over the student link on the Moderate page and seeing what their anonymous_id is, and comparing it to the anonymous_id you noted in step 6. Test Plan 2: Legacy Support for Concluded Students 1. Switch to the 'master' branch. 2. Conclude a student that has only a single active enrollment (this student must not have other enrollments, not even soft-deleted ones). 3. Create an assignment that is due for everyone. 4. In a rails console, verify no submission was created for the concluded student: user = User.find(<concluded student id>) Assignment.find(<assignment id>).submissions.where(user: user).count => should be 0 5. Switch to the feature branch for this commit 6. As a teacher, go to Gradebook and in the student header choose to view 'Concluded' students. 7. Go to SpeedGrader for the assignment you made in step 3. 8. Verify the concluded student that does not have a submission does not show up in SpeedGrader. Test Plan 3: 1. Conclude a student in a course. It's important that the student is concluded before any of the following steps. It's also important that the student only has a single enrollment in the course (including soft-deleted enrollments). You can make sure the student only has 1 enrollment in the course with: # should return 1 Enrollment.where(course_id: <course id>, user_id: <student id>).count 2. Create an assignment. 3. In the gradebook, select the option to show concluded enrollments. 4. Launch the SpeedGrader for the assignment. 5. Enable the Hide student names in the SpeedGrader settings. 6. Refresh the page and verify SpeedGrader loads as expected. And verify you can navigate to the concluded student in SpeedGrader. Change-Id: I265ee97953d1a9ebd098911ed4388b0e1da49af7 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/282581 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> QA-Review: Dustin Cowles <dustin.cowles@instructure.com> Product-Review: Jody Sailor Reviewed-by: Dustin Cowles <dustin.cowles@instructure.com> Reviewed-by: Kai Bjorkman <kbjorkman@instructure.com>
2022-01-12 05:56:41 +08:00
if record.accepted_count > 0
if record.accepted_count == record.prior_count
counts.prior_student_ids << record.user_id
else
counts.accepted_student_ids << record.user_id
end
else
fix speedgrader anonymous moderated bug flag=none closes EVAL-2131 closes EVAL-2332 Test Plan 1: * Prerequisites: Published course with active teachers and students, and the anonymous grading feature option enabled. 1. Create an assignment. Enable moderated and anonymous grading. 2. Submit some content as a student. 3. Enter some grades as the graders. 4. De-activate the student that submitted. 5. Go to Gradebook and in the student header menu select to view 'Inactive' students. 6. Go to SpeedGrader and verify the deactivated student and their submission shows up. Take note of the student's anonymous_id in the URL query params. 7. Go to the Moderate page for the assignment as the moderator, and verify the anonymous name for the student (i.e. "Student 1") matches their anonymous name in SpeedGrader. You can verify this by hovering over the student link on the Moderate page and seeing what their anonymous_id is, and comparing it to the anonymous_id you noted in step 6. Test Plan 2: Legacy Support for Concluded Students 1. Switch to the 'master' branch. 2. Conclude a student that has only a single active enrollment (this student must not have other enrollments, not even soft-deleted ones). 3. Create an assignment that is due for everyone. 4. In a rails console, verify no submission was created for the concluded student: user = User.find(<concluded student id>) Assignment.find(<assignment id>).submissions.where(user: user).count => should be 0 5. Switch to the feature branch for this commit 6. As a teacher, go to Gradebook and in the student header choose to view 'Concluded' students. 7. Go to SpeedGrader for the assignment you made in step 3. 8. Verify the concluded student that does not have a submission does not show up in SpeedGrader. Test Plan 3: 1. Conclude a student in a course. It's important that the student is concluded before any of the following steps. It's also important that the student only has a single enrollment in the course (including soft-deleted enrollments). You can make sure the student only has 1 enrollment in the course with: # should return 1 Enrollment.where(course_id: <course id>, user_id: <student id>).count 2. Create an assignment. 3. In the gradebook, select the option to show concluded enrollments. 4. Launch the SpeedGrader for the assignment. 5. Enable the Hide student names in the SpeedGrader settings. 6. Refresh the page and verify SpeedGrader loads as expected. And verify you can navigate to the concluded student in SpeedGrader. Change-Id: I265ee97953d1a9ebd098911ed4388b0e1da49af7 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/282581 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> QA-Review: Dustin Cowles <dustin.cowles@instructure.com> Product-Review: Jody Sailor Reviewed-by: Dustin Cowles <dustin.cowles@instructure.com> Reviewed-by: Kai Bjorkman <kbjorkman@instructure.com>
2022-01-12 05:56:41 +08:00
counts.deleted_student_ids << record.user_id
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:)
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:)
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:,
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'), (
#{self.class.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'),
(#{self.class.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 #{AbstractAssignment.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(:submission_lifecycle_manager, e, :warn)
raise Delayed::RetriableError, "Unique record violation when creating new submissions"
rescue ActiveRecord::Deadlocked => e
Canvas::Errors.capture_exception(:submission_lifecycle_manager, e, :warn)
raise Delayed::RetriableError, "Deadlock when upserting submissions"
end
end
def handle_lti_deleted_submissions(batch)
quiz_lti_index = 5
assignments_and_users_query = batch.each_with_object([]) do |entry, memo|
next unless entry[quiz_lti_index]
memo << "(#{entry.first}, #{entry.second})"
end
return if assignments_and_users_query.empty?
submission_join_query = <<~SQL.squish
INNER JOIN (VALUES #{assignments_and_users_query.join(",")})
AS vals(assignment_id, student_id)
ON submissions.assignment_id = vals.assignment_id
AND submissions.user_id = vals.student_id
SQL
submission_query = Submission.deleted.joins(submission_join_query)
submission_versions_to_check = Version
.where(versionable: submission_query)
.order(number: :desc)
.distinct(:versionable_id)
submissions_in_pending_review = submission_versions_to_check
.select { |version| version.model.workflow_state == "pending_review" }
.pluck(:versionable_id)
if submissions_in_pending_review.any?
Submission.where(id: submissions_in_pending_review).update_all(workflow_state: "pending_review")
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