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'
2013-06-01 04:07:26 +08:00
class DueDateCacher
2018-05-22 06:04:15 +08:00
include Moderation
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
# to be used when DueDateCacher is invoked in a callback or a similar
# place where directly specifying an executing user is impractical.
#
# DueDateCacher.with_executing_user(a_user) do
# # do something to update due dates, like saving an assignment override
# # any DDC calls that occur while an executing user is set will
# # attribute changes to that user
# end
#
# Users are stored on a stack, so nested calls will work as expected.
# A value of nil may also be passed to indicate that no user should be
# credited (in which case audit events will not be recorded).
#
# You may also specify a user explicitly when calling the class methods:
# DueDateCacher.recompute(assignment, update_grades: true, executing_user: a_user)
#
# An explicitly specified user will take precedence over any users specified
# via with_executing_user, but will not otherwise affect the current "stack"
# of executing users.
#
# If you are calling DueDateCacher in a delayed job of your own making (e.g.,
# Assignment#run_if_overrides_changed_later!), you should pass a user
# explicitly rather than relying on the user stored in with_executing_user
# at the time you create the delayed job.
def self . with_executing_user ( user )
self . executing_users || = [ ]
self . executing_users . push ( user )
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
2021-11-11 03:05:31 +08:00
INFER_SUBMISSION_WORKFLOW_STATE_SQL = << ~ SQL_FRAGMENT
2017-11-07 08:03:58 +08:00
CASE
2019-07-18 06:57:32 +08:00
WHEN grade IS NOT NULL OR excused IS TRUE THEN
2017-11-07 08:03:58 +08:00
'graded'
2019-07-18 06:57:32 +08:00
WHEN submission_type = 'online_quiz' AND quiz_submission_id IS NOT NULL THEN
2017-11-07 08:03:58 +08:00
'pending_review'
2019-07-18 06:57:32 +08:00
WHEN submission_type IS NOT NULL AND submitted_at IS NOT NULL THEN
2017-11-07 08:03:58 +08:00
'submitted'
ELSE
'unsubmitted'
END
SQL_FRAGMENT
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 : {
2018-12-15 01:21:28 +08:00
strand : " cached_due_date:calculator:Course:Assignments: #{ assignment . context . global_id } " ,
max_attempts : 10
2018-02-15 23:28:07 +08:00
} ,
update_grades : update_grades ,
2018-09-14 00:34:39 +08:00
original_caller : current_caller ,
executing_user : executing_user
change delayed job behavior when grading period is updated
If a grading period or grading period group is updated, we were
previously creating a delayed-job-per-course for grade recalculation
and a delayed-job-per-course for DueDateCacher recalculation, with no
limit on how many of those jobs could be run in parallel.
Now, we create a delayed-job-per-1000-courses for grade recalculation,
and a delayed-job-per-1000-courses for DueDateCacher recalculation, and
the number of jobs that can be run in parallel are limited with an
n_strand.
closes GRADE-805
Test Plan:
1. Verify cached due dates and scores (grading period and overall) are
recalulated when:
a) Creating, updating, and deleting a Grading Period Set.
b) Creating, updating, and deleting a Grading Period.
2. When creating/updating a Grading Period or a Grading Period Group
such that a score + due date recalculation occurs, verify:
a) Enrollment term delayed jobs
(EnrollmentTerm#recompute_scores_for_batch) are being stranded by
GradingPeriodGroup, and a job is being created for each 1000
courses that need to be operated on in that term.
b) Score recalculation delayed jobs are not being triggered (because
the score recalculation is happening in the delayed job triggered
in step a above).
c) Due date recalculation delayed jobs are not being triggered
(because the due date recalculation is happening in the delayed
job triggered in step a above).
Change-Id: I99610d0559a449ad08b9209646490f7fa1cdf33b
Reviewed-on: https://gerrit.instructure.com/138508
Reviewed-by: Shahbaz Javeed <sjaveed@instructure.com>
Tested-by: Jenkins
Reviewed-by: Derek Bender <djbender@instructure.com>
Product-Review: Keith T. Garner <kgarner@instructure.com>
QA-Review: Keith T. Garner <kgarner@instructure.com>
2018-01-20 04:56:28 +08:00
}
2020-10-13 01:06:27 +08:00
recompute_course ( assignment . context , ** opts )
2013-10-10 06:21:57 +08:00
end
2018-09-14 00:34:39 +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 )
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
2018-05-29 21:40:52 +08:00
inst_jobs_opts [ :singleton ] || = " cached_due_date:calculator:Course: #{ course . global_id } " if assignments . nil? && ! inst_jobs_opts [ :strand ]
change delayed job behavior when grading period is updated
If a grading period or grading period group is updated, we were
previously creating a delayed-job-per-course for grade recalculation
and a delayed-job-per-course for DueDateCacher recalculation, with no
limit on how many of those jobs could be run in parallel.
Now, we create a delayed-job-per-1000-courses for grade recalculation,
and a delayed-job-per-1000-courses for DueDateCacher recalculation, and
the number of jobs that can be run in parallel are limited with an
n_strand.
closes GRADE-805
Test Plan:
1. Verify cached due dates and scores (grading period and overall) are
recalulated when:
a) Creating, updating, and deleting a Grading Period Set.
b) Creating, updating, and deleting a Grading Period.
2. When creating/updating a Grading Period or a Grading Period Group
such that a score + due date recalculation occurs, verify:
a) Enrollment term delayed jobs
(EnrollmentTerm#recompute_scores_for_batch) are being stranded by
GradingPeriodGroup, and a job is being created for each 1000
courses that need to be operated on in that term.
b) Score recalculation delayed jobs are not being triggered (because
the score recalculation is happening in the delayed job triggered
in step a above).
c) Due date recalculation delayed jobs are not being triggered
(because the due date recalculation is happening in the delayed
job triggered in step a above).
Change-Id: I99610d0559a449ad08b9209646490f7fa1cdf33b
Reviewed-on: https://gerrit.instructure.com/138508
Reviewed-by: Shahbaz Javeed <sjaveed@instructure.com>
Tested-by: Jenkins
Reviewed-by: Derek Bender <djbender@instructure.com>
Product-Review: Keith T. Garner <kgarner@instructure.com>
QA-Review: Keith T. Garner <kgarner@instructure.com>
2018-01-20 04:56:28 +08:00
assignments_to_recompute = assignments || Assignment . active . where ( context : course ) . pluck ( :id )
return if assignments_to_recompute . empty?
2018-09-14 00:34:39 +08:00
executing_user || = self . current_executing_user
due_date_cacher = new ( course , assignments_to_recompute , update_grades : update_grades , original_caller : original_caller , executing_user : executing_user )
change delayed job behavior when grading period is updated
If a grading period or grading period group is updated, we were
previously creating a delayed-job-per-course for grade recalculation
and a delayed-job-per-course for DueDateCacher recalculation, with no
limit on how many of those jobs could be run in parallel.
Now, we create a delayed-job-per-1000-courses for grade recalculation,
and a delayed-job-per-1000-courses for DueDateCacher recalculation, and
the number of jobs that can be run in parallel are limited with an
n_strand.
closes GRADE-805
Test Plan:
1. Verify cached due dates and scores (grading period and overall) are
recalulated when:
a) Creating, updating, and deleting a Grading Period Set.
b) Creating, updating, and deleting a Grading Period.
2. When creating/updating a Grading Period or a Grading Period Group
such that a score + due date recalculation occurs, verify:
a) Enrollment term delayed jobs
(EnrollmentTerm#recompute_scores_for_batch) are being stranded by
GradingPeriodGroup, and a job is being created for each 1000
courses that need to be operated on in that term.
b) Score recalculation delayed jobs are not being triggered (because
the score recalculation is happening in the delayed job triggered
in step a above).
c) Due date recalculation delayed jobs are not being triggered
(because the due date recalculation is happening in the delayed
job triggered in step a above).
Change-Id: I99610d0559a449ad08b9209646490f7fa1cdf33b
Reviewed-on: https://gerrit.instructure.com/138508
Reviewed-by: Shahbaz Javeed <sjaveed@instructure.com>
Tested-by: Jenkins
Reviewed-by: Derek Bender <djbender@instructure.com>
Product-Review: Keith T. Garner <kgarner@instructure.com>
QA-Review: Keith T. Garner <kgarner@instructure.com>
2018-01-20 04:56:28 +08:00
if run_immediately
due_date_cacher . recompute
else
2020-10-23 07:32:48 +08:00
due_date_cacher . delay_if_production ( ** inst_jobs_opts ) . recompute
change delayed job behavior when grading period is updated
If a grading period or grading period group is updated, we were
previously creating a delayed-job-per-course for grade recalculation
and a delayed-job-per-course for DueDateCacher recalculation, with no
limit on how many of those jobs could be run in parallel.
Now, we create a delayed-job-per-1000-courses for grade recalculation,
and a delayed-job-per-1000-courses for DueDateCacher recalculation, and
the number of jobs that can be run in parallel are limited with an
n_strand.
closes GRADE-805
Test Plan:
1. Verify cached due dates and scores (grading period and overall) are
recalulated when:
a) Creating, updating, and deleting a Grading Period Set.
b) Creating, updating, and deleting a Grading Period.
2. When creating/updating a Grading Period or a Grading Period Group
such that a score + due date recalculation occurs, verify:
a) Enrollment term delayed jobs
(EnrollmentTerm#recompute_scores_for_batch) are being stranded by
GradingPeriodGroup, and a job is being created for each 1000
courses that need to be operated on in that term.
b) Score recalculation delayed jobs are not being triggered (because
the score recalculation is happening in the delayed job triggered
in step a above).
c) Due date recalculation delayed jobs are not being triggered
(because the due date recalculation is happening in the delayed
job triggered in step a above).
Change-Id: I99610d0559a449ad08b9209646490f7fa1cdf33b
Reviewed-on: https://gerrit.instructure.com/138508
Reviewed-by: Shahbaz Javeed <sjaveed@instructure.com>
Tested-by: Jenkins
Reviewed-by: Derek Bender <djbender@instructure.com>
Product-Review: Keith T. Garner <kgarner@instructure.com>
QA-Review: Keith T. Garner <kgarner@instructure.com>
2018-01-20 04:56:28 +08:00
end
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 = { } )
user_ids = Array ( user_ids )
course = Course . find ( course ) unless course . is_a? ( Course )
2018-12-15 01:21:28 +08:00
inst_jobs_opts [ :max_attempts ] || = 10
2018-02-06 03:24:44 +08:00
if assignments . nil?
2021-03-31 03:15:39 +08:00
inst_jobs_opts [ :singleton ] || = " cached_due_date:calculator:Users: #{ course . global_id } : #{ Digest :: SHA256 . hexdigest ( user_ids . sort . join ( ':' ) ) } "
2018-02-06 03:24:44 +08:00
end
assignments || = Assignment . active . where ( context : course ) . pluck ( :id )
return if assignments . empty?
2018-02-15 23:28:07 +08:00
current_caller = caller ( 1 .. 1 ) . first
update_grades = inst_jobs_opts . delete ( :update_grades ) || false
2018-09-14 00:34:39 +08:00
executing_user = inst_jobs_opts . delete ( :executing_user ) || self . current_executing_user
due_date_cacher = new ( course , assignments , user_ids , update_grades : update_grades , original_caller : current_caller , executing_user : executing_user )
2018-05-02 05:18:31 +08:00
2021-01-29 06:14:48 +08:00
due_date_cacher . delay_if_production ( ** inst_jobs_opts ) . recompute
2018-02-06 03:24:44 +08:00
end
2018-09-14 00:34:39 +08:00
def initialize ( course , assignments , user_ids = [ ] , update_grades : false , original_caller : caller ( 1 .. 1 ) . first , executing_user : nil )
2017-04-19 11:09:10 +08:00
@course = course
2018-09-14 00:34:39 +08:00
2017-06-08 02:32:14 +08:00
@assignment_ids = Array ( assignments ) . map { | a | a . is_a? ( Assignment ) ? a . id : a }
2018-09-14 00:34:39 +08:00
@assignments_auditable_by_id = if @assignment_ids . present?
2021-09-23 00:25:11 +08:00
Set . new ( Assignment . auditable . where ( id : @assignment_ids ) . pluck ( :id ) )
else
Set . new
end
2018-09-14 00:34:39 +08:00
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
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
2017-04-19 11:09:10 +08:00
def recompute
2018-03-06 02:24:06 +08:00
Rails . logger . debug " DUE DATE CACHER STARTS: #{ Time . zone . now . to_i } "
Rails . logger . debug " DDC # recompute() - original caller: #{ @original_caller } "
Rails . logger . debug " DDC # recompute() - current caller: #{ caller ( 1 .. 1 ) . first } "
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
assignments_by_id = Assignment . find ( @assignment_ids ) . index_by ( & :id )
2017-09-15 03:19:56 +08:00
effective_due_dates . to_hash . each do | assignment_id , student_due_dates |
2018-03-13 04:05:42 +08:00
students_without_priors = student_due_dates . keys - enrollment_counts . prior_student_ids
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
2018-03-13 04:05:42 +08:00
students_without_priors . each do | student_id |
2017-09-15 03:19:56 +08:00
submission_info = student_due_dates [ student_id ]
2017-05-16 03:27:15 +08:00
due_date = submission_info [ :due_at ] ? " ' #{ submission_info [ :due_at ] . iso8601 } '::timestamptz " : 'NULL'
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
2021-09-23 00:25:11 +08:00
Submission . active . where ( assignment_id : assignment_id , user_id : deletable_student_ids_chunk )
. 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
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 |
2021-09-23 00:25:11 +08:00
Submission . active
. where ( assignment_id : assignment_ids_slice , user_id : student_slice )
. 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
# 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
2021-09-23 00:25:11 +08:00
User . clear_cache_keys ( values . map { | v | v [ 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
2017-06-08 02:32:14 +08:00
if @assignment_ids . size == 1
2017-05-03 07:00:21 +08:00
# Only changes to LatePolicy or (sometimes) Assignment records can result in a re-calculation
# of student scores. No changes to the Course record can trigger such re-calculations so
# let's ensure this is triggered only when DueDateCacher is called for a Assignment-level
# changes and not for Course-level changes
2017-06-08 02:32:14 +08:00
assignment = Assignment . 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 ,
" count(nullif(workflow_state not in ('rejected', 'deleted', 'completed'), false)) as accepted_count " ,
" count(nullif(workflow_state in ('completed'), false)) as prior_count " ,
" count(nullif(workflow_state in ('rejected', 'deleted'), false)) as deleted_count "
2021-09-23 00:25:11 +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 |
2018-02-06 03:24:44 +08:00
if record . accepted_count == 0 && record . deleted_count > 0
counts . deleted_student_ids << record . user_id
elsif record . accepted_count == 0 && record . prior_count > 0
counts . prior_student_ids << record . user_id
elsif record . accepted_count > 0
counts . accepted_student_ids << record . user_id
else
raise " Unknown enrollment state: #{ record . accepted_count } , #{ record . prior_count } , #{ record . deleted_count } "
end
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?
entries_for_query = assignment_and_student_id_values ( entries : entries )
2021-09-23 00:25:11 +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 )
2018-09-14 00:34:39 +08:00
submissions_with_due_dates . each_with_object ( { } ) do | ( submission_id , cached_due_date ) , map |
map [ submission_id ] = cached_due_date
end
end
def record_due_date_changes_for_auditable_assignments! ( entries : , previous_cached_dates : )
entries_for_query = assignment_and_student_id_values ( entries : entries )
2021-09-23 00:25:11 +08:00
updated_submissions = Submission . where ( " (assignment_id, user_id) IN ( #{ entries_for_query . join ( ',' ) } ) " )
. pluck ( :id , :assignment_id , :cached_due_date )
2018-09-14 00:34:39 +08:00
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
2021-09-23 00:25:11 +08:00
payload = { due_at : [ old_due_date & . iso8601 , new_due_date & . iso8601 ] }
2018-09-14 00:34:39 +08:00
records << {
assignment_id : assignment_id ,
submission_id : submission_id ,
user_id : @executing_user_id ,
event_type : 'submission_updated' ,
payload : payload . to_json ,
created_at : timestamp ,
updated_at : timestamp
}
end
AnonymousOrModerationEvent . bulk_insert ( records_to_insert )
end
def assignment_and_student_id_values ( entries : )
entries . map { | ( assignment_id , student_id ) | " ( #{ assignment_id } , #{ student_id } ) " }
end
def record_due_date_changed_events?
# Only audit if we have a user and at least one auditable assignment
@record_due_date_changed_events || = @executing_user_id . present? && @assignments_auditable_by_id . present?
end
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 || =
2021-09-23 00:25:11 +08:00
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
2019-07-17 03:01:09 +08:00
end
2020-03-27 02:12:31 +08:00
def existing_anonymous_ids_by_assignment_id
@existing_anonymous_ids_by_assignment_id || =
2021-09-23 00:25:11 +08:00
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 }
2020-03-27 02:12:31 +08:00
end
2020-12-12 05:00:12 +08:00
def perform_submission_upsert ( batch_values )
2021-09-23 00:25:11 +08:00
# Construct upsert statement to update existing Submissions or create them if needed.
2021-11-13 03:01:16 +08:00
query = << ~ SQL . squish
2021-09-23 00:25:11 +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' ) , (
#{INFER_SUBMISSION_WORKFLOW_STATE_SQL}
) ) ,
anonymous_id = COALESCE ( submissions . anonymous_id , vals . anonymous_id ) ,
cached_quiz_lti = vals . cached_quiz_lti ,
updated_at = now ( ) AT TIME ZONE 'UTC'
FROM ( VALUES #{batch_values.join(',')})
AS vals ( assignment_id , student_id , due_date , grading_period_id , anonymous_id , cached_quiz_lti , root_account_id )
WHERE submissions . user_id = vals . student_id AND
submissions . assignment_id = vals . assignment_id AND
(
( submissions . cached_due_date IS DISTINCT FROM vals . due_date :: timestamptz ) OR
( submissions . grading_period_id IS DISTINCT FROM vals . grading_period_id :: integer ) OR
( submissions . workflow_state < > COALESCE ( NULLIF ( submissions . workflow_state , 'deleted' ) ,
( #{INFER_SUBMISSION_WORKFLOW_STATE_SQL})
) ) OR
( submissions . anonymous_id IS DISTINCT FROM COALESCE ( submissions . anonymous_id , vals . anonymous_id ) ) OR
( submissions . cached_quiz_lti IS DISTINCT FROM vals . cached_quiz_lti )
) ;
INSERT INTO #{Submission.quoted_table_name}
( assignment_id , user_id , workflow_state , created_at , updated_at , course_id ,
cached_due_date , grading_period_id , anonymous_id , cached_quiz_lti , root_account_id )
SELECT
assignments . id , vals . student_id , 'unsubmitted' ,
now ( ) AT TIME ZONE 'UTC' , now ( ) AT TIME ZONE 'UTC' ,
assignments . context_id , vals . due_date :: timestamptz , vals . grading_period_id :: integer ,
vals . anonymous_id ,
vals . cached_quiz_lti ,
vals . root_account_id
FROM ( VALUES #{batch_values.join(',')})
AS vals ( assignment_id , student_id , due_date , grading_period_id , anonymous_id , cached_quiz_lti , root_account_id )
INNER JOIN #{Assignment.quoted_table_name} assignments
ON assignments . id = vals . assignment_id
LEFT OUTER JOIN #{Submission.quoted_table_name} submissions
ON submissions . assignment_id = assignments . id
AND submissions . user_id = vals . student_id
WHERE submissions . id IS NULL ;
SQL
2020-12-12 05:00:12 +08:00
begin
Submission . transaction do
Submission . connection . execute ( query )
end
rescue ActiveRecord :: RecordNotUnique = > e
Canvas :: Errors . capture_exception ( :due_date_cacher , e , :warn )
raise Delayed :: RetriableError , " Unique record violation when creating new submissions "
rescue ActiveRecord :: Deadlocked = > e
Canvas :: Errors . capture_exception ( :due_date_cacher , e , :warn )
raise Delayed :: RetriableError , " Deadlock when upserting submissions "
end
end
2013-06-01 04:07:26 +08:00
end