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
|
2017-11-07 08:03:58 +08:00
|
|
|
INFER_SUBMISSION_WORKFLOW_STATE_SQL = <<~SQL_FRAGMENT
|
|
|
|
CASE
|
|
|
|
WHEN grade IS NOT NULL OR excused IS TRUE THEN
|
|
|
|
'graded'
|
|
|
|
WHEN submission_type = 'online_quiz' AND quiz_submission_id IS NOT NULL THEN
|
|
|
|
'pending_review'
|
|
|
|
WHEN submission_type IS NOT NULL AND submitted_at IS NOT NULL THEN
|
|
|
|
'submitted'
|
|
|
|
ELSE
|
|
|
|
'unsubmitted'
|
|
|
|
END
|
|
|
|
SQL_FRAGMENT
|
|
|
|
|
2018-02-15 23:28:07 +08:00
|
|
|
def self.recompute(assignment, update_grades: false)
|
2018-04-24 08:08:42 +08:00
|
|
|
current_caller = caller(1..1).first
|
|
|
|
Rails.logger.debug "DDC.recompute(#{assignment&.id}) - #{current_caller}"
|
2017-06-08 02:32:14 +08:00
|
|
|
return unless assignment.active?
|
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}"
|
2018-02-15 23:28:07 +08:00
|
|
|
},
|
|
|
|
update_grades: update_grades,
|
2018-04-24 08:08:42 +08:00
|
|
|
original_caller: current_caller
|
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)
|
2013-10-10 06:21:57 +08:00
|
|
|
end
|
|
|
|
|
2018-04-24 08:08:42 +08:00
|
|
|
def self.recompute_course(course, assignments: nil, inst_jobs_opts: {}, run_immediately: false, update_grades: false, original_caller: caller(1..1).first)
|
|
|
|
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)
|
|
|
|
inst_jobs_opts[:singleton] ||= "cached_due_date:calculator:Course:#{course.global_id}" if assignments.nil?
|
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-02-15 23:28:07 +08:00
|
|
|
due_date_cacher = new(course, assignments_to_recompute, update_grades: update_grades, original_caller: original_caller)
|
change delayed job behavior when grading period is updated
If a grading period or grading period group is updated, we were
previously creating a delayed-job-per-course for grade recalculation
and a delayed-job-per-course for DueDateCacher recalculation, with no
limit on how many of those jobs could be run in parallel.
Now, we create a delayed-job-per-1000-courses for grade recalculation,
and a delayed-job-per-1000-courses for DueDateCacher recalculation, and
the number of jobs that can be run in parallel are limited with an
n_strand.
closes GRADE-805
Test Plan:
1. Verify cached due dates and scores (grading period and overall) are
recalulated when:
a) Creating, updating, and deleting a Grading Period Set.
b) Creating, updating, and deleting a Grading Period.
2. When creating/updating a Grading Period or a Grading Period Group
such that a score + due date recalculation occurs, verify:
a) Enrollment term delayed jobs
(EnrollmentTerm#recompute_scores_for_batch) are being stranded by
GradingPeriodGroup, and a job is being created for each 1000
courses that need to be operated on in that term.
b) Score recalculation delayed jobs are not being triggered (because
the score recalculation is happening in the delayed job triggered
in step a above).
c) Due date recalculation delayed jobs are not being triggered
(because the due date recalculation is happening in the delayed
job triggered in step a above).
Change-Id: I99610d0559a449ad08b9209646490f7fa1cdf33b
Reviewed-on: https://gerrit.instructure.com/138508
Reviewed-by: Shahbaz Javeed <sjaveed@instructure.com>
Tested-by: Jenkins
Reviewed-by: Derek Bender <djbender@instructure.com>
Product-Review: Keith T. Garner <kgarner@instructure.com>
QA-Review: Keith T. Garner <kgarner@instructure.com>
2018-01-20 04:56:28 +08:00
|
|
|
if run_immediately
|
|
|
|
due_date_cacher.recompute
|
|
|
|
else
|
|
|
|
due_date_cacher.send_later_if_production_enqueue_args(:recompute, inst_jobs_opts)
|
|
|
|
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)
|
|
|
|
if assignments.nil?
|
2018-04-05 20:59:15 +08:00
|
|
|
inst_jobs_opts[:singleton] ||= "cached_due_date:calculator:Users:#{course.global_id}:#{Digest::MD5.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-05-02 05:18:31 +08:00
|
|
|
due_date_cacher = new(course, assignments, user_ids, update_grades: update_grades, original_caller: current_caller)
|
|
|
|
|
|
|
|
run_immediately = inst_jobs_opts.delete(:run_immediately) || false
|
|
|
|
if run_immediately
|
|
|
|
due_date_cacher.recompute
|
|
|
|
else
|
|
|
|
due_date_cacher.send_later_if_production_enqueue_args(:recompute, inst_jobs_opts)
|
|
|
|
end
|
2018-02-06 03:24:44 +08:00
|
|
|
end
|
|
|
|
|
2018-04-24 08:08:42 +08:00
|
|
|
def initialize(course, assignments, user_ids = [], update_grades: false, original_caller: caller(1..1).first)
|
2017-04-19 11:09:10 +08:00
|
|
|
@course = course
|
2017-06-08 02:32:14 +08:00
|
|
|
@assignment_ids = Array(assignments).map { |a| a.is_a?(Assignment) ? a.id : a }
|
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
|
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 = []
|
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
|
|
|
|
existing_anonymous_ids = Submission.where.not(user: nil).
|
|
|
|
where(user: students_without_priors).
|
|
|
|
anonymous_ids_for(assignment_id)
|
|
|
|
|
|
|
|
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)
|
|
|
|
values << [assignment_id, student_id, due_date, grading_period_id, sql_ready_anonymous_id]
|
2017-05-16 03:27:15 +08:00
|
|
|
end
|
|
|
|
end
|
2018-03-13 04:05:42 +08:00
|
|
|
|
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
|
|
|
|
submission_scope = Submission.active.where(assignment_id: assignment_id)
|
|
|
|
|
2018-02-06 03:24:44 +08:00
|
|
|
if @user_ids.blank? && assigned_student_ids.blank? && enrollment_counts.prior_student_ids.blank?
|
2017-08-29 03:28:04 +08:00
|
|
|
submission_scope.in_batches.update_all(workflow_state: :deleted)
|
|
|
|
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
|
|
|
|
submission_scope.where(user_id: deletable_student_ids_chunk).update_all(workflow_state: :deleted)
|
|
|
|
end
|
|
|
|
end
|
2017-06-08 02:32:14 +08:00
|
|
|
end
|
|
|
|
|
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).
|
|
|
|
update_all(workflow_state: :deleted)
|
|
|
|
end
|
|
|
|
end
|
2018-03-13 04:05:42 +08:00
|
|
|
|
2017-05-16 03:27:15 +08:00
|
|
|
return if values.empty?
|
|
|
|
|
2018-03-13 04:05:42 +08:00
|
|
|
# prepare values for SQL interpolation
|
2017-05-16 03:27:15 +08:00
|
|
|
values = values.sort_by(&:first).map { |v| "(#{v.join(',')})" }
|
|
|
|
values.each_slice(1000) do |batch|
|
2017-06-08 02:32:14 +08:00
|
|
|
# Construct upsert statement to update existing Submissions or create them if needed.
|
2018-03-13 04:05:42 +08:00
|
|
|
query = <<~SQL
|
2017-06-08 02:32:14 +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'), (
|
2017-11-07 08:03:58 +08:00
|
|
|
#{INFER_SUBMISSION_WORKFLOW_STATE_SQL}
|
2018-03-13 04:05:42 +08:00
|
|
|
)),
|
|
|
|
anonymous_id = COALESCE(submissions.anonymous_id, vals.anonymous_id)
|
|
|
|
FROM (VALUES #{batch.join(',')})
|
|
|
|
AS vals(assignment_id, student_id, due_date, grading_period_id, anonymous_id)
|
2017-06-08 02:32:14 +08:00
|
|
|
WHERE submissions.user_id = vals.student_id AND
|
|
|
|
submissions.assignment_id = vals.assignment_id;
|
|
|
|
INSERT INTO #{Submission.quoted_table_name}
|
2018-03-13 04:05:42 +08:00
|
|
|
(assignment_id, user_id, workflow_state, created_at, updated_at, context_code, process_attempts,
|
|
|
|
cached_due_date, grading_period_id, anonymous_id)
|
2017-06-08 02:32:14 +08:00
|
|
|
SELECT
|
|
|
|
assignments.id, vals.student_id, 'unsubmitted',
|
|
|
|
now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC',
|
2018-03-13 04:05:42 +08:00
|
|
|
assignments.context_code, 0, vals.due_date::timestamptz, vals.grading_period_id::integer,
|
|
|
|
vals.anonymous_id
|
|
|
|
FROM (VALUES #{batch.join(',')})
|
|
|
|
AS vals(assignment_id, student_id, due_date, grading_period_id, anonymous_id)
|
2017-06-08 02:32:14 +08:00
|
|
|
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
|
2017-04-19 11:09:10 +08:00
|
|
|
|
2017-05-16 03:27:15 +08:00
|
|
|
Assignment.connection.execute(query)
|
2017-04-19 11:09:10 +08:00
|
|
|
end
|
|
|
|
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
|
|
|
|
2017-10-10 04:26:28 +08:00
|
|
|
Shackles.activate(:slave) do
|
|
|
|
# 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"
|
|
|
|
).
|
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|
|
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
|
2013-06-01 04:07:26 +08:00
|
|
|
end
|