canvas-lms/spec/models/assignment_group_spec.rb

659 lines
23 KiB
Ruby

# frozen_string_literal: true
#
# Copyright (C) 2011 - 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/>.
#
describe AssignmentGroup do
before(:once) do
@valid_attributes = {
name: "value for name",
rules: "value for rules",
default_assignment_name: "value for default assignment name",
assignment_weighting_scheme: "value for assignment weighting scheme",
group_weight: 1.0
}
course_with_student(active_all: true)
@course.update_attribute(:group_weighting_scheme, "percent")
end
it "acts as list" do
expect(AssignmentGroup).to respond_to(:acts_as_list)
end
it "converts NaN group weight values to 0 on save" do
ag = @course.assignment_groups.create!(@valid_attributes)
ag.group_weight = 0 / 0.0
ag.save!
expect(ag.group_weight).to eq 0
end
it "allows association with scores" do
ag = @course.assignment_groups.create!(@valid_attributes)
@course.student_enrollments.first
score = ag.scores.first
expect(score.assignment_group_id).to be ag.id
end
context "visible_assignments" do
before do
@ag = @course.assignment_groups.create!(@valid_attributes)
@s = @course.course_sections.create!(name: "test section")
student_in_section(@s, user: @student)
assignments = (0...4).map do
@course.assignments.create!(
title: "test_foo",
assignment_group: @ag,
points_possible: 10,
only_visible_to_overrides: true
)
end
@destroyed_assignment = assignments.first
@destroyed_assignment.destroy
@assignment = assignments.second
create_adhoc_override_for_assignment(@assignment, @student)
@overridden_assignment = assignments.last
create_section_override_for_assignment(@overridden_assignment, course_section: @s)
@course.reload
@ag.reload
end
describe "class method" do
it "optionally scopes results to specific assignment IDs" do
assignment_ids = AssignmentGroup.visible_assignments(
@student,
@course,
[@ag],
assignment_ids: [@assignment.id]
).pluck(:id)
expect(assignment_ids).to match_array [@assignment.id]
end
it "does not include requested assignments that would otherwise not be returned" do
assignment_ids = AssignmentGroup.visible_assignments(
@student,
@course,
[@ag],
assignment_ids: [@assignment.id, @destroyed_assignment.id]
).pluck(:id)
expect(assignment_ids).to match_array [@assignment.id]
end
it "gracefully ignores assignment_ids if passed nil" do
assignment_ids = AssignmentGroup.visible_assignments(
@student,
@course,
[@ag],
assignment_ids: nil
).pluck(:id)
expect(assignment_ids).to match_array [@assignment.id, @overridden_assignment.id]
end
end
describe "instance method" do
it "optionally scopes results to specific assignment IDs" do
assignment_ids = @ag.visible_assignments(@student, assignment_ids: [@assignment.id]).pluck(:id)
expect(assignment_ids).to match_array [@assignment.id]
end
it "does not include requested assignments that would otherwise not be returned" do
assignment_ids = @ag.visible_assignments(
@student,
assignment_ids: [@assignment.id, @destroyed_assignment.id]
).pluck(:id)
expect(assignment_ids).to match_array [@assignment.id]
end
it "gracefully ignores assignment_ids if passed nil" do
assignment_ids = @ag.visible_assignments(@student, assignment_ids: nil).pluck(:id)
expect(assignment_ids).to match_array [@assignment.id, @overridden_assignment.id]
end
end
context "with differentiated assignments and draft state on" do
it "returns only active assignments with overrides or grades for the user" do
expect(@ag.active_assignments.count).to eq 3
# one with override, one with grade
expect(@ag.visible_assignments(@student).count).to eq 2
expect(AssignmentGroup.visible_assignments(@student, @course, [@ag]).count).to eq 2
end
end
context "logged out users" do
it "returns published assignments for logged out users so that invited users can see them before accepting a course invite" do
@course.active_assignments.first.unpublish
expect(@ag.visible_assignments(nil).count).to eq 2
expect(AssignmentGroup.visible_assignments(nil, @course, [@ag]).count).to eq 2
end
end
end
context "broadcast policy" do
context "grade weight changed" do
before(:once) do
Notification.create!(name: "Grade Weight Changed", category: "TestImmediately")
assignment_group_model
end
it "sends a notification when the grade weight changes" do
@ag.update_attribute(:group_weight, 0.2)
expect(@ag.context.messages_sent["Grade Weight Changed"].any? { |m| m.user_id == @student.id }).to be_truthy
end
it "sends a notification to observers when the grade weight changes" do
course_with_observer(course: @course, associated_user_id: @student.id, active_all: true)
@ag.reload.update_attribute(:group_weight, 0.2)
expect(@ag.context.messages_sent["Grade Weight Changed"].any? { |m| m.user_id == @observer.id }).to be_truthy
end
end
end
it "has a state machine" do
assignment_group_model
expect(@ag.state).to be(:available)
end
it "returns never_drop list as ints" do
expected = [9, 22, 16, 4]
rules = "drop_lowest:2\n"
expected.each do |val|
rules += "never_drop:#{val}\n"
end
assignment_group_model(rules:)
result = @ag.rules_hash
expect(result["never_drop"]).to eql(expected)
end
it "converts rules decimal values into integers" do
custom_rules_hash = {
drop_lowest: 1.1,
drop_highest: 2.9,
never_drop: [1, 2]
}.with_indifferent_access
assignment_group_model.rules_hash = custom_rules_hash
expect(@ag.rules).to eql("drop_lowest:1\ndrop_highest:2\nnever_drop:1\nnever_drop:2\n")
end
it "converts string values into integers" do
custom_rules_hash = {
drop_lowest: "123abc",
drop_highest: 2,
never_drop: [1, 2]
}.with_indifferent_access
assignment_group_model.rules_hash = custom_rules_hash
expect(@ag.rules).to eql("drop_lowest:123\ndrop_highest:2\nnever_drop:1\nnever_drop:2\n")
end
it "validate is false when drop_lowest is negative" do
rules = "drop_lowest:-1\ndrop_highest:1\nnever_drop:1\nnever_drop:2\n"
assignment_group_model(rules:)
@ag.validate_rules = true
@ag.reload
@course.assignments.create!(title: "test", assignment_group: @ag)
expect(@ag.validate).to be(false)
expect(@ag.errors.full_messages).to eql(["Rules Drop rules must be a positive number"])
end
it "validate is false when drop_highest is negative" do
rules = "drop_lowest:1\ndrop_highest:-1\nnever_drop:1\nnever_drop:2\n"
assignment_group_model(rules:)
@ag.validate_rules = true
@ag.reload
@course.assignments.create!(title: "test", assignment_group: @ag)
expect(@ag.validate).to be(false)
expect(@ag.errors.full_messages).to eql(["Rules Drop rules must be a positive number"])
end
it "validate is false when drop_lowest is greater than number of assignments" do
rules = "drop_lowest:2\ndrop_highest:1\nnever_drop:1\nnever_drop:2\n"
assignment_group_model(rules:)
@ag.validate_rules = true
@ag.reload
@course.assignments.create!(title: "test", assignment_group: @ag)
expect(@ag.validate).to be(false)
expect(@ag.errors.full_messages).to eql(["Rules Drop rules cannot be higher than the number of assignments"])
end
it "validate is false when drop_highest and drop_lowest are valid" do
rules = "drop_lowest:1\ndrop_highest:1\nnever_drop:1\nnever_drop:2\n"
assignment_group_model(rules:)
@ag.validate_rules = true
@ag.reload
@course.assignments.create!(title: "test", assignment_group: @ag)
expect(@ag.validate).to be(true)
end
it "validate is true when ndrop_highest is greater than number of assignments" do
rules = "drop_lowest:1\ndrop_highest:2\nnever_drop:1\nnever_drop:2\n"
assignment_group_model(rules:)
@ag.validate_rules = true
@ag.reload
@course.assignments.create!(title: "test", assignment_group: @ag)
expect(@ag.validate).to be(false)
expect(@ag.errors.full_messages).to eql(["Rules Drop rules cannot be higher than the number of assignments"])
end
it "returns never_drop list as strings if `stringify_json_ids` is true" do
expected = %w[9 22 16 4]
rules = "drop_highest:25\n"
expected.each do |val|
rules += "never_drop:#{val}\n"
end
assignment_group_model(rules:)
result = @ag.rules_hash({ stringify_json_ids: true })
expect(result["never_drop"]).to eql(expected)
end
it "returns rules that aren't never_drops as ints" do
rules = "drop_highest:25\n"
assignment_group_model(rules:)
result = @ag.rules_hash
expect(result["drop_highest"]).to be(25)
end
it "returns rules that aren't never_drops as ints when `strigify_json_ids` is true" do
rules = "drop_lowest:2\n"
assignment_group_model(rules:)
result = @ag.rules_hash({ stringify_json_ids: true })
expect(result["drop_lowest"]).to be(2)
end
describe "#grants_right?" do
before(:once) do
@assignment_group = @course.assignment_groups.create!(@valid_attributes)
assignments = (0..1).map do |n|
@course.assignments.create!(
title: "Example Assignment #{n}",
assignment_group: @assignment_group,
points_possible: 10
)
end
@assignment = assignments.first
@quiz = quiz_model(course: @course)
@quiz.assignment_group_id = @assignment_group.id
@quiz.save!
@admin = account_admin_user
teacher_in_course(course: @course)
@grading_period_group = @course.root_account.grading_period_groups.create!(title: "Example Group")
@grading_period_group.enrollment_terms << @course.enrollment_term
@course.enrollment_term.save!
@assignment_group.reload
@grading_period_group.grading_periods.create!({
title: "Example Grading Period",
start_date: 5.weeks.ago,
end_date: 3.weeks.ago,
close_date: 1.week.ago
})
@grading_period_group.grading_periods.create!({
title: "Example Grading Period",
start_date: 3.weeks.ago,
end_date: 1.week.ago,
close_date: 1.week.from_now
})
end
context "to delete" do
context "without grading periods" do
it "is true for admins" do
allow(@course).to receive(:grading_periods?).and_return false
expect(@assignment_group.reload.grants_right?(@admin, :delete)).to be true
end
it "is false for teachers" do
allow(@course).to receive(:grading_periods?).and_return false
expect(@assignment_group.reload.grants_right?(@teacher, :delete)).to be true
end
end
context "when the assignment is due in a closed grading period" do
before(:once) do
@assignment.update(due_at: 4.weeks.ago)
end
it "is true for admins" do
expect(@assignment_group.reload.grants_right?(@admin, :delete)).to be(true)
end
it "is false for teachers" do
expect(@assignment_group.reload.grants_right?(@teacher, :delete)).to be(false)
end
end
context "when the assignment is due in an open grading period" do
before(:once) do
@assignment.update(due_at: 2.weeks.ago)
end
it "is true for admins" do
expect(@assignment_group.reload.grants_right?(@admin, :delete)).to be(true)
end
it "is true for teachers" do
expect(@assignment_group.reload.grants_right?(@teacher, :delete)).to be(true)
end
end
context "when the assignment is due after all grading periods" do
before(:once) do
@assignment.update(due_at: 1.day.from_now)
end
it "is true for admins" do
expect(@assignment_group.reload.grants_right?(@admin, :delete)).to be(true)
end
it "is true for teachers" do
expect(@assignment_group.reload.grants_right?(@teacher, :delete)).to be(true)
end
end
context "when the assignment is due before all grading periods" do
before(:once) do
@assignment.update(due_at: 6.weeks.ago)
end
it "is true for admins" do
expect(@assignment_group.reload.grants_right?(@admin, :delete)).to be(true)
end
it "is true for teachers" do
expect(@assignment_group.reload.grants_right?(@teacher, :delete)).to be(true)
end
end
context "when the assignment has no due date" do
before(:once) do
@assignment.update(due_at: nil)
end
it "is true for admins" do
expect(@assignment_group.reload.grants_right?(@admin, :delete)).to be(true)
end
it "is true for teachers" do
expect(@assignment_group.reload.grants_right?(@teacher, :delete)).to be(true)
end
end
context "when the assignment is due in a closed grading period for a student" do
before(:once) do
@assignment.update(due_at: 2.days.from_now)
override = @assignment.assignment_overrides.build
override.set = @course.default_section
override.override_due_at(4.weeks.ago)
override.save!
end
it "is true for admins" do
expect(@assignment_group.reload.grants_right?(@admin, :delete)).to be(true)
end
it "is false for teachers" do
expect(@assignment_group.reload.grants_right?(@teacher, :delete)).to be(false)
end
end
context "when the assignment is overridden with no due date for a student" do
before(:once) do
@assignment.update(due_at: nil)
override = @assignment.assignment_overrides.build
override.set = @course.default_section
override.save!
end
it "is true for admins" do
expect(@assignment_group.reload.grants_right?(@admin, :delete)).to be(true)
end
it "is true for teachers" do
expect(@assignment_group.reload.grants_right?(@teacher, :delete)).to be(true)
end
end
context "when the assignment is deleted and due in a closed grading period" do
before(:once) do
@assignment.update(due_at: 4.weeks.ago)
@assignment.destroy
end
it "is true for admins" do
expect(@assignment_group.reload.grants_right?(@admin, :delete)).to be(true)
end
it "is true for teachers" do
expect(@assignment_group.reload.grants_right?(@teacher, :delete)).to be(true)
end
end
context "when the quiz is due in a closed grading period" do
before(:once) do
@quiz.update(due_at: 4.weeks.ago)
end
it "is true for admins" do
expect(@assignment_group.reload.grants_right?(@admin, :delete)).to be(true)
end
it "is false for teachers" do
expect(@assignment_group.reload.grants_right?(@teacher, :delete)).to be(false)
end
end
context "when the quiz is due in an open grading period" do
before(:once) do
@quiz.update(due_at: 2.weeks.ago)
end
it "is true for admins" do
expect(@assignment_group.reload.grants_right?(@admin, :delete)).to be(true)
end
it "is true for teachers" do
expect(@assignment_group.reload.grants_right?(@teacher, :delete)).to be(true)
end
end
context "when the quiz is due after all grading periods" do
before(:once) do
@quiz.update(due_at: 1.day.from_now)
end
it "is true for admins" do
expect(@assignment_group.reload.grants_right?(@admin, :delete)).to be(true)
end
it "is true for teachers" do
expect(@assignment_group.reload.grants_right?(@teacher, :delete)).to be(true)
end
end
context "when the quiz is due before all grading periods" do
before(:once) do
@quiz.update(due_at: 6.weeks.ago)
end
it "is true for admins" do
expect(@assignment_group.reload.grants_right?(@admin, :delete)).to be(true)
end
it "is true for teachers" do
expect(@assignment_group.reload.grants_right?(@teacher, :delete)).to be(true)
end
end
context "when the quiz has no due date" do
before(:once) do
@quiz.update(due_at: nil)
end
it "is true for admins" do
expect(@assignment_group.reload.grants_right?(@admin, :delete)).to be(true)
end
it "is true for teachers" do
expect(@assignment_group.reload.grants_right?(@teacher, :delete)).to be(true)
end
end
context "when the quiz is due in a closed grading period for a student" do
before(:once) do
@quiz.update(due_at: 2.days.from_now)
override = @quiz.assignment_overrides.build
override.set = @course.default_section
override.override_due_at(4.weeks.ago)
override.save!
end
it "is true for admins" do
expect(@assignment_group.reload.grants_right?(@admin, :delete)).to be(true)
end
it "is false for teachers" do
expect(@assignment_group.reload.grants_right?(@teacher, :delete)).to be(false)
end
end
context "when the quiz is overridden with no due date for a student" do
before(:once) do
@quiz.update(due_at: nil)
override = @quiz.assignment_overrides.build
override.set = @course.default_section
override.save!
end
it "is true for admins" do
expect(@assignment_group.reload.grants_right?(@admin, :delete)).to be(true)
end
it "is true for teachers" do
expect(@assignment_group.reload.grants_right?(@teacher, :delete)).to be(true)
end
end
context "when the quiz is deleted and due in a closed grading period" do
before(:once) do
@quiz.update(due_at: 4.weeks.ago)
@quiz.destroy
end
it "is true for admins" do
expect(@assignment_group.reload.grants_right?(@admin, :delete)).to be(true)
end
it "is true for teachers" do
expect(@assignment_group.reload.grants_right?(@teacher, :delete)).to be(true)
end
end
end
end
describe "#any_assignment_in_closed_grading_period?" do
it "calls EffectiveDueDates#in_closed_grading_period?" do
assignment_group_model
edd = EffectiveDueDates.for_course(@ag.context, @ag.published_assignments)
expect(EffectiveDueDates).to receive(:for_course).with(@ag.context, @ag.published_assignments).and_return(edd)
expect(edd).to receive(:any_in_closed_grading_period?).and_return(true)
expect(@ag.any_assignment_in_closed_grading_period?).to be(true)
end
end
describe "#destroy" do
before(:once) do
@student_enrollment = @student.enrollments.find_by(course_id: @course)
@group = @course.assignment_groups.create!(@valid_attributes)
end
let(:student_score) do
Score.find_by(enrollment_id: @student_enrollment, assignment_group_id: @group)
end
it "destroys scores belonging to active students" do
expect { @group.destroy }.to change { student_score.reload.state }.from(:active).to(:deleted)
end
it "does not destroy scores belonging to concluded students" do
@student_enrollment.conclude
expect { @group.destroy }.not_to change { student_score.reload.state }
end
it "destroys active assignments belonging to the group" do
assignment = @course.assignments.create!
@group.destroy
expect(assignment.reload).to be_deleted
end
it "does not run validations on soft-deleted assignments belonging to the group" do
now = Time.zone.now
assignment = @course.assignments.create!(
unlock_at: 3.days.ago(now),
due_at: now,
lock_at: 3.days.from_now(now),
workflow_state: "deleted"
)
# update the assignment to be invalid, so that if validations are run
# we'll get an error
assignment.update_columns(lock_at: 2.days.ago(now))
expect { @group.destroy }.not_to raise_error
end
end
describe "#restore" do
before(:once) do
@student_enrollment = @student.enrollments.find_by(course_id: @course)
@group = @course.assignment_groups.create!(@valid_attributes)
@group.destroy
end
let(:student_score) do
Score.find_by(enrollment_id: @student_enrollment, assignment_group_id: @group)
end
it "restores the assignment group back to an 'available' state" do
expect { @group.restore }.to change { @group.state }.from(:deleted).to(:available)
end
it "restores scores belonging to active students" do
expect { @group.restore }.to change { student_score.reload.state }.from(:deleted).to(:active)
end
it "does not restore scores belonging to concluded students" do
@student_enrollment.conclude
expect { @group.restore }.not_to change { student_score.reload.state }
end
it "does not restore scores belonging to deleted students" do
@student_enrollment.destroy
expect { @group.restore }.not_to change { student_score.reload.state }
end
end
describe "#create" do
it "sets the root_account_id using context" do
group = @course.assignment_groups.create!(@valid_attributes)
expect(group.root_account_id).to eq @course.root_account_id
end
end
end
def assignment_group_model(opts = {})
@ag = @course.assignment_groups.create!(@valid_attributes.merge(opts))
end