366 lines
12 KiB
Ruby
366 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
#
|
|
# 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/>.
|
|
#
|
|
|
|
class GradeSummaryPresenter
|
|
attr_reader :groups_assignments, :assignment_order
|
|
|
|
class << self
|
|
def cache_key(context, method)
|
|
["grade_summary_presenter", context, method].cache_key
|
|
end
|
|
end
|
|
|
|
def initialize(context, current_user, id_param, assignment_order: :due_at)
|
|
@context = context
|
|
@current_user = current_user
|
|
@id_param = id_param
|
|
@groups_assignments = []
|
|
@periods_assignments = []
|
|
@assignment_order = assignment_order
|
|
end
|
|
|
|
def user_has_elevated_permissions?
|
|
@context.grants_any_right?(@current_user, :manage_grades, :view_all_grades)
|
|
end
|
|
|
|
def user_needs_redirection?
|
|
user_has_elevated_permissions? && !@id_param
|
|
end
|
|
|
|
def user_an_observer_of_student?
|
|
observed_students.key? student
|
|
end
|
|
|
|
def student_is_user?
|
|
student == @current_user
|
|
end
|
|
|
|
def multiple_observed_students?
|
|
observed_students && observed_students.keys.length > 1
|
|
end
|
|
|
|
def has_courses_with_grades?
|
|
courses_with_grades && courses_with_grades.length > 1
|
|
end
|
|
|
|
def editable?
|
|
student_is_user? && !no_calculations?
|
|
end
|
|
|
|
def turnitin_enabled?
|
|
unless defined?(@turnitin_enabled)
|
|
@turnitin_enabled = @context.turnitin_enabled? && assignments.any?(&:turnitin_enabled)
|
|
end
|
|
@turnitin_enabled
|
|
end
|
|
|
|
def vericite_enabled?
|
|
unless defined?(@vericite_enabled)
|
|
@vericite_enabled = @context.vericite_enabled? && assignments.any?(&:vericite_enabled)
|
|
end
|
|
@vericite_enabled
|
|
end
|
|
|
|
def observed_students
|
|
@observed_students ||= ObserverEnrollment.observed_students(@context, @current_user, include_restricted_access: false)
|
|
end
|
|
|
|
def observed_student
|
|
# be consistent about which student we return by default
|
|
(observed_students.to_a.min_by { |e| e[0].sortable_name })[1].first
|
|
end
|
|
|
|
def linkable_observed_students
|
|
observed_students.keys.select { |student| observed_students[student].all? { |e| e.grants_right?(@current_user, :read_grades) } }
|
|
end
|
|
|
|
def student_enrollment_for(course, user)
|
|
enrollment = course.all_student_enrollments.where(user_id: user)
|
|
enrollment = enrollment.where.not(workflow_state: "inactive") unless user_has_elevated_permissions?
|
|
enrollment.first
|
|
end
|
|
|
|
def student_enrollment
|
|
@student_enrollment ||= if @id_param # always use id if given
|
|
validate_id
|
|
user_id = Shard.relative_id_for(@id_param, @context.shard, @context.shard)
|
|
@context.shard.activate { student_enrollment_for(@context, user_id) }
|
|
elsif observed_students.present? # otherwise try to find an observed student
|
|
observed_student
|
|
else # or just fall back to @current_user
|
|
@context.shard.activate { student_enrollment_for(@context, @current_user) }
|
|
end
|
|
end
|
|
|
|
def students
|
|
if multiple_observed_students?
|
|
linkable_observed_students
|
|
else
|
|
Array.wrap(student)
|
|
end
|
|
end
|
|
|
|
def validate_id
|
|
raise ActiveRecord::RecordNotFound if !@id_param.is_a?(User) && (@id_param.to_s =~ Api::ID_REGEX).nil?
|
|
|
|
true
|
|
end
|
|
|
|
def student
|
|
@student ||= student_enrollment&.user
|
|
end
|
|
|
|
def student_name
|
|
student ? student.name : nil
|
|
end
|
|
|
|
def student_id
|
|
student ? student.id : nil
|
|
end
|
|
|
|
def groups
|
|
all_groups
|
|
end
|
|
|
|
def assignments
|
|
@assignments ||= begin
|
|
visible_assignments = assignments_for_student
|
|
overridden_assignments = assignments_overridden_for_student(visible_assignments)
|
|
sorted_assignments(overridden_assignments)
|
|
end
|
|
end
|
|
|
|
def assignments_for_student
|
|
includes = [:assignment_overrides, :post_policy]
|
|
includes << :assignment_group if @assignment_order == :assignment_group
|
|
|
|
# AssignmentGroup#visible_assignments returns all published assignments if you pass it
|
|
# a nil user. On the other hand, if you pass it a student, it returns only assignments
|
|
# visible to that student.
|
|
#
|
|
# The logic here is needed in order to ensure that teachers can view assignment grades
|
|
# for deactivated students (who themselves can not view those assignments).
|
|
assignments = if user_has_elevated_permissions?
|
|
AssignmentGroup
|
|
.visible_assignments(nil, @context, all_groups, includes: includes)
|
|
.assigned_to_student(student.id)
|
|
else
|
|
AssignmentGroup.visible_assignments(student, @context, all_groups, includes: includes)
|
|
end
|
|
|
|
assignments.where.not(submission_types: %w[not_graded wiki_page]).except(:order)
|
|
end
|
|
|
|
def assignments_overridden_for_student(assignments)
|
|
group_index = all_groups.index_by(&:id)
|
|
assignments.map do |assignment|
|
|
assignment.context = @context
|
|
assignment.assignment_group = group_index.fetch(assignment.assignment_group_id)
|
|
assignment.overridden_for(student)
|
|
end
|
|
end
|
|
|
|
def sorted_assignments(assignments)
|
|
case @assignment_order
|
|
when :due_at
|
|
assignments.sort_by { |a| [a.due_at || CanvasSort::Last, Canvas::ICU.collation_key(a.title)] }
|
|
when :title
|
|
Canvas::ICU.collate_by(assignments, &:title)
|
|
when :module
|
|
sorted_by_modules(assignments)
|
|
when :assignment_group
|
|
assignments.sort_by { |a| [a.assignment_group.position, a.position].map { |p| p || CanvasSort::Last } }
|
|
end
|
|
end
|
|
|
|
def sort_options
|
|
options = [[I18n.t("Due Date"), "due_at"], [I18n.t("Name"), "title"]]
|
|
if @context.active_record_types[:assignments] && assignments.uniq(&:assignment_group_id).length > 1
|
|
options << [I18n.t("Assignment Group"), "assignment_group"]
|
|
end
|
|
options << [I18n.t("Module"), "module"] if @context.active_record_types[:modules]
|
|
Canvas::ICU.collate_by(options, &:first)
|
|
end
|
|
|
|
def submissions
|
|
@submissions ||= begin
|
|
ss = @context.submissions
|
|
.preload(
|
|
:visible_submission_comments,
|
|
{ rubric_assessments: [:rubric, :rubric_association] },
|
|
:content_participations,
|
|
{ assignment: [:context, :post_policy] }
|
|
)
|
|
.joins(:assignment)
|
|
.where("assignments.workflow_state != 'deleted'")
|
|
.where(user_id: student).to_a
|
|
|
|
if vericite_enabled? || turnitin_enabled?
|
|
ActiveRecord::Associations::Preloader.new.preload(ss, :originality_reports)
|
|
end
|
|
|
|
assignments_index = assignments.index_by(&:id)
|
|
|
|
# preload submission comment stuff
|
|
comments = ss.map do |s|
|
|
assign = assignments_index[s.assignment_id]
|
|
s.assignment = assign if assign.present?
|
|
|
|
s.visible_submission_comments.map do |c|
|
|
c.submission = s
|
|
c
|
|
end
|
|
end.flatten
|
|
SubmissionComment.preload_attachments comments
|
|
|
|
ss
|
|
end
|
|
end
|
|
|
|
def rubric_assessments
|
|
assignment_presenters.flat_map(&:rubric_assessments)
|
|
end
|
|
|
|
def rubrics
|
|
rubric_assessments.map(&:rubric).uniq
|
|
end
|
|
|
|
def assignment_stats
|
|
@stats ||= ScoreStatistic.where(assignment: @context.assignments.active.except(:order)).index_by(&:assignment_id)
|
|
end
|
|
|
|
def assignment_presenters
|
|
submission_index = submissions.index_by(&:assignment_id)
|
|
assignments.map do |a|
|
|
GradeSummaryAssignmentPresenter.new(self, @current_user, a, submission_index[a.id])
|
|
end
|
|
end
|
|
|
|
def hidden_submissions_for_published_assignments?
|
|
submissions_with_published_assignment = submissions.select { |submission| submission.assignment.published? }
|
|
submissions_with_published_assignment.any? do |sub|
|
|
return !sub.posted? if sub.assignment.post_manually?
|
|
|
|
sub.graded? && !sub.posted?
|
|
end
|
|
end
|
|
|
|
def courses_with_grades
|
|
@courses_with_grades ||= student.shard.activate do
|
|
course_list = if student_is_user?
|
|
Course.preload(:enrollment_term, :grading_period_groups)
|
|
.where(id: student.participating_student_current_and_concluded_course_ids).to_a
|
|
elsif user_an_observer_of_student?
|
|
observed_courses = []
|
|
Shard.partition_by_shard(student.participating_student_current_and_concluded_course_ids) do |course_ids|
|
|
observed_course_ids = ObserverEnrollment
|
|
.not_deleted
|
|
.where(course_id: course_ids, user_id: @current_user, associated_user_id: student)
|
|
.pluck(:course_id)
|
|
next unless observed_course_ids.any?
|
|
|
|
observed_courses += Course.preload(:enrollment_term, :grading_period_groups)
|
|
.where(id: observed_course_ids).to_a
|
|
end
|
|
observed_courses
|
|
else
|
|
[]
|
|
end
|
|
|
|
course_list.select { |c| c.grants_right?(student, :read) }
|
|
end
|
|
end
|
|
|
|
def unread_submission_ids
|
|
@unread_submission_ids ||= if student_is_user?
|
|
subs = submissions.select { |s| s.unread?(@current_user) }
|
|
subs.map(&:id)
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
def no_calculations?
|
|
@groups_assignments.empty? && @periods_assignments.empty?
|
|
end
|
|
|
|
def total_weight
|
|
@total_weight ||= if @context.group_weighting_scheme == "percent"
|
|
groups.sum(&:group_weight)
|
|
else
|
|
0
|
|
end
|
|
end
|
|
|
|
def groups_assignments=(value)
|
|
@groups_assignments = value
|
|
assignments.concat(value)
|
|
end
|
|
|
|
def periods_assignments=(value)
|
|
@periods_assignments = value
|
|
assignments.concat(value)
|
|
end
|
|
|
|
def grading_periods
|
|
@all_grading_periods ||= GradingPeriod.for(@context).order(:start_date).to_a
|
|
end
|
|
|
|
def show_updated_plagiarism_icons?(plagiarism_data)
|
|
plagiarism_data.present? && @context.root_account.feature_enabled?(:new_gradebook_plagiarism_indicator)
|
|
end
|
|
|
|
private
|
|
|
|
def all_groups
|
|
@all_groups ||= @context.assignment_groups.active.to_a
|
|
end
|
|
|
|
def sorted_by_modules(assignments)
|
|
Assignment.preload_context_module_tags(assignments, include_context_modules: true)
|
|
assignments.sort do |a, b|
|
|
a_tags = a.all_context_module_tags
|
|
b_tags = b.all_context_module_tags
|
|
# assignments without modules come after assignments with modules
|
|
next -1 if a_tags.present? && b_tags.empty?
|
|
next 1 if a_tags.empty? && b_tags.present?
|
|
# if both assignments do not belong to a module, compare by
|
|
# assignment title
|
|
next a.title.downcase <=> b.title.downcase if a_tags.empty? && b_tags.empty?
|
|
|
|
# if both assignments belong to modules, compare the module
|
|
# position of the first module they each belong to
|
|
compare_by_module_position(a_tags.first, b_tags.first)
|
|
end
|
|
end
|
|
|
|
def compare_by_module_position(module_tag1, module_tag2)
|
|
module_position_comparison =
|
|
module_tag1.context_module.position <=> module_tag2.context_module.position
|
|
# if module position above is the same, compare by assignment
|
|
# position within the module
|
|
if module_position_comparison.zero?
|
|
module_tag1.position <=> module_tag2.position
|
|
else
|
|
module_position_comparison
|
|
end
|
|
end
|
|
end
|