212 lines
7.9 KiB
Ruby
212 lines
7.9 KiB
Ruby
#
|
|
# Copyright (C) 2015 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 GradebookExporter
|
|
include GradebookTransformer
|
|
|
|
def initialize(course, user, options = {})
|
|
@course = course
|
|
@user = user
|
|
@options = options
|
|
end
|
|
|
|
def to_csv
|
|
collection = @options[:include_priors] ? @course.all_student_enrollments : @course.admin_visible_student_enrollments
|
|
enrollments_scope = @course.apply_enrollment_visibility(collection, @user)
|
|
student_enrollments = enrollments_for_csv(enrollments_scope, @options)
|
|
|
|
student_section_names = {}
|
|
student_enrollments.each do |enrollment|
|
|
student_section_names[enrollment.user_id] ||= []
|
|
student_section_names[enrollment.user_id] << (enrollment.course_section.display_name rescue nil)
|
|
end
|
|
|
|
# remove duplicate enrollments for students enrolled in multiple sections
|
|
student_enrollments = student_enrollments.uniq(&:user_id)
|
|
|
|
# grading_period_id == 0 means no grading period selected
|
|
unless @options[:grading_period_id].try(:to_i) == 0
|
|
grading_period = GradingPeriod.context_find @course, @options[:grading_period_id]
|
|
end
|
|
|
|
calc = GradeCalculator.new(student_enrollments.map(&:user_id), @course,
|
|
ignore_muted: false,
|
|
grading_period: grading_period)
|
|
grades = calc.compute_scores
|
|
|
|
submissions = {}
|
|
calc.submissions.each { |s| submissions[[s.user_id, s.assignment_id]] = s }
|
|
|
|
assignments = select_in_grading_period calc.assignments, @course, grading_period
|
|
|
|
assignments = assignments.sort_by do |a|
|
|
[a.assignment_group_id, a.position, a.due_at || CanvasSort::Last, a.title]
|
|
end
|
|
groups = calc.groups
|
|
|
|
read_only = I18n.t('csv.read_only_field', '(read only)')
|
|
include_root_account = @course.root_account.trust_exists?
|
|
should_show_totals = show_totals?
|
|
include_sis_id = @options[:include_sis_id]
|
|
CSV.generate do |csv|
|
|
# First row
|
|
row = ["Student", "ID"]
|
|
row << "SIS User ID" if include_sis_id
|
|
row << "SIS Login ID"
|
|
row << "Root Account" if include_sis_id && include_root_account
|
|
row << "Section"
|
|
row.concat assignments.map(&:title_with_id)
|
|
include_points = !@course.apply_group_weights?
|
|
|
|
if should_show_totals
|
|
groups.each do |group|
|
|
if include_points
|
|
row << "#{group.name} Current Points" << "#{group.name} Final Points"
|
|
end
|
|
row << "#{group.name} Current Score" << "#{group.name} Final Score"
|
|
end
|
|
row << "Current Points" << "Final Points" if include_points
|
|
row << "Current Score" << "Final Score"
|
|
if @course.grading_standard_enabled?
|
|
row << "Current Grade" << "Final Grade"
|
|
end
|
|
end
|
|
csv << row
|
|
|
|
group_filler_length = groups.size * (include_points ? 4 : 2)
|
|
|
|
# Possible muted row
|
|
if assignments.any?(&:muted)
|
|
# This is is not translated since we look for this exact string when we upload to gradebook.
|
|
row = [nil, nil, nil, nil]
|
|
row << nil if include_sis_id
|
|
row.concat(assignments.map { |a| 'Muted' if a.muted? })
|
|
|
|
if should_show_totals
|
|
row.concat([nil] * group_filler_length)
|
|
row << nil << nil if include_points
|
|
row << nil << nil
|
|
end
|
|
|
|
row << nil if @course.grading_standard_enabled?
|
|
csv << row
|
|
end
|
|
|
|
# Second Row
|
|
row = [" Points Possible", nil, nil, nil]
|
|
if include_sis_id
|
|
row << nil
|
|
row << nil if include_root_account
|
|
end
|
|
row.concat assignments.map(&:points_possible)
|
|
|
|
if should_show_totals
|
|
row.concat([read_only] * group_filler_length)
|
|
row << read_only << read_only if include_points
|
|
row << read_only << read_only
|
|
row << read_only if @course.grading_standard_enabled?
|
|
end
|
|
csv << row
|
|
|
|
student_enrollments.each_slice(100) do |student_enrollments_batch|
|
|
|
|
da_enabled = @course.feature_enabled?(:differentiated_assignments)
|
|
if da_enabled
|
|
visible_assignments = AssignmentStudentVisibility.visible_assignment_ids_in_course_by_user(
|
|
user_id: student_enrollments_batch.map(&:user_id),
|
|
course_id: @course.id
|
|
)
|
|
end
|
|
|
|
student_enrollments_batch.each do |student_enrollment|
|
|
student = student_enrollment.user
|
|
student_sections = student_section_names[student.id].sort.to_sentence
|
|
student_submissions = assignments.map do |a|
|
|
if da_enabled && visible_assignments[student.id] && !visible_assignments[student.id].include?(a.id)
|
|
"N/A"
|
|
else
|
|
submission = submissions[[student.id, a.id]]
|
|
if submission.try(:excused?)
|
|
"EX"
|
|
elsif a.grading_type == "gpa_scale" && submission.try(:score)
|
|
a.score_to_grade(submission.score)
|
|
else
|
|
submission.try(:score)
|
|
end
|
|
end
|
|
end
|
|
row = [student_name(student), student.id]
|
|
pseudonym = SisPseudonym.for(student, @course, include_root_account)
|
|
row << pseudonym.try(:sis_user_id) if include_sis_id
|
|
pseudonym ||= student.find_pseudonym_for_account(@course.root_account, include_root_account)
|
|
row << pseudonym.try(:unique_id)
|
|
row << (pseudonym && HostUrl.context_host(pseudonym.account)) if include_sis_id && include_root_account
|
|
row << student_sections
|
|
row.concat(student_submissions)
|
|
|
|
|
|
if should_show_totals
|
|
(current_info, current_group_info),
|
|
(final_info, final_group_info) = grades.shift
|
|
groups.each do |g|
|
|
row << current_group_info[g.id][:score] << final_group_info[g.id][:score] if include_points
|
|
row << current_group_info[g.id][:grade] << final_group_info[g.id][:grade]
|
|
end
|
|
row << current_info[:total] << final_info[:total] if include_points
|
|
row << current_info[:grade] << final_info[:grade]
|
|
if @course.grading_standard_enabled?
|
|
row << @course.score_to_grade(current_info[:grade])
|
|
row << @course.score_to_grade(final_info[:grade])
|
|
end
|
|
end
|
|
csv << row
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
def enrollments_for_csv(scope, options={})
|
|
# user: used for name in csv output
|
|
# course_section: used for display_name in csv output
|
|
# user > pseudonyms: used for sis_user_id/unique_id if options[:include_sis_id]
|
|
# user > pseudonyms > account: used in find_pseudonym_for_account > works_for_account
|
|
includes = {:user => {:pseudonyms => :account}, :course_section => []}
|
|
|
|
enrollments = scope.preload(includes).eager_load(:user).order_by_sortable_name.to_a
|
|
enrollments.partition { |e| e.type != "StudentViewEnrollment" }.flatten
|
|
end
|
|
|
|
def show_totals?
|
|
return true if !@course.feature_enabled?(:multiple_grading_periods)
|
|
return true if @options[:grading_period_id].try(:to_i) != 0
|
|
@course.feature_enabled?(:all_grading_periods_totals)
|
|
end
|
|
|
|
STARTS_WITH_EQUAL = /^\s*=/
|
|
|
|
# Returns the student name to use for the export. If the name
|
|
# starts with =, quote it so anyone pulling the data into Excel
|
|
# doesn't have a formula execute.
|
|
def student_name(student)
|
|
name = @course.list_students_by_sortable_name? ? student.sortable_name : student.name
|
|
name = "=\"#{name}\"" if name =~ STARTS_WITH_EQUAL
|
|
name
|
|
end
|
|
end
|