canvas-lms/lib/sorts_assignments.rb

127 lines
5.1 KiB
Ruby

# frozen_string_literal: true
#
# Copyright (C) 2023 - 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/>.
class SortsAssignments
class InvalidBucketError < StandardError; end
VALID_BUCKETS = %i[past overdue undated ungraded unsubmitted upcoming future].freeze
def initialize(assignments_scope:, user:, session:, course:, requested_user: nil, include_discussion_checkpoints: false)
@assignments_scope = assignments_scope
@user = user
@session = session
@course = course
@requested_user = requested_user
@include_discussion_checkpoints = include_discussion_checkpoints
end
def assignments(bucket, &)
raise InvalidBucketError if VALID_BUCKETS.exclude?(bucket)
@now = Time.zone.now
filter(bucket, &)
end
private
def filter(bucket)
assignments_in_bucket = buckets.fetch(bucket).call(assignments_for_students)
if block_given?
yield assignments_in_bucket
else
@assignments_scope.where(id: assignments_in_bucket)
end
end
def buckets
{
past: filters[:has_date] >> filters[:past_due],
overdue: (
filters[:has_date] >> filters[:past_due] >>
filters[:expects_submission].call(additional_excludes: %w[external_tool online_quiz attendance]) >>
filters[:not_submitted_or_graded] >> filters[:can_submit]
),
undated: filters[:has_no_date],
ungraded: filters[:expects_submission].call >> filters[:needs_grading],
unsubmitted: filters[:expects_submission].call >> filters[:not_submitted_or_graded],
upcoming: filters[:has_date] >> filters[:due_soon],
future: filters[:due_in_future]
}
end
def filters
@filters ||= {
has_no_date: ->(scope) { scope.where(submissions: { cached_due_date: nil }) },
has_date: ->(scope) { scope.where.not(submissions: { cached_due_date: nil }) },
past_due: ->(scope) { scope.where(submissions: { cached_due_date: ...@now }) },
due_in_future: ->(scope) { scope.where("submissions.cached_due_date IS NULL OR submissions.cached_due_date >= ?", @now) },
due_soon: ->(scope) { scope.where(submissions: { cached_due_date: @now..1.week.from_now(@now) }) },
expects_submission: lambda do |additional_excludes: ["external_tool"]|
->(scope) { scope.expecting_submission(additional_excludes:) }
end,
needs_grading: ->(scope) { scope.where(Submission.needs_grading_conditions) },
not_submitted_or_graded: ->(scope) { scope.merge(Submission.not_submitted_or_graded) },
can_submit: lambda do |scope|
students_by_id = students.index_by(&:id)
students_by_assignment_id = scope.pluck("submissions.assignment_id", "submissions.user_id").each_with_object({}) do |(assignment_id, user_id), acc|
acc[assignment_id] ||= []
acc[assignment_id] << students_by_id[user_id]
end
assignments = @course.assignments.except(:order).where(id: students_by_assignment_id.keys).select do |assignment|
submittable_by_any_student?(assignment, students_by_assignment_id[assignment.id])
end
@course.assignments.where(id: assignments).except(:order)
end
}
end
def submittable_by_any_student?(assignment, students)
students.any? { |student| student.present? && assignment.grants_right?(student, :submit) }
end
def assignments_for_students
course_assignments = @course.assignments.where(id: assignment_ids)
if @include_discussion_checkpoints
# We need to use AbstractAssignment instead of its subclass Assignment so that we can merge the query with checkpoint_assignments
assignments = AbstractAssignment.where(id: course_assignments)
checkpoint_assignments = SubAssignment.active.where(parent_assignment_id: course_assignments)
course_assignments = assignments.or(checkpoint_assignments)
end
course_assignments.except(:order).joins(:submissions).where(submissions: { user: students })
end
def assignment_ids
@assignment_ids ||= @assignments_scope.pluck(:id)
end
def students
@students ||= if @requested_user.present? && @user != @requested_user
[@requested_user]
elsif @course.grants_right?(@user, @session, :read_as_admin)
@course.students_visible_to(@user).merge(Enrollment.of_student_type).distinct.to_a
elsif @course.observers.where(id: @user).exists?
ObserverEnrollment.observed_students(@course, @user).keys
else
[@user]
end
end
end