canvas-lms/lib/gradebook_user_ids.rb

204 lines
7.1 KiB
Ruby

#
# Copyright (C) 2017 - 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 GradebookUserIds
def initialize(course, user)
settings = (user.preferences.dig(:gradebook_settings, course.id) || {}).with_indifferent_access
@course = course
@user = user
@include_inactive = settings[:show_inactive_enrollments] == "true"
@include_concluded = settings[:show_concluded_enrollments] == "true"
@column = settings[:sort_rows_by_column_id] || "student"
@sort_by = settings[:sort_rows_by_setting_key] || "name"
@selected_grading_period_id = settings.dig(:filter_columns_by, :grading_period_id)
@selected_section_id = settings.dig(:filter_rows_by, :section_id)
@selected_student_group_id = settings.dig(:filter_rows_by, :student_group_id)
@direction = settings[:sort_rows_by_direction] || "ascending"
end
def user_ids
if @column == "student"
sort_by_student_name
elsif @column =~ /assignment_\d+$/
assignment_id = @column[/\d+$/]
send("sort_by_assignment_#{@sort_by}", assignment_id)
elsif @column =~ /^assignment_group_\d+$/
assignment_id = @column[/\d+$/]
sort_by_assignment_group(assignment_id)
elsif @column == "total_grade"
sort_by_total_grade
else
sort_by_student_name
end
end
private
def sort_by_student_name
students.
order(Arel.sql("enrollments.type = 'StudentViewEnrollment'")).
order_by_sortable_name(direction: @direction.to_sym).
pluck(:id).
uniq
end
def sort_by_assignment_grade(assignment_id)
students.
joins("LEFT JOIN #{Submission.quoted_table_name} ON submissions.user_id=users.id AND
submissions.workflow_state<>'deleted' AND
submissions.assignment_id=#{Submission.connection.quote(assignment_id)}").
order(Arel.sql("enrollments.type = 'StudentViewEnrollment'")).
order(Arel.sql("submissions.score #{sort_direction} NULLS LAST")).
order(Arel.sql("submissions.id IS NULL")).
order_by_sortable_name(direction: @direction.to_sym).
pluck(:id).
uniq
end
def sort_by_assignment_missing(assignment_id)
sort_user_ids(Submission.missing.where(assignment_id: assignment_id))
end
def sort_by_assignment_late(assignment_id)
sort_user_ids(Submission.late.where(assignment_id: assignment_id))
end
def sort_by_total_grade
grading_period_id ? sort_by_scores(:grading_period, grading_period_id) : sort_by_scores(:total_grade)
end
def sort_by_assignment_group(assignment_group_id)
sort_by_scores(:assignment_group, assignment_group_id)
end
def all_user_ids
@all_user_ids ||= students.order_by_sortable_name(direction: @direction.to_sym).pluck(:id).uniq
end
def all_user_ids_index
@all_user_ids_index ||= index_user_ids(all_user_ids)
end
def fake_user_ids
student_enrollments_scope.where(type: "StudentViewEnrollment").pluck(:user_id).uniq
end
def sorted_fake_user_ids
@sorted_fake_user_ids ||= sort_using_index(fake_user_ids, all_user_ids_index)
end
def sorted_real_user_ids
@sorted_real_user_ids ||= sort_using_index(all_user_ids - sorted_fake_user_ids, all_user_ids_index)
end
def real_user_ids_from_submissions(submissions)
submissions.where(user_id: sorted_real_user_ids).pluck(:user_id)
end
def sorted_real_user_ids_from_submissions(submissions)
sort_using_index(real_user_ids_from_submissions(submissions), all_user_ids_index)
end
def sort_user_ids(submissions)
sorted_real_user_ids_from_submissions(submissions).concat(sorted_real_user_ids, sorted_fake_user_ids).uniq
end
def index_user_ids(user_ids)
user_ids_index = {}
# Traverse the array once and cache all indexes so we don't incur traversal costs at the end
user_ids.each_with_index { |item, idx| user_ids_index[item] = idx }
user_ids_index
end
def sort_using_index(user_ids, user_ids_index)
user_ids.sort_by { |item| user_ids_index[item] }
end
def student_enrollments_scope
workflow_states = [:active, :invited]
workflow_states << :inactive if @include_inactive
workflow_states << :completed if @include_concluded || @course.completed?
student_enrollments = @course.enrollments.where(
workflow_state: workflow_states,
type: [:StudentEnrollment, :StudentViewEnrollment]
)
section_ids = section_id ? [section_id] : nil
@course.apply_enrollment_visibility(student_enrollments, @user, section_ids, include: workflow_states)
end
def students
# Because of AR internals (https://github.com/rails/rails/issues/32598),
# we avoid using Arel left_joins here so that sort_by_scores will have
# Enrollment defined.
students = User.
joins("LEFT JOIN #{Enrollment.quoted_table_name} ON enrollments.user_id=users.id").
merge(student_enrollments_scope)
if student_group_id.present?
students.joins(group_memberships: :group).
where(group_memberships: {group: student_group_id, workflow_state: :accepted})
else
students
end
end
def sort_by_scores(type = :total_grade, id = nil)
score_scope = if type == :assignment_group
"scores.assignment_group_id=#{Score.connection.quote(id)}"
elsif type == :grading_period
"scores.grading_period_id=#{Score.connection.quote(id)}"
else
"scores.course_score IS TRUE"
end
# Without doing the score conditions in the join, we lose data. For
# example, we might lose concluded enrollments who don't have a Score.
students.joins("LEFT JOIN #{Score.quoted_table_name} ON scores.enrollment_id=enrollments.id AND
scores.workflow_state='active' AND #{score_scope}").
order(Arel.sql("enrollments.type = 'StudentViewEnrollment'")).
order(Arel.sql("scores.unposted_current_score #{sort_direction} NULLS LAST")).
order_by_sortable_name(direction: @direction.to_sym).
pluck(:id).uniq
end
def sort_direction
@direction == "ascending" ? :asc : :desc
end
def grading_period_id
return nil unless @course.grading_periods?
return nil if @selected_grading_period_id == "0"
if @selected_grading_period_id.nil? || @selected_grading_period_id == "null"
GradingPeriod.current_period_for(@course)&.id
else
@selected_grading_period_id
end
end
def section_id
return nil if @selected_section_id.nil? || @selected_section_id == "null" || @selected_section_id == "0"
@selected_section_id
end
def student_group_id
return nil if @selected_student_group_id.nil? || ["0", "null"].include?(@selected_student_group_id)
Group.active.exists?(id: @selected_student_group_id) ? @selected_student_group_id : nil
end
end