2020-10-27 00:50:13 +08:00
# frozen_string_literal: true
2017-04-28 04:02:40 +08:00
#
# 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/>.
2018-05-12 04:48:25 +08:00
require " anonymity "
2023-06-16 10:40:01 +08:00
class SubmissionLifecycleManager
2018-05-22 06:04:15 +08:00
include Moderation
2023-12-08 03:55:41 +08:00
MAX_RUNNING_JOBS = 10
2018-09-14 00:34:39 +08:00
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
2023-06-16 10:40:01 +08:00
# to be used when SubmissionLifecycleManager is invoked in a callback or a similar
2018-09-14 00:34:39 +08:00
# place where directly specifying an executing user is impractical.
#
2023-06-16 10:40:01 +08:00
# SubmissionLifecycleManager.with_executing_user(a_user) do
2018-09-14 00:34:39 +08:00
# # 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:
2023-06-16 10:40:01 +08:00
# SubmissionLifecycleManager.recompute(assignment, update_grades: true, executing_user: a_user)
2018-09-14 00:34:39 +08:00
#
# 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.
#
2023-06-16 10:40:01 +08:00
# If you are calling SubmissionLifecycleManager in a delayed job of your own making (e.g.,
2018-09-14 00:34:39 +08:00
# 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 )
2019-09-10 04:39:12 +08:00
begin
result = yield
ensure
self . executing_users . pop
end
2018-09-14 00:34:39 +08:00
result
end
def self . current_executing_user
self . executing_users || = [ ]
self . executing_users . last
end
2022-10-04 03:32:08 +08:00
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
2017-11-07 08:03:58 +08:00
2018-09-14 00:34:39 +08:00
def self . recompute ( assignment , update_grades : false , executing_user : nil )
2018-04-24 08:08:42 +08:00
current_caller = caller ( 1 .. 1 ) . first
Rails . logger . debug " DDC.recompute( #{ assignment & . id } ) - #{ current_caller } "
2020-03-27 02:12:31 +08:00
return unless assignment . persisted? && assignment . active?
2021-09-23 00:25:11 +08:00
2018-05-29 21:40:52 +08:00
# 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 : {
2022-03-24 22:29:36 +08:00
singleton : " cached_due_date:calculator:Assignment: #{ assignment . global_id } :UpdateGrades: #{ update_grades ? 1 : 0 } " ,
2018-12-15 01:21:28 +08:00
max_attempts : 10
2018-02-15 23:28:07 +08:00
} ,
2023-06-02 06:06:09 +08:00
update_grades : ,
2018-09-14 00:34:39 +08:00
original_caller : current_caller ,
2023-06-02 06:06:09 +08:00
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
}
2020-10-13 01:06:27 +08:00
recompute_course ( assignment . context , ** opts )
2013-10-10 06:21:57 +08:00
end
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 )
2018-04-24 08:08:42 +08:00
Rails . logger . debug " DDC.recompute_course( #{ course . inspect } , #{ assignments . inspect } , #{ inst_jobs_opts . inspect } ) - #{ original_caller } "
2017-04-19 11:09:10 +08:00
course = Course . find ( course ) unless course . is_a? ( Course )
2018-12-15 01:21:28 +08:00
inst_jobs_opts [ :max_attempts ] || = 10
2022-03-24 22:29:36 +08:00
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
2023-12-05 05:08:18 +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?
2018-09-14 00:34:39 +08:00
executing_user || = current_executing_user
2023-06-16 10:40:01 +08:00
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
2023-06-16 10:40:01 +08:00
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
2023-06-16 10:40:01 +08:00
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
2013-06-01 04:07:26 +08:00
end
2018-02-06 03:24:44 +08:00
def self . recompute_users_for_course ( user_ids , course , assignments = nil , inst_jobs_opts = { } )
2022-04-06 02:31:52 +08:00
opts = inst_jobs_opts . extract! ( :update_grades , :executing_user , :sis_import , :require_singleton ) . reverse_merge ( require_singleton : assignments . nil? )
2018-02-06 03:24:44 +08:00
user_ids = Array ( user_ids )
course = Course . find ( course ) unless course . is_a? ( Course )
2022-04-06 02:31:52 +08:00
update_grades = opts [ :update_grades ] || false
2018-12-15 01:21:28 +08:00
inst_jobs_opts [ :max_attempts ] || = 10
2022-03-24 22:29:36 +08:00
inst_jobs_opts [ :strand ] || = " cached_due_date:calculator:Course: #{ course . global_id } "
2022-04-06 02:31:52 +08:00
if opts [ :require_singleton ]
2022-03-24 22:29:36 +08:00
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 } "
2018-02-06 03:24:44 +08:00
end
2023-12-05 05:08:18 +08:00
assignments || = AbstractAssignment . active . where ( context : course ) . pluck ( :id )
2018-02-06 03:24:44 +08:00
return if assignments . empty?
2018-02-15 23:28:07 +08:00
current_caller = caller ( 1 .. 1 ) . first
2022-04-06 02:31:52 +08:00
executing_user = opts [ :executing_user ] || current_executing_user
if opts [ :sis_import ]
2023-06-16 10:40:01 +08:00
running_jobs_count = Delayed :: Job . running . where ( shard_id : course . shard . id , tag : " SubmissionLifecycleManager # recompute_for_sis_import " ) . count
2022-04-06 02:31:52 +08:00
2023-12-08 03:55:41 +08:00
if running_jobs_count > = MAX_RUNNING_JOBS
2022-04-06 02:31:52 +08:00
# 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 ,
2023-12-08 03:55:41 +08:00
run_at : 10 . seconds . from_now
2022-04-06 02:31:52 +08:00
) . recompute_users_for_course ( user_ids , course , assignments , opts )
else
2023-06-16 10:40:01 +08:00
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
2022-04-06 02:31:52 +08:00
end
end
2018-05-02 05:18:31 +08:00
2023-06-16 10:40:01 +08:00
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
2018-02-06 03:24:44 +08:00
end
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 )
2017-04-19 11:09:10 +08:00
@course = course
2023-12-05 05:08:18 +08:00
@assignment_ids = Array ( assignments ) . map { | a | a . is_a? ( AbstractAssignment ) ? a . id : a }
2022-02-12 00:57:48 +08:00
# ensure we're dealing with local IDs to avoid headaches downstream
if @assignment_ids . present?
@course . shard . activate do
2023-12-05 05:08:18 +08:00
if @assignment_ids . any? { | id | AbstractAssignment . global_id? ( id ) }
@assignment_ids = AbstractAssignment . where ( id : @assignment_ids ) . pluck ( :id )
2022-02-12 00:57:48 +08:00
end
2023-12-05 05:08:18 +08:00
@assignments_auditable_by_id = Set . new ( AbstractAssignment . auditable . where ( id : @assignment_ids ) . pluck ( :id ) )
2022-02-12 00:57:48 +08:00
end
else
@assignments_auditable_by_id = Set . new
end
2018-02-06 03:24:44 +08:00
@user_ids = Array ( user_ids )
2018-02-15 23:28:07 +08:00
@update_grades = update_grades
@original_caller = original_caller
2023-02-28 06:51:04 +08:00
@skip_late_policy_applicator = skip_late_policy_applicator
2018-09-14 00:34:39 +08:00
if executing_user . present?
@executing_user_id = executing_user . is_a? ( User ) ? executing_user . id : executing_user
end
2013-06-01 04:07:26 +08:00
end
2022-04-06 02:31:52 +08:00
# exists so that we can identify (and limit) jobs running specifically for sis imports
2023-06-16 10:40:01 +08:00
# Delayed::Job.where(tag: "SubmissionLifecycleManager#recompute_for_sis_import")
2022-04-06 02:31:52 +08:00
def recompute_for_sis_import
recompute
end
2017-04-19 11:09:10 +08:00
def recompute
2023-06-16 10:40:01 +08:00
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 } "
2018-03-13 04:05:42 +08:00
2017-04-19 11:09:10 +08:00
# in a transaction on the correct shard:
@course . shard . activate do
2017-05-16 03:27:15 +08:00
values = [ ]
2020-03-27 02:12:31 +08:00
2023-12-05 05:08:18 +08:00
assignments_by_id = AbstractAssignment . find ( @assignment_ids ) . index_by ( & :id )
2020-03-27 02:12:31 +08:00
2017-09-15 03:19:56 +08:00
effective_due_dates . to_hash . each do | assignment_id , student_due_dates |
2020-03-27 02:12:31 +08:00
existing_anonymous_ids = existing_anonymous_ids_by_assignment_id [ assignment_id ]
2018-03-13 04:05:42 +08:00
2020-03-27 02:12:31 +08:00
create_moderation_selections_for_assignment ( assignments_by_id [ assignment_id ] , student_due_dates . keys , @user_ids )
2018-05-22 06:04:15 +08:00
2019-07-17 03:01:09 +08:00
quiz_lti = quiz_lti_assignments . include? ( assignment_id )
2019-06-15 07:13:39 +08:00
2022-01-12 05:56:41 +08:00
student_due_dates . each_key do | student_id |
2017-09-15 03:19:56 +08:00
submission_info = student_due_dates [ student_id ]
2022-07-27 05:50:17 +08:00
due_date = submission_info [ :due_at ] ? " ' #{ ActiveRecord :: Base . connection . quoted_date ( submission_info [ :due_at ] . change ( usec : 0 ) ) } '::timestamptz " : " NULL "
2017-05-16 03:27:15 +08:00
grading_period_id = submission_info [ :grading_period_id ] || " NULL "
2018-03-13 04:05:42 +08:00
2018-05-12 04:48:25 +08:00
anonymous_id = Anonymity . generate_id ( existing_ids : existing_anonymous_ids )
2018-03-13 04:05:42 +08:00
existing_anonymous_ids << anonymous_id
sql_ready_anonymous_id = Submission . connection . quote ( anonymous_id )
2020-04-23 23:49:53 +08:00
values << [ assignment_id , student_id , due_date , grading_period_id , sql_ready_anonymous_id , quiz_lti , @course . root_account_id ]
2017-05-16 03:27:15 +08:00
end
end
2018-03-13 04:05:42 +08:00
2020-03-27 02:12:31 +08:00
assignments_to_delete_all_submissions_for = [ ]
2017-06-08 02:32:14 +08:00
# Delete submissions for students who don't have visibility to this assignment anymore
@assignment_ids . each do | assignment_id |
2017-08-29 03:28:04 +08:00
assigned_student_ids = effective_due_dates . find_effective_due_dates_for_assignment ( assignment_id ) . keys
2018-02-06 03:24:44 +08:00
if @user_ids . blank? && assigned_student_ids . blank? && enrollment_counts . prior_student_ids . blank?
2020-03-27 02:12:31 +08:00
assignments_to_delete_all_submissions_for << assignment_id
2017-08-29 03:28:04 +08:00
else
# Delete the users we KNOW we need to delete in batches (it makes the database happier this way)
2017-10-10 04:26:28 +08:00
deletable_student_ids =
enrollment_counts . accepted_student_ids - assigned_student_ids - enrollment_counts . prior_student_ids
2017-08-29 03:28:04 +08:00
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
2023-06-02 06:06:09 +08:00
Submission . active . where ( assignment_id : , user_id : deletable_student_ids_chunk )
2018-12-11 07:09:59 +08:00
. update_all ( workflow_state : :deleted , updated_at : Time . zone . now )
2017-08-29 03:28:04 +08:00
end
2019-05-20 22:12:48 +08:00
User . clear_cache_keys ( deletable_student_ids , :submissions )
2017-08-29 03:28:04 +08:00
end
2017-06-08 02:32:14 +08:00
end
2020-03-31 21:46:02 +08:00
assignments_to_delete_all_submissions_for . each_slice ( 50 ) do | assignment_slice |
2020-03-31 22:28:50 +08:00
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
2020-03-27 02:12:31 +08:00
end
2017-06-08 02:32:14 +08:00
2023-02-03 02:39:29 +08:00
nq_restore_pending_flag_enabled = Account . site_admin . feature_enabled? ( :new_quiz_deleted_workflow_restore_pending_review_state )
2017-08-29 03:28:04 +08:00
# Get any stragglers that might have had their enrollment removed from the course
2017-10-10 04:26:28 +08:00
# 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 )
2018-12-11 07:09:59 +08:00
. update_all ( workflow_state : :deleted , updated_at : Time . zone . now )
2017-10-10 04:26:28 +08:00
end
2019-05-20 22:12:48 +08:00
User . clear_cache_keys ( student_slice , :submissions )
2017-10-10 04:26:28 +08:00
end
2018-03-13 04:05:42 +08:00
2017-05-16 03:27:15 +08:00
return if values . empty?
2018-09-14 00:34:39 +08:00
values = values . sort_by ( & :first )
2017-05-16 03:27:15 +08:00
values . each_slice ( 1000 ) do | batch |
2018-09-14 00:34:39 +08:00
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
2023-02-03 02:39:29 +08:00
if nq_restore_pending_flag_enabled
handle_lti_deleted_submissions ( batch )
end
2018-09-14 00:34:39 +08:00
# prepare values for SQL interpolation
batch_values = batch . map { | entry | " ( #{ entry . join ( " , " ) } ) " }
2020-12-12 05:00:12 +08:00
perform_submission_upsert ( batch_values )
2018-09-14 00:34:39 +08:00
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
)
2017-04-19 11:09:10 +08:00
end
2023-04-13 04:20:50 +08:00
User . clear_cache_keys ( values . pluck ( 1 ) , :submissions )
2017-04-19 11:09:10 +08:00
end
2017-05-03 07:00:21 +08:00
2018-02-15 23:28:07 +08:00
if @update_grades
@course . recompute_student_scores_without_send_later ( @user_ids )
end
2023-02-28 06:51:04 +08:00
if @assignment_ids . size == 1 && ! @skip_late_policy_applicator
2023-12-05 05:08:18 +08:00
# Only changes to LatePolicy or (sometimes) AbstractAssignment records can result in a re-calculation
2017-05-03 07:00:21 +08:00
# of student scores. No changes to the Course record can trigger such re-calculations so
2023-06-16 10:40:01 +08:00
# let's ensure this is triggered only when SubmissionLifecycleManager is called for a Assignment-level
2017-05-03 07:00:21 +08:00
# changes and not for Course-level changes
2023-12-05 05:08:18 +08:00
assignment = @course . shard . activate { AbstractAssignment . find ( @assignment_ids . first ) }
2017-05-03 07:00:21 +08:00
LatePolicyApplicator . for_assignment ( assignment )
end
2013-06-01 04:07:26 +08:00
end
2017-04-19 11:09:10 +08:00
private
2017-10-10 04:26:28 +08:00
EnrollmentCounts = Struct . new ( :accepted_student_ids , :prior_student_ids , :deleted_student_ids )
def enrollment_counts
@enrollment_counts || = begin
counts = EnrollmentCounts . new ( [ ] , [ ] , [ ] )
2017-08-29 03:28:04 +08:00
2020-10-06 06:42:27 +08:00
GuardRail . activate ( :secondary ) do
2017-10-10 04:26:28 +08:00
# The various workflow states below try to mimic similarly named scopes off of course
2018-02-06 03:24:44 +08:00
scope = Enrollment . select (
2017-10-10 04:26:28 +08:00
:user_id ,
2022-01-12 05:56:41 +08:00
" count(nullif(workflow_state not in ('rejected', 'deleted'), false)) as accepted_count " ,
2017-10-10 04:26:28 +08:00
" count(nullif(workflow_state in ('completed'), false)) as prior_count " ,
" count(nullif(workflow_state in ('rejected', 'deleted'), false)) as deleted_count "
)
2018-03-28 08:22:07 +08:00
. where ( course_id : @course , type : [ " StudentEnrollment " , " StudentViewEnrollment " ] )
. group ( :user_id )
2018-02-06 03:24:44 +08:00
scope = scope . where ( user_id : @user_ids ) if @user_ids . present?
2018-03-28 08:22:07 +08:00
scope . find_each do | record |
2022-01-12 05:56:41 +08:00
if record . accepted_count > 0
2022-05-11 22:54:15 +08:00
if record . accepted_count == record . prior_count
counts . prior_student_ids << record . user_id
else
counts . accepted_student_ids << record . user_id
end
2018-02-06 03:24:44 +08:00
else
2022-01-12 05:56:41 +08:00
counts . deleted_student_ids << record . user_id
2018-02-06 03:24:44 +08:00
end
2017-10-10 04:26:28 +08:00
end
end
counts
end
2017-09-15 03:19:56 +08:00
end
2017-04-19 11:09:10 +08:00
def effective_due_dates
2018-02-06 03:24:44 +08:00
@effective_due_dates || = begin
edd = EffectiveDueDates . for_course ( @course , @assignment_ids )
edd . filter_students_to ( @user_ids ) if @user_ids . present?
edd
end
2013-10-17 03:31:11 +08:00
end
2018-09-14 00:34:39 +08:00
def current_cached_due_dates ( entries )
return { } if entries . empty?
2023-06-02 06:06:09 +08:00
entries_for_query = assignment_and_student_id_values ( entries : )
2018-09-14 00:34:39 +08:00
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 : )
2023-06-02 06:06:09 +08:00
entries_for_query = assignment_and_student_id_values ( entries : )
2018-09-14 00:34:39 +08:00
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 << {
2023-06-02 06:06:09 +08:00
assignment_id : ,
submission_id : ,
2018-09-14 00:34:39 +08:00
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
2019-07-17 03:01:09 +08:00
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 )
2021-08-12 06:05:21 +08:00
. 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 )
2019-07-17 03:01:09 +08:00
. where . not ( workflow_state : " deleted " ) . distinct . pluck ( :context_id ) . to_set
end
2020-03-27 02:12:31 +08:00
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
2020-12-12 05:00:12 +08:00
def perform_submission_upsert ( batch_values )
# Construct upsert statement to update existing Submissions or create them if needed.
2021-11-13 03:01:16 +08:00
query = << ~ SQL . squish
2020-12-12 05:00:12 +08:00
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' ) , (
2022-10-04 03:32:08 +08:00
#{self.class.infer_submission_workflow_state_sql}
2020-12-12 05:00:12 +08:00
) ) ,
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' ) ,
2022-10-04 03:32:08 +08:00
( #{self.class.infer_submission_workflow_state_sql})
2020-12-12 05:00:12 +08:00
) ) 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 )
2023-12-05 05:08:18 +08:00
INNER JOIN #{AbstractAssignment.quoted_table_name} assignments
2020-12-12 05:00:12 +08:00
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
2023-06-16 10:40:01 +08:00
Canvas :: Errors . capture_exception ( :submission_lifecycle_manager , e , :warn )
2020-12-12 05:00:12 +08:00
raise Delayed :: RetriableError , " Unique record violation when creating new submissions "
rescue ActiveRecord :: Deadlocked = > e
2023-06-16 10:40:01 +08:00
Canvas :: Errors . capture_exception ( :submission_lifecycle_manager , e , :warn )
2020-12-12 05:00:12 +08:00
raise Delayed :: RetriableError , " Deadlock when upserting submissions "
end
end
2023-02-03 02:39:29 +08:00
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
2013-06-01 04:07:26 +08:00
end