1100 lines
45 KiB
Ruby
1100 lines
45 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
#
|
|
# Copyright (C) 2015 - 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/>.
|
|
#
|
|
|
|
require_relative '../spec_helper'
|
|
require 'csv'
|
|
|
|
describe GradebookExporter do
|
|
before(:once) do
|
|
@course = course_model(grading_standard_id: 0)
|
|
course_with_teacher(course: @course, active_all: true)
|
|
end
|
|
|
|
def enable_final_grade_override!
|
|
@course.enable_feature!(:final_grades_override)
|
|
@course.update!(allow_final_grade_override: true)
|
|
end
|
|
|
|
describe "#to_csv" do
|
|
def exporter(opts = {})
|
|
GradebookExporter.new(@course, @teacher, opts)
|
|
end
|
|
|
|
describe "assignment order" do
|
|
def format_assignment_preferences(assignments)
|
|
assignments.map { |assignment| "assignment_#{assignment.id}" }
|
|
end
|
|
|
|
def format_assignment_headers(assignments)
|
|
assignments.map(&:title_with_id)
|
|
end
|
|
|
|
before(:once) do
|
|
student_in_course(course: @course, active_all: true)
|
|
|
|
# The assignment groups are created out of order on purpose. The old code would order by assignment_group.id, so
|
|
# by creating the assignment groups out of order, we should get ids that are out of order. The new code orders
|
|
# using assignment_group.position which is guaranteed to be there in the model.
|
|
@first_group = @course.assignment_groups.create!(name: "first group", position: 1)
|
|
@last_group = @course.assignment_groups.create!(name: "last group", position: 3)
|
|
@second_group = @course.assignment_groups.create!(name: "second group", position: 2)
|
|
|
|
@assignments = []
|
|
@first_group_assignment = @course.assignments.create!(name: "First group assignment", assignment_group: @first_group)
|
|
@assignments[0] = @first_group_assignment
|
|
@last_group_assignment = @course.assignments.create!(name: "last group assignment", assignment_group: @last_group)
|
|
@assignments[2] = @last_group_assignment
|
|
@second_group_assignment = @course.assignments.create!(name: "second group assignment", assignment_group: @second_group)
|
|
@assignments[1] = @second_group_assignment
|
|
@exporter_options = {}
|
|
end
|
|
|
|
let(:headers) do
|
|
csv = GradebookExporter.new(@course, @teacher, @exporter_options).to_csv
|
|
CSV.parse(csv, headers: true).headers
|
|
end
|
|
|
|
context "when assignment column order is specified" do
|
|
it "returns assignments ordered by the supplied custom order" do
|
|
custom_assignment_order = [@last_group_assignment, @second_group_assignment, @first_group_assignment]
|
|
@exporter_options[:assignment_order] = custom_assignment_order.map(&:id)
|
|
actual_assignment_headers = headers[4, 3]
|
|
|
|
expect(actual_assignment_headers).to eq format_assignment_headers(custom_assignment_order)
|
|
end
|
|
|
|
it "returns assignments ordered by assignment group position when feature is disabled" do
|
|
expect(Account.site_admin).to receive(:feature_enabled?).and_call_original
|
|
expect(Account.site_admin).to receive(:feature_enabled?).with(:gradebook_csv_export_order_matches_gradebook_grid).and_return(false)
|
|
|
|
actual_assignment_headers = headers[4, 3]
|
|
expected_assignment_headers = format_assignment_headers(@assignments)
|
|
|
|
expect(actual_assignment_headers).to eq expected_assignment_headers
|
|
end
|
|
|
|
it "orders assignments not in the custom order after the assignments in the custom order" do
|
|
custom_assignment_order = [@second_group_assignment, @first_group_assignment]
|
|
@exporter_options[:assignment_order] = custom_assignment_order.map(&:id)
|
|
|
|
actual_assignment_headers = headers[4, 3]
|
|
expected_assignment_headers = format_assignment_headers [@second_group_assignment, @first_group_assignment, @last_group_assignment]
|
|
|
|
expect(actual_assignment_headers).to eq expected_assignment_headers
|
|
end
|
|
|
|
it "orders by ID within the group of assignments not in the custom order" do
|
|
custom_assignment_order = [@last_group_assignment]
|
|
@exporter_options[:assignment_order] = custom_assignment_order.map(&:id)
|
|
|
|
actual_assignment_headers = headers[4, 3]
|
|
expected_assignment_headers = format_assignment_headers [@last_group_assignment, @first_group_assignment, @second_group_assignment]
|
|
|
|
expect(actual_assignment_headers).to eq expected_assignment_headers
|
|
end
|
|
|
|
it "includes a column for anonymized assignments" do
|
|
@first_group_assignment.update!(anonymous_grading: true)
|
|
|
|
expect(headers).to include(/First group assignment/)
|
|
end
|
|
end
|
|
|
|
context "when assignment column order preferences do not exist" do
|
|
it "returns assignments ordered by assignment group position" do
|
|
actual_assignment_headers = headers[4, 3]
|
|
expected_headers = format_assignment_headers @assignments
|
|
|
|
expect(actual_assignment_headers).to eq(expected_headers)
|
|
end
|
|
|
|
it "returns assignments ordered by assignment group position when feature is disabled" do
|
|
expect(Account.site_admin).to receive(:feature_enabled?).and_call_original
|
|
expect(Account.site_admin).to receive(:feature_enabled?).with(:gradebook_csv_export_order_matches_gradebook_grid).and_return(false)
|
|
|
|
actual_assignment_headers = headers[4, 3]
|
|
expected_headers = format_assignment_headers @assignments
|
|
|
|
expect(actual_assignment_headers).to eq(expected_headers)
|
|
end
|
|
|
|
it "includes a column for anonymized assignments" do
|
|
@first_group_assignment.update!(anonymous_grading: true)
|
|
|
|
expect(headers).to include(/First group assignment/)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "custom columns" do
|
|
before(:once) do
|
|
first_column = @course.custom_gradebook_columns.create! title: "Custom Column 1"
|
|
second_column = @course.custom_gradebook_columns.create! title: "Custom Column 2"
|
|
third_column = @course.custom_gradebook_columns.create!({ title: "Custom Column 3", workflow_state: "hidden" })
|
|
|
|
student1_enrollment = student_in_course(course: @course, active_all: true).user
|
|
student2_enrollment = student_in_course(course: @course, active_all: true).user
|
|
|
|
first_column.custom_gradebook_column_data.create!({ content: 'Row1 Custom Column 1', user_id: student1_enrollment.id })
|
|
first_column.custom_gradebook_column_data.create!({ content: 'Row2 Custom Column 1', user_id: student2_enrollment.id })
|
|
second_column.custom_gradebook_column_data.create!({ content: 'Row1 Custom Column 2', user_id: student1_enrollment.id })
|
|
second_column.custom_gradebook_column_data.create!({ content: 'Row2 Custom Column 2', user_id: student2_enrollment.id })
|
|
third_column.custom_gradebook_column_data.create!({ content: 'Row1 Custom Column 3', user_id: student1_enrollment.id })
|
|
third_column.custom_gradebook_column_data.create!({ content: 'Row2 Custom Column 3', user_id: student2_enrollment.id })
|
|
end
|
|
|
|
it "have the correct custom column data in proper order" do
|
|
csv = GradebookExporter.new(@course, @teacher).to_csv
|
|
rows = CSV.parse(csv, headers: true)
|
|
|
|
expect(rows[1]['Custom Column 1']).to eq 'Row1 Custom Column 1'
|
|
expect(rows[2]['Custom Column 1']).to eq 'Row2 Custom Column 1'
|
|
expect(rows[1]['Custom Column 2']).to eq 'Row1 Custom Column 2'
|
|
expect(rows[2]['Custom Column 2']).to eq 'Row2 Custom Column 2'
|
|
expect(rows[1]['Custom Column 3']).to eq nil
|
|
expect(rows[2]['Custom Column 3']).to eq nil
|
|
end
|
|
end
|
|
|
|
describe "separate columns for student last and first names" do
|
|
subject(:csv) { exporter(@exporter_options).to_csv }
|
|
|
|
before(:once) do
|
|
@exporter_options = {}
|
|
@current_assignment = @course.assignments.create! due_at: 1.week.from_now,
|
|
title: "current",
|
|
points_possible: 10
|
|
student_in_course active_all: true
|
|
@current_assignment.grade_student @student, grade: 3, grader: @teacher
|
|
@rows = CSV.parse(csv)
|
|
end
|
|
|
|
it "is a csv with three rows" do
|
|
expect(@rows.count).to be 3
|
|
end
|
|
|
|
it "is a csv with rows of equal length" do
|
|
expect(@rows.first.length).to eq @rows.second.length
|
|
end
|
|
|
|
it "shows student first and last names in headers" do
|
|
@exporter_options[:show_student_first_last_name] = true
|
|
expect(CSV.parse(csv, headers: true).headers).to include("LastName", "FirstName")
|
|
expect(CSV.parse(csv, headers: true).headers).not_to include("Student")
|
|
end
|
|
|
|
it "shows student first and last name in rows" do
|
|
@exporter_options[:show_student_first_last_name] = true
|
|
rows = CSV.parse(csv)
|
|
expect(rows[2][0]).to eq(@student.last_name)
|
|
expect(rows[2][1]).to eq(@student.first_name)
|
|
end
|
|
end
|
|
|
|
describe "default output with blank course" do
|
|
before(:once) do
|
|
@course.custom_gradebook_columns.create! title: "Custom Column 1"
|
|
@course.custom_gradebook_columns.create! title: "Custom Column 2"
|
|
@course.custom_gradebook_columns.create!({ title: "Custom Column 3", workflow_state: "hidden" })
|
|
end
|
|
|
|
subject(:csv) { exporter.to_csv }
|
|
|
|
let(:expected_headers) do
|
|
[
|
|
"Student", "ID", "SIS Login ID", "Section", "Custom Column 1", "Custom Column 2",
|
|
"Current Points", "Final Points",
|
|
"Current Score", "Unposted Current Score", "Final Score", "Unposted Final Score",
|
|
"Current Grade", "Unposted Current Grade", "Final Grade", "Unposted Final Grade"
|
|
]
|
|
end
|
|
|
|
it { is_expected.to be_a String }
|
|
|
|
it "is a csv with two rows" do
|
|
expect(CSV.parse(csv).count).to be 2
|
|
end
|
|
|
|
it "is a csv with rows of equal length" do
|
|
rows = CSV.parse(csv)
|
|
expect(rows.first.length).to eq rows.second.length
|
|
end
|
|
|
|
it "has headers in a default order" do
|
|
actual_headers = CSV.parse(csv, headers: true).headers
|
|
expect(actual_headers).to match_array(expected_headers)
|
|
end
|
|
|
|
context "when Final Grade Override is enabled" do
|
|
before(:once) { enable_final_grade_override! }
|
|
let_once(:override_headers) { expected_headers.push("Override Score") }
|
|
|
|
it "includes the Override Score and Override Grade headers when the course has a grading standard" do
|
|
actual_headers = CSV.parse(csv, headers: true).headers
|
|
expect(actual_headers).to match_array(override_headers.push("Override Grade"))
|
|
end
|
|
|
|
it "omits the Override Grade header when the course lacks a grading standard" do
|
|
@course.update!(grading_standard_id: nil)
|
|
actual_headers = CSV.parse(exporter.to_csv, headers: true).headers
|
|
expect(actual_headers).not_to include("Override Grade")
|
|
end
|
|
|
|
describe "read-only indicator for Override Score" do
|
|
let(:parsed_csv) { CSV.parse(exporter.to_csv, headers: true) }
|
|
let(:read_only_row) { parsed_csv[0] }
|
|
|
|
it "is omitted if importing override scores is enabled" do
|
|
Account.site_admin.enable_feature!(:import_override_scores_in_gradebook)
|
|
expect(read_only_row["Override Score"]).to eq nil
|
|
end
|
|
|
|
it "is included if importing override scores is not enabled" do
|
|
expect(read_only_row["Override Score"]).to eq "(read only)"
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when Final Grade Override is not enabled" do
|
|
before(:once) do
|
|
@course.enable_feature!(:final_grades_override)
|
|
@course.update!(allow_final_grade_override: false)
|
|
end
|
|
|
|
it "excludes the Override Score headers" do
|
|
actual_headers = CSV.parse(csv, headers: true).headers
|
|
expect(actual_headers).not_to include("Override Grade")
|
|
end
|
|
|
|
it "excludes the Override Grade headers when the course has a grading standard" do
|
|
actual_headers = CSV.parse(csv, headers: true).headers
|
|
expect(actual_headers).not_to include("Override Score")
|
|
end
|
|
end
|
|
|
|
context "when Final Grade Override is not enabled" do
|
|
it "excludes the Override Score headers" do
|
|
actual_headers = CSV.parse(csv, headers: true).headers
|
|
expect(actual_headers).not_to include("Override Grade")
|
|
end
|
|
|
|
it "excludes the Override Grade headers when the course has a grading standard" do
|
|
actual_headers = CSV.parse(csv, headers: true).headers
|
|
expect(actual_headers).not_to include("Override Score")
|
|
end
|
|
end
|
|
|
|
describe "byte-order mark" do
|
|
it "is included when the user has it enabled" do
|
|
@teacher.enable_feature!(:include_byte_order_mark_in_gradebook_exports)
|
|
actual_headers = CSV.parse(exporter.to_csv, headers: true).headers
|
|
expect(actual_headers[0]).to eq("\xEF\xBB\xBFStudent")
|
|
end
|
|
|
|
it "is excluded when the user has it disabled" do
|
|
@teacher.disable_feature!(:include_byte_order_mark_in_gradebook_exports)
|
|
actual_headers = CSV.parse(exporter.to_csv, headers: true).headers
|
|
expect(actual_headers[0]).to eq("Student")
|
|
end
|
|
end
|
|
|
|
context "when muted assignments are present" do
|
|
before(:each) do
|
|
@course.assignments.create!(muted: true, points_possible: 10)
|
|
@exporter_options = {}
|
|
end
|
|
|
|
let(:csv) do
|
|
unparsed_csv = GradebookExporter.new(@course, @teacher, @exporter_options).to_csv
|
|
CSV.parse(unparsed_csv)
|
|
end
|
|
|
|
let(:header_row_length) { csv.first.length }
|
|
let(:muted_row_length) { csv.second.length }
|
|
|
|
it "the length of the 'muted' row matches the length of the header row" do
|
|
expect(header_row_length).to eq muted_row_length
|
|
end
|
|
|
|
it "the length of the 'muted' row matches the length of the header row when include_sis_id is true" do
|
|
@exporter_options[:include_sis_id] = true
|
|
expect(header_row_length).to eq muted_row_length
|
|
end
|
|
|
|
it "the length of the 'muted' row matches the length of the header row when integration_ids are passed" do
|
|
@exporter_options[:include_sis_id] = true
|
|
@course.root_account.settings[:include_integration_ids_in_gradebook_exports] = true
|
|
@course.root_account.save!
|
|
expect(header_row_length).to eq muted_row_length
|
|
end
|
|
|
|
it "the length of the 'muted' row matches the length of the header row when include_sis_id " \
|
|
"is true and the account is a trust account" do
|
|
expect(@course.root_account).to receive(:trust_exists?).and_return(true)
|
|
@exporter_options[:include_sis_id] = true
|
|
expect(header_row_length).to eq muted_row_length
|
|
end
|
|
end
|
|
|
|
context "when at least one assignment is manually-posted" do
|
|
let_once(:manual_assignment) { @course.assignments.create!(title: "manual") }
|
|
let_once(:manual_header) { "manual (#{manual_assignment.id})" }
|
|
let_once(:auto_assignment) { @course.assignments.create!(title: "auto") }
|
|
let_once(:auto_header) { "auto (#{auto_assignment.id})" }
|
|
|
|
let(:csv) do
|
|
unparsed_csv = GradebookExporter.new(@course, @teacher, {}).to_csv
|
|
CSV.parse(unparsed_csv, headers: true)
|
|
end
|
|
|
|
before(:once) do
|
|
manual_assignment.ensure_post_policy(post_manually: true)
|
|
auto_assignment.ensure_post_policy(post_manually: false)
|
|
end
|
|
|
|
let(:manual_posting_row) { csv[0] }
|
|
|
|
it "includes a line consisting entirely of 'Manual Posting' or empty values" do
|
|
expect(manual_posting_row.fields.uniq).to contain_exactly(nil, "Manual Posting")
|
|
end
|
|
|
|
it "designates manually-posted assignments as 'Manual Posting'" do
|
|
expect(manual_posting_row[manual_header]).to eq "Manual Posting"
|
|
end
|
|
|
|
it "has a special designation for anonymous unposted assignments" do
|
|
course_with_student(course: @course, active_all: true)
|
|
manual_assignment.update!(anonymous_grading: true)
|
|
expect(manual_posting_row[manual_header]).to eq "Manual Posting (scores hidden from instructors)"
|
|
end
|
|
|
|
it "emits an empty value for auto-posted assignments" do
|
|
expect(manual_posting_row[auto_header]).to be nil
|
|
end
|
|
end
|
|
|
|
it "omits the 'Manual Posting' row if no assignments are manually-posted" do
|
|
unparsed_csv = GradebookExporter.new(@course, @teacher, {}).to_csv
|
|
csv = CSV.parse(unparsed_csv, headers: true)
|
|
|
|
auto_assignment = @course.assignments.create!(title: "auto")
|
|
auto_assignment.ensure_post_policy(post_manually: false)
|
|
|
|
expect(csv[0].fields).not_to include("Manual Posting")
|
|
end
|
|
end
|
|
|
|
context "internationalization" do
|
|
it "can use localized column separators" do
|
|
csv = exporter(col_sep: ";", encoding: "UTF-8").to_csv
|
|
actual_headers = CSV.parse(csv, col_sep: ";", headers: true).headers
|
|
expected_headers = ['Student', 'ID', 'SIS Login ID']
|
|
|
|
expect(actual_headers[0..2]).to eq(expected_headers)
|
|
end
|
|
|
|
it "can automatically determine the column separator to use when asked to autodetect" do
|
|
@teacher.enable_feature!(:autodetect_field_separators_for_gradebook_exports)
|
|
@course.assignments.create!(title: "Verkefni 1", points_possible: 8.5)
|
|
csv = exporter(locale: :is).to_csv
|
|
expect(csv).to match(/;8,50;/)
|
|
end
|
|
|
|
it "uses comma as the column separator when not asked to autodetect" do
|
|
@course.assignments.create!(title: "Verkefni 1", points_possible: 8.5)
|
|
csv = exporter(locale: :is).to_csv
|
|
expect(csv).to match(/,"8,50",/)
|
|
end
|
|
|
|
it "prepends byte order mark with UTF-8 encoding when the user enables it" do
|
|
@teacher.enable_feature!(:include_byte_order_mark_in_gradebook_exports)
|
|
csv = exporter(encoding: "UTF-8").to_csv
|
|
headers = CSV.parse(csv, headers: true).headers
|
|
expect(headers[0]).to eq "\xEF\xBB\xBFStudent"
|
|
end
|
|
|
|
it "omits byte order mark with US-ASCII encoding even when the user enables it" do
|
|
@teacher.enable_feature!(:include_byte_order_mark_in_gradebook_exports)
|
|
csv = exporter(encoding: "US-ASCII").to_csv
|
|
headers = CSV.parse(csv, headers: true).headers
|
|
expect(headers[0]).to eq "Student".encode("US-ASCII")
|
|
end
|
|
|
|
describe "grades" do
|
|
before :each do
|
|
@assignment = @course.assignments.create!(title: 'Verkefni 1', points_possible: 10, grading_type: 'gpa_scale')
|
|
@student = student_in_course(course: @course, active_all: true).user
|
|
@assignment.grade_student(@student, grader: @teacher, score: 7.5)
|
|
end
|
|
|
|
context 'when forcing the field separator to be a semicolon' do
|
|
before :each do
|
|
@teacher.enable_feature!(:use_semi_colon_field_separators_in_gradebook_exports)
|
|
@csv = exporter(locale: :is).to_csv
|
|
@icsv = CSV.parse(@csv, col_sep: ";", headers: true)
|
|
end
|
|
|
|
it "localizes numbers" do
|
|
expect(@icsv[1]['Assignments Current Points']).to eq('7,50')
|
|
end
|
|
|
|
it "does not localize grading scheme grades for assignments" do
|
|
expect(@icsv[1]["#{@assignment.title} (#{@assignment.id})"]).to eq('C')
|
|
end
|
|
|
|
it "does not localize grading scheme grades for the total" do
|
|
expect(@icsv[1]["Final Grade"]).to eq('C')
|
|
end
|
|
end
|
|
|
|
context 'when not forcing the field separator to be a semicolon' do
|
|
before :each do
|
|
@teacher.disable_feature!(:use_semi_colon_field_separators_in_gradebook_exports)
|
|
end
|
|
|
|
context 'when autodetecting field separator to use' do
|
|
before :each do
|
|
@teacher.enable_feature!(:autodetect_field_separators_for_gradebook_exports)
|
|
@csv = exporter(locale: :is).to_csv
|
|
@icsv = CSV.parse(@csv, col_sep: ";", headers: true)
|
|
end
|
|
|
|
it "localizes numbers" do
|
|
expect(@icsv[1]['Assignments Current Points']).to eq('7,50')
|
|
end
|
|
|
|
it "does not localize grading scheme grades for assignments" do
|
|
expect(@icsv[1]["#{@assignment.title} (#{@assignment.id})"]).to eq('C')
|
|
end
|
|
|
|
it "does not localize grading scheme grades for the total" do
|
|
expect(@icsv[1]["Final Grade"]).to eq('C')
|
|
end
|
|
end
|
|
|
|
context 'when not autodetecting field separator to use' do
|
|
before :each do
|
|
@teacher.disable_feature!(:autodetect_field_separators_for_gradebook_exports)
|
|
@csv = exporter(locale: :is).to_csv
|
|
@icsv = CSV.parse(@csv, col_sep: ",", headers: true)
|
|
end
|
|
|
|
it "localizes numbers" do
|
|
expect(@icsv[1]['Assignments Current Points']).to eq('7,50')
|
|
end
|
|
|
|
it "does not localize grading scheme grades for assignments" do
|
|
expect(@icsv[1]["#{@assignment.title} (#{@assignment.id})"]).to eq('C')
|
|
end
|
|
|
|
it "does not localize grading scheme grades for the total" do
|
|
expect(@icsv[1]["Final Grade"]).to eq('C')
|
|
end
|
|
end
|
|
end
|
|
|
|
it "rounds scores to two decimal places" do
|
|
@assignment.update!(grading_type: 'points')
|
|
@assignment.grade_student(@student, grader: @teacher, score: 7.555)
|
|
csv = exporter.to_csv
|
|
parsed_csv = CSV.parse(csv, headers: true)
|
|
|
|
expect(parsed_csv[1]["#{@assignment.title} (#{@assignment.id})"]).to eq "7.56"
|
|
end
|
|
end
|
|
end
|
|
|
|
context "a course has assignments with due dates" do
|
|
before(:each) do
|
|
@no_due_date_assignment = @course.assignments.create! title: "no due date",
|
|
points_possible: 10
|
|
|
|
@past_assignment = @course.assignments.create! due_at: 5.weeks.ago,
|
|
title: "past",
|
|
points_possible: 10
|
|
|
|
@current_assignment = @course.assignments.create! due_at: 1.weeks.from_now,
|
|
title: "current",
|
|
points_possible: 10
|
|
|
|
@future_assignment = @course.assignments.create! due_at: 8.weeks.from_now,
|
|
title: "future",
|
|
points_possible: 10
|
|
|
|
student_in_course active_all: true
|
|
|
|
@no_due_date_assignment.grade_student @student, grade: 1, grader: @teacher
|
|
@past_assignment.grade_student @student, grade: 2, grader: @teacher
|
|
@current_assignment.grade_student @student, grade: 3, grader: @teacher
|
|
@future_assignment.grade_student @student, grade: 4, grader: @teacher
|
|
|
|
@group = Factories::GradingPeriodGroupHelper.new.legacy_create_for_course(@course)
|
|
|
|
@first_period = @group.grading_periods.create!(
|
|
start_date: 6.weeks.ago, end_date: 3.weeks.ago, title: "past grading period"
|
|
)
|
|
@last_period = @group.grading_periods.create!(
|
|
start_date: 3.weeks.ago, end_date: 3.weeks.from_now, title: "present day, present time"
|
|
)
|
|
end
|
|
|
|
describe "with grading periods" do
|
|
describe "assignments in the selected grading period are exported" do
|
|
before(:each) do
|
|
@csv = exporter(grading_period_id: @last_period.id).to_csv
|
|
@rows = CSV.parse(@csv, headers: true)
|
|
@headers = @rows.headers
|
|
end
|
|
|
|
it "exports selected grading period's assignments" do
|
|
expect(@headers).to include @no_due_date_assignment.title_with_id,
|
|
@current_assignment.title_with_id
|
|
final_grade = @rows[1]["Final Score (#{@last_period.title})"].try(:to_f)
|
|
expect(final_grade).to eq 20
|
|
end
|
|
|
|
it "exports assignments without due dates if exporting last grading period" do
|
|
expect(@headers).to include @current_assignment.title_with_id,
|
|
@no_due_date_assignment.title_with_id
|
|
final_grade = @rows[1]["Final Score (#{@last_period.title})"].try(:to_f)
|
|
expect(final_grade).to eq 20
|
|
end
|
|
|
|
it "does not export assignments without due date" do
|
|
@grading_period_id = @first_period.id
|
|
@csv = exporter(grading_period_id: @grading_period_id).to_csv
|
|
@rows = CSV.parse(@csv, headers: true)
|
|
@headers = @rows.headers
|
|
|
|
expect(@headers).to_not include @no_due_date_assignment.title_with_id
|
|
end
|
|
|
|
it "does not export assignments in other grading periods" do
|
|
expect(@headers).to_not include @past_assignment.title_with_id,
|
|
@future_assignment.title_with_id
|
|
end
|
|
|
|
it "does not export future assignments" do
|
|
expect(@headers).to_not include @future_assignment.title_with_id
|
|
end
|
|
|
|
it "exports the entire gradebook when grading_period_id is 0" do
|
|
@grading_period_id = 0
|
|
@csv = exporter(grading_period_id: @grading_period_id).to_csv
|
|
@rows = CSV.parse(@csv, headers: true)
|
|
@headers = @rows.headers
|
|
|
|
expect(@headers).to include @past_assignment.title_with_id,
|
|
@current_assignment.title_with_id,
|
|
@future_assignment.title_with_id,
|
|
@no_due_date_assignment.title_with_id
|
|
expect(@headers).not_to include "Final Score"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "with inactive students" do
|
|
before :once do
|
|
assmt = @course.assignments.create!(title: "assmt", points_possible: 10)
|
|
|
|
student1_enrollment = student_in_course(course: @course, active_all: true)
|
|
@student1 = student1_enrollment.user
|
|
student2_enrollment = student_in_course(course: @course, active_all: true)
|
|
@student2 = student2_enrollment.user
|
|
|
|
assmt.grade_student(@student1, grade: 1, grader: @teacher)
|
|
assmt.grade_student(@student2, grade: 2, grader: @teacher)
|
|
|
|
student1_enrollment.deactivate
|
|
student2_enrollment.deactivate
|
|
|
|
@teacher.set_preference(:gradebook_settings, @course.global_id, {
|
|
'show_inactive_enrollments' => 'true',
|
|
'show_concluded_enrollments' => 'false'
|
|
})
|
|
end
|
|
|
|
it "includes inactive students" do
|
|
csv = exporter.to_csv
|
|
rows = CSV.parse(csv, headers: true)
|
|
expect([rows[1]["ID"], rows[2]["ID"]]).to match_array([@student1.id.to_s, @student2.id.to_s])
|
|
end
|
|
|
|
it "includes grades for inactive students if show inactive enrollments" do
|
|
csv = exporter.to_csv
|
|
rows = CSV.parse(csv, headers: true)
|
|
assignment_data_first_student = rows[1].find { |column_info| column_info.first.include? "assmt" }
|
|
assignment_data_second_student = rows[2].find { |column_info| column_info.first.include? "assmt" }
|
|
expect([assignment_data_first_student.second, assignment_data_second_student.second]).to match_array(["1.00", "2.00"])
|
|
end
|
|
|
|
it "does not include inactive students if show inactive enrollments is set to false" do
|
|
@teacher.set_preference(:gradebook_settings, @course.global_id, {
|
|
'show_inactive_enrollments' => 'false',
|
|
'show_concluded_enrollments' => 'false'
|
|
})
|
|
csv = exporter.to_csv
|
|
rows = CSV.parse(csv, headers: true)
|
|
expect([rows[1], rows[2]]).to match_array([nil, nil])
|
|
end
|
|
end
|
|
|
|
it 'handles gracefully any assignments with nil position' do
|
|
@course.assignments.create! title: 'assignment #1'
|
|
assignment = @course.assignments.create! title: 'assignment #2'
|
|
assignment.update_attribute(:position, nil)
|
|
|
|
expect { exporter.to_csv }.not_to raise_error
|
|
end
|
|
|
|
describe "column headers" do
|
|
before(:once) do
|
|
enable_final_grade_override!
|
|
|
|
group = Factories::GradingPeriodGroupHelper.new.create_for_account(@course.root_account)
|
|
group.grading_periods.create!(
|
|
start_date: 6.weeks.ago,
|
|
end_date: 3.weeks.ago,
|
|
title: "past grading period"
|
|
)
|
|
@last_grading_period = group.grading_periods.create!(
|
|
start_date: 3.weeks.ago,
|
|
end_date: 3.weeks.from_now,
|
|
title: "present day, present time"
|
|
)
|
|
|
|
enrollment_term = @course.root_account.enrollment_terms.create!(grading_period_group: group)
|
|
@course.update!(enrollment_term: enrollment_term)
|
|
|
|
assignment_group = @course.assignment_groups.create!(name: "my group")
|
|
@course.assignments.create!(
|
|
assignment_group: assignment_group,
|
|
due_at: 1.day.after(@last_grading_period.start_date),
|
|
title: "my assignment"
|
|
)
|
|
end
|
|
|
|
let(:exporter) do
|
|
GradebookExporter.new(@course, @teacher, { grading_period_id: @last_grading_period.id })
|
|
end
|
|
let(:exported_headers) { CSV.parse(exporter.to_csv, headers: true).headers }
|
|
|
|
let(:total_columns) do
|
|
[
|
|
"Current Points", "Final Points",
|
|
"Current Grade", "Unposted Current Grade", "Final Grade", "Unposted Final Grade",
|
|
"Current Score", "Unposted Current Score", "Final Score", "Unposted Final Score"
|
|
]
|
|
end
|
|
let(:total_and_override_columns) { total_columns + ["Override Score", "Override Grade"] }
|
|
|
|
it "appends the grading period to overall total and override columns" do
|
|
columns_with_grading_period = total_and_override_columns.map do |column|
|
|
"#{column} (present day, present time)"
|
|
end
|
|
|
|
expect(exported_headers).to include(*columns_with_grading_period)
|
|
end
|
|
|
|
it "appends the grading period to assignment group total columns" do
|
|
aggregate_failures do
|
|
expect(exported_headers).to include("my group Current Score (present day, present time)")
|
|
expect(exported_headers).not_to include("my group Current Score")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context "a course with a student whose name starts with an equals sign" do
|
|
let(:student) do
|
|
user = user_factory(name: "=sum(A)", active_user: true)
|
|
course_with_student(course: @course, user: user)
|
|
user
|
|
end
|
|
let(:course) { @course }
|
|
let(:assignment) { @course.assignments.create!(title: "Assignment", points_possible: 4) }
|
|
|
|
it "quotes the name that starts with an equals so it's not considered a formula" do
|
|
assignment.grade_student(student, grade: 1, grader: @teacher)
|
|
csv = GradebookExporter.new(@course, @teacher, {}).to_csv
|
|
rows = CSV.parse(csv, headers: true)
|
|
|
|
expect(rows[1][0]).to eql('="=sum(A)"')
|
|
end
|
|
end
|
|
|
|
context "when a course has anonymous assignments" do
|
|
before(:each) do
|
|
@student = User.create!
|
|
student_in_course(user: @student, course: @course, active_all: true)
|
|
@assignment = @course.assignments.create!(title: "Anon Assignment", points_possible: 10, anonymous_grading: true)
|
|
@assignment.ensure_post_policy(post_manually: true)
|
|
@assignment.grade_student(@student, grade: 8, grader: @teacher)
|
|
end
|
|
|
|
let(:submission_score) do
|
|
csv = GradebookExporter.new(@course, @teacher, {}).to_csv
|
|
rows = CSV.parse(csv, headers: true)
|
|
rows[2]["Anon Assignment (#{@assignment.id})"]
|
|
end
|
|
|
|
it "shows 'N/A' for submission scores in the export when the assignment is unposted" do
|
|
expect(submission_score).to eq "N/A"
|
|
end
|
|
|
|
it "shows actual submission scores in the export when the assignment is posted" do
|
|
@assignment.post_submissions
|
|
expect(submission_score).to eq "8.00"
|
|
end
|
|
end
|
|
|
|
context "when a course has unposted assignments" do
|
|
let(:posted_assignment) { @course.assignments.create!(title: "Posted", points_possible: 10) }
|
|
let(:unposted_assignment) { @course.assignments.create!(title: "Unposted", points_possible: 10) }
|
|
let(:unposted_anonymous_assignment) do
|
|
@course.assignments.create!(title: "Unposted Anon", points_possible: 10, anonymous_grading: true)
|
|
end
|
|
|
|
before(:each) do
|
|
@course.assignments.create!(title: "Ungraded", points_possible: 10)
|
|
|
|
posted_assignment.ensure_post_policy(post_manually: true)
|
|
unposted_assignment.ensure_post_policy(post_manually: true)
|
|
unposted_anonymous_assignment.ensure_post_policy(post_manually: true)
|
|
|
|
student_in_course active_all: true
|
|
|
|
posted_assignment.grade_student @student, grade: 9, grader: @teacher
|
|
unposted_assignment.grade_student @student, grade: 3, grader: @teacher
|
|
unposted_anonymous_assignment.grade_student @student, grade: 1, grader: @teacher
|
|
|
|
posted_assignment.post_submissions
|
|
end
|
|
|
|
it "calculates assignment group scores correctly" do
|
|
csv = GradebookExporter.new(@course, @teacher, {}).to_csv
|
|
rows = CSV.parse(csv, headers: true)
|
|
|
|
expect(rows[2]["Assignments Current Score"].try(:to_f)).to eq 90
|
|
expect(rows[2]["Assignments Unposted Current Score"].try(:to_f)).to eq 60
|
|
expect(rows[2]["Assignments Final Score"].try(:to_f)).to eq 30
|
|
expect(rows[2]["Assignments Unposted Final Score"].try(:to_f)).to eq 40
|
|
end
|
|
|
|
it "calculates totals correctly" do
|
|
csv = GradebookExporter.new(@course, @teacher, {}).to_csv
|
|
rows = CSV.parse(csv, headers: true)
|
|
|
|
expect(rows[2]["Current Score"].try(:to_f)).to eq 90
|
|
expect(rows[2]["Unposted Current Score"].try(:to_f)).to eq 60
|
|
expect(rows[2]["Final Score"].try(:to_f)).to eq 30
|
|
expect(rows[2]["Unposted Final Score"].try(:to_f)).to eq 40
|
|
end
|
|
end
|
|
|
|
context "with weighted assignment groups" do
|
|
before(:once) do
|
|
student_in_course active_all: true
|
|
@course.update(group_weighting_scheme: 'percent')
|
|
|
|
first_group = @course.assignment_groups.create!(name: "First Group", group_weight: 0.5)
|
|
@course.assignment_groups.create!(name: "Second Group", group_weight: 0.5)
|
|
|
|
@assignment = @course.assignments.create!(title: 'Assignment 1', points_possible: 10,
|
|
grading_type: 'gpa_scale', assignment_group: first_group)
|
|
@assignment.grade_student(@student, grade: 8, grader: @teacher)
|
|
end
|
|
|
|
it "emits rows of equal length when no assignments are muted" do
|
|
csv = GradebookExporter.new(@course, @teacher, {}).to_csv
|
|
rows = CSV.parse(csv)
|
|
|
|
expect(rows.group_by(&:size).count).to be 1
|
|
end
|
|
|
|
it "emits rows of equal length when an assignment is muted" do
|
|
@assignment.mute!
|
|
csv = GradebookExporter.new(@course, @teacher, {}).to_csv
|
|
rows = CSV.parse(csv)
|
|
|
|
expect(rows.group_by(&:size).count).to be 1
|
|
end
|
|
end
|
|
|
|
describe "#show_overall_totals" do
|
|
let(:enrollment) { @student.enrollments.find_by(course: @course) }
|
|
|
|
before(:once) do
|
|
student_in_course(course: @course, active_all: true)
|
|
end
|
|
|
|
# this test is needed to guarantee the stubbing in the following specs on
|
|
# enrollment reflects reality and isn't a false positive.
|
|
it 'includes the student enrollment in the course' do
|
|
exporter = GradebookExporter.new(@course, @teacher)
|
|
|
|
expect(exporter).to receive(:enrollments_for_csv).with([enrollment]).and_call_original
|
|
exporter.to_csv
|
|
end
|
|
|
|
context "when a grading period is present" do
|
|
let(:group) { Factories::GradingPeriodGroupHelper.new.legacy_create_for_course(@course) }
|
|
let(:grading_period) do
|
|
group.grading_periods.create!(
|
|
start_date: 1.week.ago, end_date: 1.week.from_now, title: "test period"
|
|
)
|
|
end
|
|
let(:exporter) { GradebookExporter.new(@course, @teacher, { grading_period_id: grading_period.id }) }
|
|
|
|
before(:each) do
|
|
allow(exporter).to receive(:enrollments_for_csv).and_return([enrollment])
|
|
end
|
|
|
|
it 'includes the computed current score for the grading period' do
|
|
expect(enrollment).to receive(:computed_current_score).with({ grading_period_id: grading_period.id })
|
|
exporter.to_csv
|
|
end
|
|
|
|
it 'includes the unposted current score for the grading period' do
|
|
expect(enrollment).to receive(:unposted_current_score).with({ grading_period_id: grading_period.id })
|
|
exporter.to_csv
|
|
end
|
|
|
|
it 'includes the computed final score for the grading period' do
|
|
expect(enrollment).to receive(:computed_final_score).with({ grading_period_id: grading_period.id })
|
|
exporter.to_csv
|
|
end
|
|
|
|
it 'includes the unposted final score for the grading period' do
|
|
expect(enrollment).to receive(:unposted_final_score).with({ grading_period_id: grading_period.id })
|
|
exporter.to_csv
|
|
end
|
|
|
|
it 'includes the computed current grade for the grading period' do
|
|
expect(enrollment).to receive(:computed_current_grade).with({ grading_period_id: grading_period.id })
|
|
exporter.to_csv
|
|
end
|
|
|
|
it 'includes the unposted current grade for the grading period' do
|
|
expect(enrollment).to receive(:unposted_current_grade).with({ grading_period_id: grading_period.id })
|
|
exporter.to_csv
|
|
end
|
|
|
|
it 'includes the computed final grade for the grading period' do
|
|
expect(enrollment).to receive(:computed_final_grade).with({ grading_period_id: grading_period.id })
|
|
exporter.to_csv
|
|
end
|
|
|
|
it 'includes the unposted final grade for the grading period' do
|
|
expect(enrollment).to receive(:unposted_final_grade).with({ grading_period_id: grading_period.id })
|
|
exporter.to_csv
|
|
end
|
|
|
|
context "when final grade override is enabled for the course" do
|
|
before(:each) { enable_final_grade_override! }
|
|
|
|
let(:parsed_csv) { CSV.parse(exporter.to_csv, headers: true) }
|
|
|
|
it "includes the overridden score for the current grading period" do
|
|
aggregate_failures do
|
|
expect(enrollment).to receive(:override_score).with({ grading_period_id: grading_period.id }).and_return(64)
|
|
expect(parsed_csv[1]["Override Score (#{grading_period.title})"]).to eq("64")
|
|
end
|
|
end
|
|
|
|
it "includes the overridden grade for the current grading period if the course has a grading standard" do
|
|
aggregate_failures do
|
|
expect(enrollment).to receive(:override_grade).with({ grading_period_id: grading_period.id }).and_return("D")
|
|
expect(parsed_csv[1]["Override Grade (#{grading_period.title})"]).to eq("D")
|
|
end
|
|
end
|
|
|
|
it "omits the overridden grade for the current grading period if the course has no grading standard" do
|
|
@course.update!(grading_standard_id: nil)
|
|
|
|
aggregate_failures do
|
|
expect(enrollment).not_to receive(:override_grade)
|
|
expect(parsed_csv.headers).not_to include("Override Grade")
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when final grade override is not allowed for the course" do
|
|
before(:each) do
|
|
@course.enable_feature!(:final_grades_override)
|
|
@course.update!(allow_final_grade_override: false)
|
|
end
|
|
|
|
let(:parsed_csv) { CSV.parse(exporter.to_csv, headers: true) }
|
|
|
|
it "does not include the overridden score for the current grading period" do
|
|
aggregate_failures do
|
|
expect(enrollment).not_to receive(:override_score)
|
|
expect(parsed_csv.headers).not_to include("Override Score")
|
|
end
|
|
end
|
|
|
|
it "does not include the overridden grade for the current grading period" do
|
|
aggregate_failures do
|
|
expect(enrollment).not_to receive(:override_grade)
|
|
expect(parsed_csv.headers).not_to include("Override Grade")
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when final grade override is not enabled for the course" do
|
|
let(:parsed_csv) { CSV.parse(exporter.to_csv, headers: true) }
|
|
|
|
it "does not include the overridden score for the current grading period" do
|
|
aggregate_failures do
|
|
expect(enrollment).not_to receive(:override_score)
|
|
expect(parsed_csv.headers).not_to include("Override Score")
|
|
end
|
|
end
|
|
|
|
it "does not include the overridden grade for the current grading period" do
|
|
aggregate_failures do
|
|
expect(enrollment).not_to receive(:override_grade)
|
|
expect(parsed_csv.headers).not_to include("Override Grade")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when no grading period is supplied" do
|
|
let(:exporter) { GradebookExporter.new(@course, @teacher) }
|
|
|
|
before(:each) do
|
|
allow(exporter).to receive(:enrollments_for_csv).and_return([enrollment])
|
|
end
|
|
|
|
it 'includes the computed current score for the course' do
|
|
expect(enrollment).to receive(:computed_current_score).with(Score.params_for_course)
|
|
exporter.to_csv
|
|
end
|
|
|
|
it 'includes the unposted current score for the course' do
|
|
expect(enrollment).to receive(:unposted_current_score).with(Score.params_for_course)
|
|
exporter.to_csv
|
|
end
|
|
|
|
it 'includes the computed final score for the course' do
|
|
expect(enrollment).to receive(:computed_final_score).with(Score.params_for_course)
|
|
exporter.to_csv
|
|
end
|
|
|
|
it 'includes the unposted final score for the course' do
|
|
expect(enrollment).to receive(:unposted_final_score).with(Score.params_for_course)
|
|
exporter.to_csv
|
|
end
|
|
|
|
it 'includes the computed current grade for the course' do
|
|
expect(enrollment).to receive(:computed_current_grade).with(Score.params_for_course)
|
|
exporter.to_csv
|
|
end
|
|
|
|
it 'includes the unposted current grade for the course' do
|
|
expect(enrollment).to receive(:unposted_current_grade).with(Score.params_for_course)
|
|
exporter.to_csv
|
|
end
|
|
|
|
it 'includes the computed final grade for the course' do
|
|
expect(enrollment).to receive(:computed_final_grade).with(Score.params_for_course)
|
|
exporter.to_csv
|
|
end
|
|
|
|
it 'includes the unposted final grade for the course' do
|
|
expect(enrollment).to receive(:unposted_final_grade).with(Score.params_for_course)
|
|
exporter.to_csv
|
|
end
|
|
|
|
context "when final grade override is enabled for the course" do
|
|
before(:each) { enable_final_grade_override! }
|
|
|
|
let(:parsed_csv) { CSV.parse(exporter.to_csv, headers: true) }
|
|
|
|
it "includes the overridden score for the course" do
|
|
aggregate_failures do
|
|
expect(enrollment).to receive(:override_score).with(Score.params_for_course).and_return(78)
|
|
expect(parsed_csv[1]["Override Score"]).to eq("78")
|
|
end
|
|
end
|
|
|
|
it "includes the overridden grade for the course" do
|
|
aggregate_failures do
|
|
expect(enrollment).to receive(:override_grade).with(Score.params_for_course).and_return("C+")
|
|
expect(parsed_csv[1]["Override Grade"]).to eq("C+")
|
|
end
|
|
end
|
|
|
|
it "omits the overridden grade for the course if the course has no grading standard" do
|
|
@course.update!(grading_standard_id: nil)
|
|
aggregate_failures do
|
|
expect(enrollment).not_to receive(:override_grade)
|
|
expect(parsed_csv.headers).not_to include("Override Grade")
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when final grade override is not allowed for the course" do
|
|
before(:each) do
|
|
@course.enable_feature!(:final_grades_override)
|
|
@course.update!(allow_final_grade_override: false)
|
|
end
|
|
|
|
let(:parsed_csv) { CSV.parse(exporter.to_csv, headers: true) }
|
|
|
|
it "does not include the overridden score for the course" do
|
|
aggregate_failures do
|
|
expect(enrollment).not_to receive(:override_score)
|
|
expect(parsed_csv.headers).not_to include("Override Score")
|
|
end
|
|
end
|
|
|
|
it "does not include the overridden grade for the course" do
|
|
aggregate_failures do
|
|
expect(enrollment).not_to receive(:override_grade)
|
|
expect(parsed_csv.headers).not_to include("Override Grade")
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when final grade override is not enabled for the course" do
|
|
let(:parsed_csv) { CSV.parse(exporter.to_csv, headers: true) }
|
|
|
|
it "does not include the overridden score for the course" do
|
|
aggregate_failures do
|
|
expect(enrollment).not_to receive(:override_score)
|
|
expect(parsed_csv.headers).not_to include("Override Score")
|
|
end
|
|
end
|
|
|
|
it "does not include the overridden grade for the course" do
|
|
aggregate_failures do
|
|
expect(enrollment).not_to receive(:override_grade)
|
|
expect(parsed_csv.headers).not_to include("Override Grade")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|