9032 lines
343 KiB
Ruby
9032 lines
343 KiB
Ruby
#
|
|
# 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/>.
|
|
#
|
|
|
|
require 'spec_helper'
|
|
require_relative '../selenium/helpers/groups_common'
|
|
require_relative '../lti2_spec_helper'
|
|
|
|
describe Assignment do
|
|
include_context 'lti2_spec_helper'
|
|
|
|
describe 'relationships' do
|
|
it { is_expected.to have_one(:score_statistic).dependent(:destroy) }
|
|
it { is_expected.to have_many(:moderation_graders) }
|
|
it { is_expected.to have_many(:moderation_grader_users) }
|
|
it { is_expected.to have_one(:post_policy).dependent(:destroy).inverse_of(:assignment) }
|
|
end
|
|
|
|
before :once do
|
|
course_with_teacher(active_all: true)
|
|
@initial_student = student_in_course(active_all: true, user_name: 'a student').user
|
|
end
|
|
|
|
# workaround for our version of shoulda-matchers not having the 'optional' method
|
|
it { is_expected.to belong_to(:grader_section).class_name('CourseSection') }
|
|
it { is_expected.not_to validate_presence_of(:grader_section) }
|
|
it { is_expected.to belong_to(:final_grader).class_name('User') }
|
|
it { is_expected.not_to validate_presence_of(:final_grader) }
|
|
|
|
it "should create a new instance given valid attributes" do
|
|
course = @course.assignments.create!(assignment_valid_attributes)
|
|
expect(course).to be_valid
|
|
end
|
|
|
|
it "should set the lti_context_id on create" do
|
|
assignment = @course.assignments.create!(assignment_valid_attributes)
|
|
expect(assignment.lti_context_id).to be_present
|
|
end
|
|
|
|
it "should have a useful state machine" do
|
|
assignment_model(course: @course)
|
|
expect(@a.state).to eql(:published)
|
|
@a.unpublish
|
|
expect(@a.state).to eql(:unpublished)
|
|
end
|
|
|
|
it "should always be associated with a group" do
|
|
assignment_model(course: @course)
|
|
@assignment.save!
|
|
expect(@assignment.assignment_group).not_to be_nil
|
|
end
|
|
|
|
it "should be associated with a group when the course has no active groups" do
|
|
@course.require_assignment_group
|
|
@course.assignment_groups.first.destroy
|
|
expect(@course.assignment_groups.size).to eq 1
|
|
expect(@course.assignment_groups.active.size).to eq 0
|
|
@assignment = assignment_model(:course => @course)
|
|
expect(@assignment.assignment_group).not_to be_nil
|
|
end
|
|
|
|
it "should touch assignment group on create/save" do
|
|
group = @course.assignment_groups.create!(:name => "Assignments")
|
|
AssignmentGroup.where(:id => group).update_all(:updated_at => 1.hour.ago)
|
|
orig_time = group.reload.updated_at.to_i
|
|
a = @course.assignments.build("title"=>"test")
|
|
a.assignment_group = group
|
|
a.save!
|
|
expect(@course.assignments.count).to eq 1
|
|
group.reload
|
|
expect(group.updated_at.to_i).not_to eq orig_time
|
|
end
|
|
|
|
it "should be able to submit homework" do
|
|
setup_assignment_with_homework
|
|
expect(@assignment.submissions.size).to eql(1)
|
|
@submission = @assignment.submissions.first
|
|
expect(@submission.user_id).to eql(@user.id)
|
|
expect(@submission.versions.length).to eql(1)
|
|
end
|
|
|
|
it "should validate grading_type inclusion" do
|
|
@invalid_grading_type = "invalid"
|
|
@assignment = Assignment.new(assignment_valid_attributes.merge({
|
|
course: @course,
|
|
grading_type: @invalid_grading_type
|
|
}))
|
|
|
|
expect(@assignment).not_to be_valid
|
|
expect(@assignment.errors[:grading_type]).not_to be_nil
|
|
end
|
|
|
|
describe 'default values' do
|
|
it 'sets grader_count to 0' do
|
|
assignment = Assignment.create!(
|
|
course: @course,
|
|
name: 'some assignment',
|
|
anonymous_grading: true
|
|
)
|
|
expect(assignment.grader_count).to be 0
|
|
end
|
|
end
|
|
|
|
describe 'callbacks' do
|
|
describe 'apply_late_policy' do
|
|
it "calls apply_late_policy for the assignment if points_possible changes" do
|
|
assignment = @course.assignments.new(assignment_valid_attributes)
|
|
expect(LatePolicyApplicator).to receive(:for_assignment).with(assignment)
|
|
|
|
assignment.update!(points_possible: 3.14)
|
|
end
|
|
|
|
it 'invokes the LatePolicyApplicator for this assignment if grading type changes but due dates do not' do
|
|
assignment = @course.assignments.new(assignment_valid_attributes)
|
|
|
|
allow(assignment).to receive(:update_cached_due_dates?).and_return(false)
|
|
allow(assignment).to receive(:saved_change_to_grading_type?).and_return(true)
|
|
expect(LatePolicyApplicator).to receive(:for_assignment).with(assignment)
|
|
|
|
assignment.save!
|
|
end
|
|
|
|
it 'invokes the LatePolicyApplicator only once if grading type changes and due dates also change' do
|
|
assignment = @course.assignments.new(assignment_valid_attributes)
|
|
|
|
allow(assignment).to receive(:update_cached_due_dates?).and_return(true)
|
|
allow(assignment).to receive(:saved_change_to_grading_type?).and_return(true)
|
|
expect(LatePolicyApplicator).to receive(:for_assignment).with(assignment).once
|
|
|
|
assignment.save!
|
|
end
|
|
|
|
it 'does not invoke the LatePolicyApplicator if neither grading type nor due dates change' do
|
|
assignment = @course.assignments.new(assignment_valid_attributes)
|
|
|
|
allow(assignment).to receive(:update_cached_due_dates?).and_return(false)
|
|
allow(assignment).to receive(:saved_change_to_grading_type?).and_return(false)
|
|
expect(LatePolicyApplicator).not_to receive(:for_assignment).with(assignment)
|
|
|
|
assignment.save!
|
|
end
|
|
|
|
it 'invokes the LatePolicyApplicator only once if grading type does not change but due dates change' do
|
|
assignment = @course.assignments.new(assignment_valid_attributes)
|
|
|
|
allow(assignment).to receive(:update_cached_due_dates?).and_return(true)
|
|
allow(assignment).to receive(:saved_change_to_grading_type?).and_return(false)
|
|
expect(LatePolicyApplicator).to receive(:for_assignment).with(assignment).once
|
|
|
|
assignment.save!
|
|
end
|
|
end
|
|
|
|
describe 'update_cached_due_dates' do
|
|
it 'invokes DueDateCacher if due_at is changed' do
|
|
assignment = @course.assignments.new(assignment_valid_attributes)
|
|
expect(DueDateCacher).to receive(:recompute).with(assignment, update_grades: true)
|
|
|
|
assignment.update!(due_at: assignment.due_at + 1.day)
|
|
end
|
|
|
|
it 'invokes DueDateCacher if workflow_state is changed' do
|
|
assignment = @course.assignments.new(assignment_valid_attributes)
|
|
expect(DueDateCacher).to receive(:recompute).with(assignment, update_grades: true)
|
|
|
|
assignment.destroy
|
|
end
|
|
|
|
it 'invokes DueDateCacher if only_visible_to_overrides is changed' do
|
|
assignment = @course.assignments.new(assignment_valid_attributes)
|
|
expect(DueDateCacher).to receive(:recompute).with(assignment, update_grades: true)
|
|
|
|
assignment.update!(only_visible_to_overrides: !assignment.only_visible_to_overrides?)
|
|
end
|
|
|
|
it 'invokes DueDateCacher if moderated_grading is changed' do
|
|
assignment = @course.assignments.new(assignment_valid_attributes)
|
|
expect(DueDateCacher).to receive(:recompute).with(assignment, update_grades: true)
|
|
|
|
assignment.update!(moderated_grading: !assignment.moderated_grading, grader_count: 2)
|
|
end
|
|
|
|
it 'invokes DueDateCacher after save when moderated_grading becomes enabled' do
|
|
assignment = @course.assignments.create!(assignment_valid_attributes)
|
|
assignment.reload
|
|
|
|
expect(DueDateCacher).to receive(:recompute).with(assignment, update_grades: true)
|
|
|
|
assignment.moderated_grading = true
|
|
assignment.grader_count = 2
|
|
|
|
assignment.update_cached_due_dates
|
|
end
|
|
|
|
it 'invokes DueDateCacher if called in a before_save context' do
|
|
assignment = @course.assignments.new(assignment_valid_attributes)
|
|
allow(assignment).to receive(:update_cached_due_dates?).and_return(true)
|
|
expect(DueDateCacher).to receive(:recompute).with(assignment, update_grades: true)
|
|
|
|
assignment.save!
|
|
end
|
|
|
|
it 'invokes DueDateCacher if called in an after_save context' do
|
|
assignment = @course.assignments.new(assignment_valid_attributes)
|
|
|
|
Assignment.suspend_callbacks(:update_cached_due_dates) do
|
|
assignment.update!(due_at: assignment.due_at + 1.day)
|
|
end
|
|
|
|
expect(DueDateCacher).to receive(:recompute).with(assignment, update_grades: true)
|
|
|
|
assignment.update_cached_due_dates
|
|
end
|
|
end
|
|
|
|
describe "automatic setting of post policies" do
|
|
let(:teacher) { @course.enroll_teacher(User.create!, enrollment_state: :active).user }
|
|
|
|
it "newly-created anonymous assignments are set to post manually" do
|
|
assignment = @course.assignments.create!(title: 'hi', anonymous_grading: true)
|
|
expect(assignment.post_policy).to be_post_manually
|
|
end
|
|
|
|
it "existing assignments are set to post manually if anonymous grading is enabled" do
|
|
assignment = @course.assignments.create!(title: 'hi')
|
|
assignment.post_policy.update!(post_manually: false)
|
|
|
|
assignment.update!(anonymous_grading: true)
|
|
expect(assignment.post_policy).to be_post_manually
|
|
end
|
|
|
|
it "newly-created moderated assignments are set to post manually" do
|
|
assignment = @course.assignments.create!(
|
|
final_grader: teacher,
|
|
grader_count: 5,
|
|
title: 'hi',
|
|
moderated_grading: true
|
|
)
|
|
expect(assignment.post_policy).to be_post_manually
|
|
end
|
|
|
|
it "existing assignments are set to post manually if moderated grading is enabled" do
|
|
assignment = @course.assignments.create!(title: 'hi')
|
|
assignment.post_policy.update!(post_manually: false)
|
|
|
|
assignment.update!(moderated_grading: true, grader_count: 5, final_grader: teacher)
|
|
expect(assignment.post_policy).to be_post_manually
|
|
end
|
|
|
|
context "for newly-created non-anonymous, non-moderated assignments" do
|
|
it "the post policy is set to manual for a manually-posted course" do
|
|
@course.default_post_policy.update!(post_manually: true)
|
|
assignment = @course.assignments.create!
|
|
expect(assignment.post_policy).to be_post_manually
|
|
end
|
|
|
|
it "the post policy is set to automatic for a automatically-posted course" do
|
|
@course.default_post_policy.update!(post_manually: false)
|
|
assignment = @course.assignments.create!
|
|
expect(assignment.post_policy).not_to be_post_manually
|
|
end
|
|
|
|
it "the post policy is set to automatic if the course has no post policy" do
|
|
@course.default_post_policy.destroy
|
|
assignment = @course.assignments.create!
|
|
expect(assignment.post_policy).not_to be_post_manually
|
|
end
|
|
|
|
it "the assignment receives its own PostPolicy object" do
|
|
assignment = @course.assignments.create!
|
|
expect(assignment.post_policy).to be_present
|
|
end
|
|
end
|
|
|
|
context "when muting an assignment" do
|
|
it "sets the post policy of the assignment to manual" do
|
|
assignment = @course.assignments.create!
|
|
assignment.update!(muted: false)
|
|
assignment.mute!
|
|
|
|
expect(assignment.post_policy).to be_post_manually
|
|
end
|
|
end
|
|
|
|
context "when unmuting an assignment" do
|
|
it "does not change the post policy for anonymous assignments" do
|
|
assignment = @course.assignments.create!(anonymous_grading: true)
|
|
assignment.unmute!
|
|
|
|
expect(assignment.post_policy).to be_post_manually
|
|
end
|
|
|
|
it "sets the post policy of non-anonymous moderated assignments to automatic" do
|
|
assignment = @course.assignments.create!(final_grader: teacher, grader_count: 2, moderated_grading: true)
|
|
assignment.unmute!
|
|
|
|
expect(assignment.post_policy).not_to be_post_manually
|
|
end
|
|
|
|
it "sets the post policy of non-anonymous non-moderated assignments to automatic" do
|
|
assignment = @course.assignments.create!
|
|
assignment.unmute!
|
|
|
|
expect(assignment.post_policy).not_to be_post_manually
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#update_submittable" do
|
|
before(:each) do
|
|
Timecop.freeze(1.day.ago) do
|
|
assignment_quiz([], course: @course)
|
|
end
|
|
end
|
|
|
|
let(:assignment) { @assignment }
|
|
let(:quiz) { @quiz }
|
|
|
|
context "for an assignment with an associated quiz" do
|
|
it "updates the quiz when the assignment is updated normally" do
|
|
expect {
|
|
assignment.update!(title: "a new and even better title")
|
|
}.to change { quiz.reload.updated_at }
|
|
end
|
|
|
|
it "does not attempt to update the quiz when posting/hiding changes the assignment's muted status" do
|
|
expect {
|
|
assignment.hide_submissions(submission_ids: assignment.submissions.pluck(:id))
|
|
}.not_to change { quiz.reload.updated_at }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'sets root_account_id from Context' do
|
|
it 'sets root_account_id before_create' do
|
|
assignment = Assignment.create!(
|
|
course: @course,
|
|
name: 'some assignment',
|
|
)
|
|
expect(assignment.root_account_id).to eq @course.root_account_id
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "scope: expects_submissions" do
|
|
it 'includes assignments expecting online submissions' do
|
|
assignment_model(submission_types: "online_text_entry,online_url,online_upload", course: @course)
|
|
expect(Assignment.submittable).not_to be_empty
|
|
end
|
|
|
|
it 'excludes submissions for assignments expecting on_paper submissions' do
|
|
assignment_model(submission_types: "on_paper", course: @course)
|
|
expect(Assignment.submittable).to be_empty
|
|
end
|
|
|
|
it 'excludes submissions for assignments expecting external_tool submissions' do
|
|
assignment_model(submission_types: "external_tool", course: @course)
|
|
expect(Assignment.submittable).to be_empty
|
|
end
|
|
|
|
it 'excludes submissions for assignments expecting wiki_page submissions' do
|
|
assignment_model(submission_types: "wiki_page", course: @course)
|
|
expect(Assignment.submittable).to be_empty
|
|
end
|
|
|
|
it 'excludes submissions for assignments not expecting submissions' do
|
|
assignment_model(submission_types: "none", course: @course)
|
|
expect(Assignment.submittable).to be_empty
|
|
end
|
|
end
|
|
|
|
describe '#ordered_moderation_graders_with_slot_taken' do
|
|
let(:teacher1) { @course.enroll_teacher(User.create!, enrollment_state: :active).user }
|
|
let(:teacher2) { @course.enroll_teacher(User.create!, enrollment_state: :active).user }
|
|
let(:assignment) do
|
|
@course.assignments.create!(
|
|
moderated_grading: true,
|
|
grader_count: 3,
|
|
final_grader: @teacher
|
|
)
|
|
end
|
|
|
|
it 'returns moderation graders ordered by anonymous id' do
|
|
assignment.grade_student(@student, grader: teacher1, provisional: true, score: 0)
|
|
assignment.grade_student(@student, grader: teacher2, provisional: true, score: 5)
|
|
assignment.grade_student(@student, grader: @teacher, provisional: true, score: 10)
|
|
ordered_moderation_graders_with_slot_taken = assignment.moderation_graders.with_slot_taken.order(:anonymous_id)
|
|
expect(assignment.ordered_moderation_graders_with_slot_taken).to eq ordered_moderation_graders_with_slot_taken
|
|
end
|
|
end
|
|
|
|
describe '#moderation_grader_users_with_slot_taken' do
|
|
before(:once) do
|
|
@teacher = User.create!
|
|
@course.enroll_teacher(@teacher, enrollment_state: :active)
|
|
@student = User.create!
|
|
@course.enroll_student(@student, enrollment_state: :active)
|
|
@assignment = @course.assignments.create!(moderated_grading: true, grader_count: 3, final_grader: @teacher)
|
|
end
|
|
|
|
it 'includes users that have filled a grader slot' do
|
|
@assignment.create_moderation_grader(@teacher, occupy_slot: true)
|
|
expect(@assignment.moderation_grader_users_with_slot_taken).to include @teacher
|
|
end
|
|
|
|
it 'excludes users that have not filled a grader slot' do
|
|
@assignment.create_moderation_grader(@teacher, occupy_slot: false)
|
|
expect(@assignment.moderation_grader_users_with_slot_taken).not_to include @teacher
|
|
end
|
|
|
|
it 'excludes users that do not have a moderation grader record for the assignment' do
|
|
expect(@assignment.moderation_grader_users_with_slot_taken).not_to include @teacher
|
|
end
|
|
end
|
|
|
|
describe '#anonymous_grader_identities_by_user_id' do
|
|
before(:once) do
|
|
@teacher = User.create!
|
|
@course.enroll_teacher(@teacher, enrollment_state: :active)
|
|
@assignment = @course.assignments.create!(moderated_grading: true, grader_count: 2, final_grader: @teacher)
|
|
end
|
|
|
|
it 'includes users that have taken a grader slot' do
|
|
@assignment.create_moderation_grader(@teacher, occupy_slot: true)
|
|
expect(@assignment.anonymous_grader_identities_by_user_id).to have_key @teacher.id
|
|
end
|
|
|
|
it 'assigns grader names based on the ordered anonymous IDs' do
|
|
second_teacher = User.create!
|
|
@course.enroll_teacher(second_teacher, enrollment_state: :active)
|
|
@assignment.moderation_graders.create!(user: @teacher, anonymous_id: 'bbbbb', slot_taken: true)
|
|
@assignment.moderation_graders.create!(user: second_teacher, anonymous_id: 'aaaaa', slot_taken: true)
|
|
anonymous_name = @assignment.anonymous_grader_identities_by_user_id.dig(second_teacher.id, :name)
|
|
expect(anonymous_name).to eq 'Grader 1'
|
|
end
|
|
|
|
it 'excludes users that have not taken a grader slot' do
|
|
@assignment.create_moderation_grader(@teacher, occupy_slot: false)
|
|
expect(@assignment.anonymous_grader_identities_by_user_id).not_to have_key @teacher.id
|
|
end
|
|
|
|
it 'excludes users that do not have a moderation grader record for the assignment' do
|
|
expect(@assignment.anonymous_grader_identities_by_user_id).not_to have_key @teacher.id
|
|
end
|
|
end
|
|
|
|
describe '#anonymous_grader_identities_by_anonymous_id' do
|
|
before(:once) do
|
|
@teacher = User.create!
|
|
@course.enroll_teacher(@teacher, enrollment_state: :active)
|
|
@assignment = @course.assignments.create!(moderated_grading: true, grader_count: 2, final_grader: @teacher)
|
|
end
|
|
|
|
it 'includes users that have taken a grader slot' do
|
|
grader = @assignment.create_moderation_grader(@teacher, occupy_slot: true)
|
|
expect(@assignment.anonymous_grader_identities_by_anonymous_id).to have_key grader.anonymous_id
|
|
end
|
|
|
|
it 'assigns grader names based on the ordered anonymous IDs' do
|
|
second_teacher = User.create!
|
|
@course.enroll_teacher(second_teacher, enrollment_state: :active)
|
|
@assignment.moderation_graders.create!(user: @teacher, anonymous_id: 'bbbbb', slot_taken: true)
|
|
grader = @assignment.moderation_graders.create!(user: second_teacher, anonymous_id: 'aaaaa', slot_taken: true)
|
|
anonymous_name = @assignment.anonymous_grader_identities_by_anonymous_id.dig(grader.anonymous_id, :name)
|
|
expect(anonymous_name).to eq 'Grader 1'
|
|
end
|
|
|
|
it 'excludes users that have not taken a grader slot' do
|
|
grader = @assignment.create_moderation_grader(@teacher, occupy_slot: false)
|
|
expect(@assignment.anonymous_grader_identities_by_anonymous_id).not_to have_key grader.anonymous_id
|
|
end
|
|
|
|
it 'excludes users that do not have a moderation grader record for the assignment' do
|
|
expect(@assignment.anonymous_grader_identities_by_anonymous_id).not_to have_key @teacher.id
|
|
end
|
|
end
|
|
|
|
describe '#permits_moderation?' do
|
|
before(:once) do
|
|
@assignment = @course.assignments.create!(
|
|
moderated_grading: true,
|
|
grader_count: 2,
|
|
final_grader: @teacher
|
|
)
|
|
end
|
|
|
|
it 'returns false if the user is not the final grader and not an admin' do
|
|
assistant = User.create!
|
|
@course.enroll_ta(assistant, enrollment_state: 'active')
|
|
expect(@assignment.permits_moderation?(assistant)).to be false
|
|
end
|
|
|
|
it 'returns false if user is nil' do
|
|
expect(@assignment.permits_moderation?(nil)).to be false
|
|
end
|
|
|
|
it 'returns true if the user is the final grader' do
|
|
expect(@assignment.permits_moderation?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns true if the user is an admin with "select final grader for moderation" privileges' do
|
|
expect(@assignment.permits_moderation?(account_admin_user)).to be true
|
|
end
|
|
|
|
it 'returns false if the user is an admin without "select final grader for moderation" privileges' do
|
|
@course.account.role_overrides.create!(role: admin_role, enabled: false, permission: :select_final_grade)
|
|
expect(@assignment.permits_moderation?(account_admin_user)).to be false
|
|
end
|
|
end
|
|
|
|
describe '#can_view_other_grader_identities?' do
|
|
let_once(:admin) do
|
|
admin = account_admin_user
|
|
@course.enroll_teacher(admin, enrollment_state: 'active')
|
|
admin
|
|
end
|
|
let_once(:ta) do
|
|
ta = User.create!
|
|
@course.enroll_ta(ta, enrollment_state: 'active')
|
|
ta
|
|
end
|
|
let_once(:assignment) { @course.assignments.create!(final_grader: @teacher, grader_count: 2, moderated_grading: true) }
|
|
|
|
shared_examples "grader anonymity does not apply" do
|
|
it 'returns true when the user has permission to manage grades' do
|
|
@course.root_account.role_overrides.create!(permission: 'manage_grades', enabled: true, role: teacher_role)
|
|
@course.root_account.role_overrides.create!(permission: 'view_all_grades', enabled: false, role: teacher_role)
|
|
expect(assignment.can_view_other_grader_identities?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns true when the user has permission to view all grades' do
|
|
@course.root_account.role_overrides.create!(permission: 'manage_grades', enabled: false, role: teacher_role)
|
|
@course.root_account.role_overrides.create!(permission: 'view_all_grades', enabled: true, role: teacher_role)
|
|
expect(assignment.can_view_other_grader_identities?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns false when the user does not have sufficient privileges' do
|
|
@course.root_account.role_overrides.create!(permission: 'manage_grades', enabled: false, role: teacher_role)
|
|
@course.root_account.role_overrides.create!(permission: 'view_all_grades', enabled: false, role: teacher_role)
|
|
expect(assignment.can_view_other_grader_identities?(@teacher)).to be false
|
|
end
|
|
end
|
|
|
|
context 'when the assignment is anonymously graded' do
|
|
before(:once) do
|
|
assignment.update!(anonymous_grading: true)
|
|
end
|
|
|
|
context 'when the assignment is not moderated' do
|
|
before :once do
|
|
assignment.update!(moderated_grading: false)
|
|
end
|
|
|
|
it_behaves_like "grader anonymity does not apply"
|
|
end
|
|
|
|
context 'when the assignment is not anonymously graded' do
|
|
before :once do
|
|
assignment.update!(anonymous_grading: false, grader_names_visible_to_final_grader: true)
|
|
end
|
|
|
|
it_behaves_like "grader anonymity does not apply"
|
|
end
|
|
|
|
context 'when grader comments are visible to other graders' do
|
|
before :once do
|
|
assignment.update!(grader_comments_visible_to_graders: true)
|
|
end
|
|
|
|
context 'when graders are not anonymous' do
|
|
before :once do
|
|
assignment.update!(grader_names_visible_to_final_grader: true, graders_anonymous_to_graders: false)
|
|
end
|
|
|
|
it_behaves_like "grader anonymity does not apply"
|
|
end
|
|
|
|
context 'when graders are anonymous to each other and the final grader' do
|
|
before :once do
|
|
assignment.update!(grader_names_visible_to_final_grader: false, graders_anonymous_to_graders: true)
|
|
end
|
|
|
|
it 'returns false when the user is not the final grader and not an admin' do
|
|
expect(assignment.can_view_other_grader_identities?(ta)).to be false
|
|
end
|
|
|
|
it 'returns false when the user is the final grader and not an admin' do
|
|
expect(assignment.can_view_other_grader_identities?(@teacher)).to be false
|
|
end
|
|
|
|
it 'returns true when the user is an admin and not the final grader' do
|
|
expect(assignment.can_view_other_grader_identities?(admin)).to be true
|
|
end
|
|
|
|
it 'returns false when the user is an admin and also the final grader' do
|
|
assignment.update!(final_grader_id: admin.id)
|
|
expect(assignment.can_view_other_grader_identities?(admin)).to be false
|
|
end
|
|
end
|
|
|
|
context 'when graders are anonymous only to each other' do
|
|
before :once do
|
|
assignment.update!(grader_names_visible_to_final_grader: true, graders_anonymous_to_graders: true)
|
|
end
|
|
|
|
it 'returns false when the user is not the final grader and not an admin' do
|
|
expect(assignment.can_view_other_grader_identities?(ta)).to be false
|
|
end
|
|
|
|
it 'returns true when the user is the final grader and not an admin' do
|
|
expect(assignment.can_view_other_grader_identities?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns true when the user is an admin and not the final grader' do
|
|
expect(assignment.can_view_other_grader_identities?(admin)).to be true
|
|
end
|
|
|
|
it 'returns true when the user is an admin and also the final grader' do
|
|
assignment.update!(final_grader_id: admin.id)
|
|
expect(assignment.can_view_other_grader_identities?(admin)).to be true
|
|
end
|
|
|
|
context 'when the assignment is published' do
|
|
before(:once) { assignment.update!(grades_published_at: Time.zone.now) }
|
|
|
|
it 'returns true when the user is not the final grader and not an admin' do
|
|
expect(assignment.can_view_other_grader_identities?(ta)).to be true
|
|
end
|
|
|
|
it 'returns true when the user is the final grader and not an admin' do
|
|
expect(assignment.can_view_other_grader_identities?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns true when the user is an admin and not the final grader' do
|
|
expect(assignment.can_view_other_grader_identities?(admin)).to be true
|
|
end
|
|
|
|
it 'returns true when the user is an admin and also the final grader' do
|
|
assignment.update!(final_grader_id: admin.id)
|
|
expect(assignment.can_view_other_grader_identities?(admin)).to be true
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when graders are anonymous only to the final grader' do
|
|
before :once do
|
|
assignment.update!(grader_names_visible_to_final_grader: false, graders_anonymous_to_graders: false)
|
|
end
|
|
|
|
it 'returns true when the user is not the final grader and not an admin' do
|
|
expect(assignment.can_view_other_grader_identities?(ta)).to be true
|
|
end
|
|
|
|
it 'returns false when the user is the final grader and not an admin' do
|
|
expect(assignment.can_view_other_grader_identities?(@teacher)).to be false
|
|
end
|
|
|
|
it 'returns true when the user is an admin and not the final grader' do
|
|
expect(assignment.can_view_other_grader_identities?(admin)).to be true
|
|
end
|
|
|
|
it 'returns false when the user is an admin and also the final grader' do
|
|
assignment.update!(final_grader_id: admin.id)
|
|
expect(assignment.can_view_other_grader_identities?(admin)).to be false
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when grader comments are hidden to other graders' do
|
|
# When comments are hidden, grader names are also not displayed (effectively anonymous).
|
|
# This does not apply when the final grader explicitly can view grader names.
|
|
|
|
before :once do
|
|
assignment.update!(grader_comments_visible_to_graders: false)
|
|
end
|
|
|
|
context 'when graders are not anonymous' do
|
|
before :once do
|
|
assignment.update!(grader_names_visible_to_final_grader: true, graders_anonymous_to_graders: false)
|
|
end
|
|
|
|
it 'returns false when the user is not the final grader and not an admin' do
|
|
# grader comments must be visible for graders to not be anonymous to other graders
|
|
expect(assignment.can_view_other_grader_identities?(ta)).to be false
|
|
end
|
|
|
|
it 'returns true when the user is the final grader and not an admin' do
|
|
expect(assignment.can_view_other_grader_identities?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns true when the user is an admin and not the final grader' do
|
|
expect(assignment.can_view_other_grader_identities?(admin)).to be true
|
|
end
|
|
|
|
it 'returns true when the user is an admin and also the final grader' do
|
|
assignment.update!(final_grader_id: admin.id)
|
|
expect(assignment.can_view_other_grader_identities?(admin)).to be true
|
|
end
|
|
end
|
|
|
|
context 'when graders are anonymous to each other and the final grader' do
|
|
before :once do
|
|
assignment.update!(grader_names_visible_to_final_grader: false, graders_anonymous_to_graders: true)
|
|
end
|
|
|
|
it 'returns false when the user is not the final grader and not an admin' do
|
|
expect(assignment.can_view_other_grader_identities?(ta)).to be false
|
|
end
|
|
|
|
it 'returns false when the user is the final grader and not an admin' do
|
|
expect(assignment.can_view_other_grader_identities?(@teacher)).to be false
|
|
end
|
|
|
|
it 'returns true when the user is an admin and not the final grader' do
|
|
expect(assignment.can_view_other_grader_identities?(admin)).to be true
|
|
end
|
|
|
|
it 'returns false when the user is an admin and also the final grader' do
|
|
assignment.update!(final_grader_id: admin.id)
|
|
expect(assignment.can_view_other_grader_identities?(admin)).to be false
|
|
end
|
|
end
|
|
|
|
context 'when graders are anonymous only to each other' do
|
|
before :once do
|
|
assignment.update!(grader_names_visible_to_final_grader: true, graders_anonymous_to_graders: true)
|
|
end
|
|
|
|
it 'returns false when the user is not the final grader and not an admin' do
|
|
expect(assignment.can_view_other_grader_identities?(ta)).to be false
|
|
end
|
|
|
|
it 'returns true when the user is the final grader and not an admin' do
|
|
expect(assignment.can_view_other_grader_identities?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns true when the user is an admin and not the final grader' do
|
|
expect(assignment.can_view_other_grader_identities?(admin)).to be true
|
|
end
|
|
|
|
it 'returns true when the user is an admin and also the final grader' do
|
|
assignment.update!(final_grader_id: admin.id)
|
|
expect(assignment.can_view_other_grader_identities?(admin)).to be true
|
|
end
|
|
end
|
|
|
|
context 'when graders are anonymous only to the final grader' do
|
|
before :once do
|
|
assignment.update!(grader_names_visible_to_final_grader: false, graders_anonymous_to_graders: false)
|
|
end
|
|
|
|
it 'returns false when the user is not the final grader and not an admin' do
|
|
expect(assignment.can_view_other_grader_identities?(ta)).to be false
|
|
end
|
|
|
|
it 'returns false when the user is the final grader and not an admin' do
|
|
expect(assignment.can_view_other_grader_identities?(@teacher)).to be false
|
|
end
|
|
|
|
it 'returns true when the user is an admin and not the final grader' do
|
|
expect(assignment.can_view_other_grader_identities?(admin)).to be true
|
|
end
|
|
|
|
it 'returns false when the user is an admin and also the final grader' do
|
|
assignment.update!(final_grader_id: admin.id)
|
|
expect(assignment.can_view_other_grader_identities?(admin)).to be false
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#can_view_other_grader_comments?' do
|
|
let_once(:admin) do
|
|
admin = account_admin_user
|
|
@course.enroll_teacher(admin, enrollment_state: 'active')
|
|
admin
|
|
end
|
|
let_once(:ta) do
|
|
ta = User.create!
|
|
@course.enroll_ta(ta, enrollment_state: 'active')
|
|
ta
|
|
end
|
|
let_once(:assignment) { @course.assignments.create!(final_grader: @teacher, grader_count: 2, moderated_grading: true, anonymous_grading: true) }
|
|
|
|
shared_examples "grader comment hiding does not apply" do
|
|
it 'returns true when the user has permission to manage grades' do
|
|
@course.root_account.role_overrides.create!(permission: 'manage_grades', enabled: true, role: teacher_role)
|
|
@course.root_account.role_overrides.create!(permission: 'view_all_grades', enabled: false, role: teacher_role)
|
|
expect(assignment.can_view_other_grader_comments?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns true when the user has permission to view all grades' do
|
|
@course.root_account.role_overrides.create!(permission: 'manage_grades', enabled: false, role: teacher_role)
|
|
@course.root_account.role_overrides.create!(permission: 'view_all_grades', enabled: true, role: teacher_role)
|
|
expect(assignment.can_view_other_grader_comments?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns false when the user does not have sufficient privileges' do
|
|
@course.root_account.role_overrides.create!(permission: 'manage_grades', enabled: false, role: teacher_role)
|
|
@course.root_account.role_overrides.create!(permission: 'view_all_grades', enabled: false, role: teacher_role)
|
|
expect(assignment.can_view_other_grader_comments?(@teacher)).to be false
|
|
end
|
|
end
|
|
|
|
context 'when the assignment is not moderated' do
|
|
before :once do
|
|
assignment.update!(moderated_grading: false)
|
|
end
|
|
|
|
it_behaves_like "grader comment hiding does not apply"
|
|
end
|
|
|
|
context 'when grader comments are visible to other graders' do
|
|
before :once do
|
|
assignment.update!(
|
|
grader_comments_visible_to_graders: true,
|
|
grader_names_visible_to_final_grader: true
|
|
)
|
|
end
|
|
|
|
it_behaves_like "grader comment hiding does not apply"
|
|
|
|
it 'returns true when the user is not the final grader and not an admin' do
|
|
expect(assignment.can_view_other_grader_comments?(ta)).to be true
|
|
end
|
|
|
|
it 'returns true when the user is the final grader' do
|
|
expect(assignment.can_view_other_grader_comments?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns true when the user is an admin' do
|
|
expect(assignment.can_view_other_grader_comments?(admin)).to be true
|
|
end
|
|
end
|
|
|
|
context 'when grader comments are hidden to other graders' do
|
|
before :once do
|
|
assignment.update!(
|
|
grader_comments_visible_to_graders: false,
|
|
grader_names_visible_to_final_grader: true
|
|
)
|
|
end
|
|
|
|
it 'returns false when the user is not the final grader and not an admin' do
|
|
expect(assignment.can_view_other_grader_comments?(ta)).to be false
|
|
end
|
|
|
|
it 'returns true when the user is the final grader' do
|
|
# The final grader must always be able to see grader comments.
|
|
expect(assignment.can_view_other_grader_comments?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns true when the user is an admin' do
|
|
expect(assignment.can_view_other_grader_comments?(admin)).to be true
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#anonymize_students?' do
|
|
before(:once) do
|
|
@assignment = @course.assignments.create!
|
|
end
|
|
|
|
it 'returns false when the assignment is not graded anonymously' do
|
|
expect(@assignment).not_to be_anonymize_students
|
|
end
|
|
|
|
context 'when the assignment is anonymously graded' do
|
|
before(:once) do
|
|
@assignment.update!(anonymous_grading: true)
|
|
end
|
|
|
|
context "when the assignment is moderated" do
|
|
before(:each) do
|
|
@assignment.moderated_grading = true
|
|
end
|
|
|
|
it 'returns true when the assignment is moderated and grades are unpublished' do
|
|
expect(@assignment).to be_anonymize_students
|
|
end
|
|
|
|
it 'returns false when the assignment is moderated and grades are published' do
|
|
@assignment.grades_published_at = Time.zone.now
|
|
expect(@assignment).not_to be_anonymize_students
|
|
end
|
|
end
|
|
|
|
context "when the assignment is unmoderated" do
|
|
let(:course) { @assignment.course }
|
|
let(:active_student) { User.create! }
|
|
let(:student2) { User.create! }
|
|
let(:student3) { User.create! }
|
|
|
|
before(:each) do
|
|
course.enroll_student(active_student, workflow_state: :active)
|
|
course.enroll_student(student2, workflow_state: :active)
|
|
course.enroll_student(student3, workflow_state: :active)
|
|
end
|
|
|
|
it "returns true when at least one active student has an unposted submission" do
|
|
expect(@assignment).to be_anonymize_students
|
|
end
|
|
|
|
it "returns false when all active submissions are posted" do
|
|
student2.enrollments.find_by(course: course).conclude
|
|
student3.enrollments.find_by(course: course).deactivate
|
|
|
|
@assignment.post_submissions
|
|
expect(@assignment).not_to be_anonymize_students
|
|
end
|
|
|
|
it "returns false when all submissions are posted" do
|
|
@assignment.post_submissions
|
|
expect(@assignment).not_to be_anonymize_students
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#can_view_student_names?' do
|
|
let_once(:admin) do
|
|
admin = account_admin_user
|
|
@course.enroll_teacher(admin, enrollment_state: 'active')
|
|
admin
|
|
end
|
|
let_once(:ta) do
|
|
ta = User.create!
|
|
@course.enroll_ta(ta, enrollment_state: 'active')
|
|
ta
|
|
end
|
|
let_once(:assignment) { @course.assignments.create!(final_grader: @teacher, anonymous_grading: true) }
|
|
|
|
shared_examples "student anonymity does not apply" do
|
|
it 'returns true when the user has permission to manage grades' do
|
|
@course.root_account.role_overrides.create!(permission: 'manage_grades', enabled: true, role: teacher_role)
|
|
@course.root_account.role_overrides.create!(permission: 'view_all_grades', enabled: false, role: teacher_role)
|
|
expect(assignment.can_view_student_names?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns true when the user has permission to view all grades' do
|
|
@course.root_account.role_overrides.create!(permission: 'manage_grades', enabled: false, role: teacher_role)
|
|
@course.root_account.role_overrides.create!(permission: 'view_all_grades', enabled: true, role: teacher_role)
|
|
expect(assignment.can_view_student_names?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns false when the user does not have sufficient privileges' do
|
|
@course.root_account.role_overrides.create!(permission: 'manage_grades', enabled: false, role: teacher_role)
|
|
@course.root_account.role_overrides.create!(permission: 'view_all_grades', enabled: false, role: teacher_role)
|
|
expect(assignment.can_view_student_names?(@teacher)).to be false
|
|
end
|
|
end
|
|
|
|
context 'when the assignment is not anonymously graded' do
|
|
before :once do
|
|
assignment.update!(anonymous_grading: false)
|
|
end
|
|
|
|
it_behaves_like "student anonymity does not apply"
|
|
end
|
|
|
|
context 'when the assignment is anonymously graded' do
|
|
context "when the assignment is unmoderated" do
|
|
let(:course) { assignment.course }
|
|
let(:active_student) { User.create! }
|
|
let(:student2) { User.create! }
|
|
let(:student3) { User.create! }
|
|
|
|
before(:each) do
|
|
course.enroll_student(active_student, workflow_state: :active)
|
|
course.enroll_student(student2, workflow_state: :active)
|
|
course.enroll_student(student3, workflow_state: :active)
|
|
end
|
|
|
|
it "returns false for a teacher when at least one active student has an unposted submission" do
|
|
expect(assignment.can_view_student_names?(@teacher)).to be false
|
|
end
|
|
|
|
it "returns false for an admin when at least one active student has an unposted submission" do
|
|
expect(assignment.can_view_student_names?(admin)).to be false
|
|
end
|
|
|
|
it "returns true when all active submissions are posted" do
|
|
student2.enrollments.find_by(course: course).conclude
|
|
student3.enrollments.find_by(course: course).deactivate
|
|
|
|
assignment.post_submissions
|
|
expect(assignment.can_view_student_names?(admin)).to be true
|
|
end
|
|
|
|
it "returns true when all submissions are posted" do
|
|
assignment.post_submissions
|
|
expect(assignment.can_view_student_names?(admin)).to be true
|
|
end
|
|
end
|
|
|
|
context 'when the assignment is moderated' do
|
|
before(:once) do
|
|
assignment.moderated_grading = true
|
|
end
|
|
|
|
it 'returns false when the user is not an admin' do
|
|
expect(assignment.can_view_student_names?(@teacher)).to be false
|
|
end
|
|
|
|
it 'returns true when the user is an admin and grades are published' do
|
|
assignment.grades_published_at = Time.zone.now
|
|
expect(assignment.can_view_student_names?(admin)).to be true
|
|
end
|
|
|
|
it 'returns false when the user is an admin and grades are unpublished' do
|
|
expect(assignment.can_view_student_names?(admin)).to be false
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#tool_settings_resource_codes' do
|
|
let(:expected_hash) do
|
|
{
|
|
product_code: product_family.product_code,
|
|
vendor_code: product_family.vendor_code,
|
|
resource_type_code: resource_handler.resource_type_code
|
|
}
|
|
end
|
|
|
|
let(:assignment) { Assignment.create!(name: 'assignment with tool settings', context: course) }
|
|
|
|
before do
|
|
allow_any_instance_of(Lti::AssignmentSubscriptionsHelper).to receive(:create_subscription) { SecureRandom.uuid }
|
|
allow_any_instance_of(Lti::AssignmentSubscriptionsHelper).to receive(:destroy_subscription) { {} }
|
|
end
|
|
|
|
it 'returns a hash of three identifying lti codes' do
|
|
assignment.tool_settings_tool = message_handler
|
|
assignment.save!
|
|
expect(assignment.tool_settings_resource_codes).to eq expected_hash
|
|
end
|
|
end
|
|
|
|
describe '#tool_settings_tool_name' do
|
|
let(:assignment) { Assignment.create!(name: 'assignment with tool settings', context: course) }
|
|
|
|
before do
|
|
allow_any_instance_of(Lti::AssignmentSubscriptionsHelper).to receive(:create_subscription) { SecureRandom.uuid }
|
|
allow_any_instance_of(Lti::AssignmentSubscriptionsHelper).to receive(:destroy_subscription) { {} }
|
|
end
|
|
|
|
it 'returns the name of the tool proxy' do
|
|
expected_name = 'test name'
|
|
message_handler.tool_proxy.update!(name: expected_name)
|
|
setup_assignment_with_homework
|
|
course.assignments << @assignment
|
|
@assignment.tool_settings_tool = message_handler
|
|
@assignment.save!
|
|
expect(@assignment.tool_settings_tool_name).to eq expected_name
|
|
end
|
|
|
|
it 'returns the name of the context external tool' do
|
|
expected_name = 'test name'
|
|
setup_assignment_with_homework
|
|
tool = @course.context_external_tools.create!(name: expected_name, url: "http://www.google.com", consumer_key: '12345', shared_secret: 'secret')
|
|
@assignment.tool_settings_tool = tool
|
|
@assignment.save
|
|
expect(@assignment.tool_settings_tool_name).to eq(expected_name)
|
|
end
|
|
end
|
|
|
|
describe '#tool_settings_tool=' do
|
|
let(:stub_response){ double(code: 200, body: {}.to_json, parsed_response: {'Id' => 'test-id'}, ok?: true) }
|
|
let(:subscription_helper){ class_double(Lti::AssignmentSubscriptionsHelper).as_stubbed_const }
|
|
let(:subscription_helper_instance){ double(destroy_subscription: true, create_subscription: true) }
|
|
|
|
before(:each) do
|
|
allow(subscription_helper).to receive_messages(new: subscription_helper_instance)
|
|
end
|
|
|
|
it "should allow ContextExternalTools through polymorphic association" do
|
|
setup_assignment_with_homework
|
|
tool = @course.context_external_tools.create!(name: "a", url: "http://www.google.com", consumer_key: '12345', shared_secret: 'secret')
|
|
@assignment.tool_settings_tool = tool
|
|
@assignment.save
|
|
expect(@assignment.tool_settings_tool).to eq(tool)
|
|
end
|
|
|
|
it 'destroys subscriptions when they exist' do
|
|
setup_assignment_with_homework
|
|
expect(subscription_helper_instance).to receive(:destroy_subscription)
|
|
course.assignments << @assignment
|
|
@assignment.tool_settings_tool = message_handler
|
|
@assignment.save!
|
|
@assignment.tool_settings_tool = nil
|
|
@assignment.save!
|
|
end
|
|
|
|
it "destroys tool unless tool is 'ContextExternalTool'" do
|
|
setup_assignment_with_homework
|
|
expect(subscription_helper_instance).not_to receive(:destroy_subscription)
|
|
tool = @course.context_external_tools.create!(name: "a", url: "http://www.google.com", consumer_key: '12345', shared_secret: 'secret')
|
|
@assignment.tool_settings_tool = tool
|
|
@assignment.save!
|
|
@assignment.tool_settings_tool = nil
|
|
@assignment.save!
|
|
end
|
|
|
|
context 'when the tool proxy is account-level' do
|
|
it 'sets the lookup context_type to Account when the tool proxy is account-level' do
|
|
setup_assignment_with_homework
|
|
course.assignments << @assignment
|
|
@assignment.tool_settings_tool = message_handler
|
|
@assignment.save!
|
|
lookup = @assignment.assignment_configuration_tool_lookups.last
|
|
expect(lookup.context_type).to eq('Account')
|
|
end
|
|
end
|
|
|
|
context 'when the tool proxy is course-level' do
|
|
let(:tool_proxy_context) { course }
|
|
|
|
it 'sets the lookup context_type to Course when the tool proxy' do
|
|
setup_assignment_with_homework
|
|
course.assignments << @assignment
|
|
@assignment.tool_settings_tool = message_handler
|
|
@assignment.save!
|
|
lookup = @assignment.assignment_configuration_tool_lookups.last
|
|
expect(lookup.context_type).to eq('Course')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#duplicate" do
|
|
it "duplicates the assignment" do
|
|
assignment = wiki_page_assignment_model({ :title => "Wiki Assignment" })
|
|
rubric = @course.rubrics.create! { |r| r.user = @teacher }
|
|
rubric_association_params = HashWithIndifferentAccess.new({
|
|
hide_score_total: "0",
|
|
purpose: "grading",
|
|
skip_updating_points_possible: false,
|
|
update_if_existing: true,
|
|
use_for_grading: "1",
|
|
association_object: assignment
|
|
})
|
|
|
|
rubric_assoc = RubricAssociation.generate(@teacher, rubric, @course, rubric_association_params)
|
|
assignment.rubric_association = rubric_assoc
|
|
assignment.attachments.push(Attachment.new)
|
|
assignment.submissions.push(Submission.new)
|
|
assignment.ignores.push(Ignore.new)
|
|
assignment.turnitin_asset_string
|
|
new_assignment = assignment.duplicate
|
|
expect(new_assignment.id).to be_nil
|
|
expect(new_assignment.new_record?).to be true
|
|
expect(new_assignment.attachments.length).to be(0)
|
|
expect(new_assignment.submissions.length).to be(0)
|
|
expect(new_assignment.ignores.length).to be(0)
|
|
expect(new_assignment.rubric_association).not_to be_nil
|
|
expect(new_assignment.title).to eq "Wiki Assignment Copy"
|
|
expect(new_assignment.wiki_page.title).to eq "Wiki Assignment Copy"
|
|
expect(new_assignment.duplicate_of).to eq assignment
|
|
expect(new_assignment.workflow_state).to eq "unpublished"
|
|
new_assignment.save!
|
|
new_assignment2 = assignment.duplicate
|
|
expect(new_assignment2.title).to eq "Wiki Assignment Copy 2"
|
|
new_assignment2.save!
|
|
expect(assignment.duplicates).to match_array [new_assignment, new_assignment2]
|
|
# Go back to the first new assignment to test something just ending in
|
|
# "Copy"
|
|
new_assignment3 = new_assignment.duplicate
|
|
expect(new_assignment3.title).to eq "Wiki Assignment Copy 3"
|
|
end
|
|
|
|
it "does not duplicate grades_published_at" do
|
|
assignment = @course.assignments.create!(title: "whee", points_possible: 10)
|
|
assignment.grades_published_at = Time.zone.now
|
|
assignment.save!
|
|
new_assignment = assignment.reload.duplicate
|
|
expect(new_assignment.grades_published_at).to be_nil
|
|
end
|
|
|
|
it "should not explode duplicating a mismatched rubric association" do
|
|
assmt = @course.assignments.create!(:title => "assmt", :points_possible => 3)
|
|
rubric = @course.rubrics.new(:title => "rubric")
|
|
rubric.update_with_association(@teacher, {
|
|
criteria: {"0" => {description: "correctness", points: 15, ratings: {"0" => {points: 15, description: "a description"}}, }, },
|
|
}, @course, {
|
|
association_object: assmt, update_if_existing: true,
|
|
use_for_grading: "1", purpose: "grading", skip_updating_points_possible: true
|
|
})
|
|
new_assmt = assmt.reload.duplicate
|
|
new_assmt.save!
|
|
expect(new_assmt.points_possible).to eq 3
|
|
end
|
|
|
|
context "with an assignment that can't be duplicated" do
|
|
let(:assignment) { @course.assignments.create!(assignment_valid_attributes) }
|
|
|
|
before { allow(assignment).to receive(:can_duplicate?).and_return(false) }
|
|
|
|
it "raises an exception" do
|
|
expect { assignment.duplicate }.to raise_error(RuntimeError)
|
|
end
|
|
end
|
|
|
|
context "with an assignment that uses an external tool" do
|
|
let_once(:assignment) do
|
|
@course.assignments.create!(
|
|
submission_types: 'external_tool',
|
|
external_tool_tag_attributes: { url: 'http://example.com/launch' },
|
|
**assignment_valid_attributes
|
|
)
|
|
end
|
|
|
|
before { allow(assignment).to receive(:can_duplicate?).and_return(true) }
|
|
|
|
it "duplicates the assignment's external_tool_tag" do
|
|
new_assignment = assignment.duplicate
|
|
new_assignment.save!
|
|
expect(new_assignment.external_tool_tag).to be_present
|
|
expect(new_assignment.external_tool_tag.content).to eq(assignment.external_tool_tag.content)
|
|
end
|
|
|
|
it "sets the assignment's state to 'duplicating'" do
|
|
expect(assignment.duplicate.workflow_state).to eq('duplicating')
|
|
end
|
|
|
|
it "sets duplication_started_at to the current time" do
|
|
expect(assignment.duplicate.duplication_started_at).to be_within(5).of(Time.zone.now)
|
|
end
|
|
end
|
|
|
|
context 'with a plagiarism detection tool' do
|
|
subject { assignment.duplicate.assignment_configuration_tool_lookups.first }
|
|
|
|
let(:assignment) { assignment_model }
|
|
let(:lookup) { assignment.assignment_configuration_tool_lookups.first }
|
|
let(:subscription_helper) { double(create_subscription: SecureRandom.uuid) }
|
|
|
|
before do
|
|
allow(Lti::AssignmentSubscriptionsHelper).to receive(:new).and_return(subscription_helper)
|
|
assignment.assignment_configuration_tool_lookups.create!(
|
|
context_type: 'Account',
|
|
tool_vendor_code: product_family.vendor_code,
|
|
tool_product_code: product_family.product_code,
|
|
tool_resource_type_code: resource_handler.resource_type_code,
|
|
tool_type: 'Lti::MessageHandler'
|
|
)
|
|
end
|
|
|
|
it 'uses the correct product code' do
|
|
expect(subject.tool_product_code).to eq product_family.product_code
|
|
end
|
|
|
|
it 'uses the correct vendor code' do
|
|
expect(subject.tool_vendor_code).to eq product_family.vendor_code
|
|
end
|
|
|
|
it 'uses the correct resource type code' do
|
|
expect(subject.tool_resource_type_code).to eq resource_handler.resource_type_code
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#can_duplicate?" do
|
|
subject { assignment.can_duplicate? }
|
|
|
|
let(:assignment) { @course.assignments.create!(assignment_valid_attributes) }
|
|
|
|
context "with a regular assignment" do
|
|
it { is_expected.to be true }
|
|
end
|
|
|
|
context "with a quiz" do
|
|
before { allow(assignment).to receive(:quiz?).and_return(true) }
|
|
|
|
it { is_expected.to be false }
|
|
end
|
|
|
|
context "with an assignment that uses an external tool" do
|
|
let_once(:assignment) do
|
|
@course.assignments.create!(
|
|
submission_types: 'external_tool',
|
|
external_tool_tag_attributes: { url: 'http://example.com/launch' },
|
|
**assignment_valid_attributes
|
|
)
|
|
end
|
|
|
|
it { is_expected.to be false }
|
|
|
|
context "quiz_lti" do
|
|
before { allow(assignment).to receive(:quiz_lti?).and_return(true) }
|
|
|
|
it { is_expected.to be true }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "scope: duplicating_for_too_long" do
|
|
subject { described_class.duplicating_for_too_long }
|
|
|
|
let_once(:unpublished_assignment) do
|
|
@course.assignments.create!(workflow_state: 'unpublished', **assignment_valid_attributes)
|
|
end
|
|
let_once(:new_duplicating_assignment) do
|
|
@course.assignments.create!(
|
|
workflow_state: 'duplicating',
|
|
duplication_started_at: 5.seconds.ago,
|
|
**assignment_valid_attributes
|
|
)
|
|
end
|
|
let_once(:old_duplicating_assignment) do
|
|
@course.assignments.create!(
|
|
workflow_state: 'duplicating',
|
|
duplication_started_at: 20.minutes.ago,
|
|
**assignment_valid_attributes
|
|
)
|
|
end
|
|
|
|
it { is_expected.to eq([old_duplicating_assignment]) }
|
|
end
|
|
|
|
describe ".clean_up_duplicating_assignments" do
|
|
before { allow(described_class).to receive(:duplicating_for_too_long).and_return(double()) }
|
|
|
|
it "marks all assignments that have been duplicating for too long as failed_to_duplicate" do
|
|
now = double('now')
|
|
expect(Time.zone).to receive(:now).and_return(now)
|
|
expect(described_class.duplicating_for_too_long).to receive(:update_all).with(
|
|
duplication_started_at: nil,
|
|
workflow_state: 'failed_to_duplicate',
|
|
updated_at: now
|
|
)
|
|
described_class.clean_up_duplicating_assignments
|
|
end
|
|
end
|
|
|
|
describe "scope: importing_for_too_long" do
|
|
subject { described_class.importing_for_too_long }
|
|
|
|
let_once(:unpublished_assignment) do
|
|
@course.assignments.create!(workflow_state: 'unpublished', **assignment_valid_attributes)
|
|
end
|
|
let_once(:new_importing_assignment) do
|
|
@course.assignments.create!(
|
|
workflow_state: 'importing',
|
|
importing_started_at: 5.seconds.ago,
|
|
**assignment_valid_attributes
|
|
)
|
|
end
|
|
let_once(:old_importing_assignment) do
|
|
@course.assignments.create!(
|
|
workflow_state: 'importing',
|
|
importing_started_at: 20.minutes.ago,
|
|
**assignment_valid_attributes
|
|
)
|
|
end
|
|
|
|
it { is_expected.to eq([old_importing_assignment]) }
|
|
end
|
|
|
|
describe ".cleanup_importing_assignments" do
|
|
before { allow(described_class).to receive(:importing_for_too_long).and_return(double()) }
|
|
|
|
it "marks all assignments that have been importing for too long as failed_to_import" do
|
|
now = double('now')
|
|
expect(Time.zone).to receive(:now).and_return(now)
|
|
expect(described_class.importing_for_too_long).to receive(:update_all).with(
|
|
importing_started_at: nil,
|
|
workflow_state: 'failed_to_import',
|
|
updated_at: now
|
|
)
|
|
described_class.clean_up_importing_assignments
|
|
end
|
|
end
|
|
|
|
describe "scope: migrating_for_too_long" do
|
|
subject { described_class.migrating_for_too_long }
|
|
|
|
let_once(:unpublished_assignment) do
|
|
@course.assignments.create!(workflow_state: 'unpublished', **assignment_valid_attributes)
|
|
end
|
|
let_once(:new_migrating_assignment) do
|
|
@course.assignments.create!(
|
|
workflow_state: 'migrating',
|
|
duplication_started_at: 5.seconds.ago,
|
|
**assignment_valid_attributes
|
|
)
|
|
end
|
|
let_once(:old_migrating_assignment) do
|
|
@course.assignments.create!(
|
|
workflow_state: 'migrating',
|
|
duplication_started_at: 20.minutes.ago,
|
|
**assignment_valid_attributes
|
|
)
|
|
end
|
|
|
|
it { is_expected.to eq([old_migrating_assignment]) }
|
|
|
|
describe ".clean_up_migrating_assignments" do
|
|
it "marks all assignments that have been migrating for too long as failed_to_migrate" do
|
|
expect(old_migrating_assignment.duplication_started_at).not_to be_nil
|
|
expect(old_migrating_assignment.workflow_state).to eq 'migrating'
|
|
described_class.clean_up_migrating_assignments
|
|
expect(
|
|
old_migrating_assignment.reload.duplication_started_at
|
|
).to be_nil
|
|
expect(old_migrating_assignment.workflow_state).to eq 'failed_to_migrate'
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#representatives" do
|
|
context "individual students" do
|
|
it "sorts by sortable_name" do
|
|
student_one = student_in_course(
|
|
active_all: true, name: 'Frodo Bravo', sortable_name: 'Bravo, Frodo'
|
|
).user
|
|
student_two = student_in_course(
|
|
active_all: true, name: 'Alfred Charlie', sortable_name: 'Charlie, Alfred'
|
|
).user
|
|
student_three = student_in_course(
|
|
active_all: true, name: 'Beauregard Alpha', sortable_name: 'Alpha, Beauregard'
|
|
).user
|
|
|
|
expect(User).to receive(:best_unicode_collation_key).with('sortable_name').and_call_original
|
|
|
|
assignment = @course.assignments.create!(assignment_valid_attributes)
|
|
representatives = assignment.representatives(user: @teacher)
|
|
|
|
expect(representatives[0].name).to eql(student_three.name)
|
|
expect(representatives[1].name).to eql(student_one.name)
|
|
expect(representatives[2].name).to eql(student_two.name)
|
|
end
|
|
end
|
|
|
|
context "group assignments with all students assigned to a group" do
|
|
include GroupsCommon
|
|
it "sorts by group name" do
|
|
student_one = student_in_course(
|
|
active_all: true, name: 'Frodo Bravo', sortable_name: 'Bravo, Frodo'
|
|
).user
|
|
student_two = student_in_course(
|
|
active_all: true, name: 'Alfred Charlie', sortable_name: 'Charlie, Alfred'
|
|
).user
|
|
student_three = student_in_course(
|
|
active_all: true, name: 'Beauregard Alpha', sortable_name: 'Alpha, Beauregard'
|
|
).user
|
|
|
|
group_category = @course.group_categories.create!(name: "Test Group Set")
|
|
group_one = @course.groups.create!(name: "Group B", group_category: group_category)
|
|
group_two = @course.groups.create!(name: "Group A", group_category: group_category)
|
|
group_three = @course.groups.create!(name: "Group C", group_category: group_category)
|
|
|
|
add_user_to_group(student_one, group_one, true)
|
|
add_user_to_group(student_two, group_two, true)
|
|
add_user_to_group(student_three, group_three, true)
|
|
add_user_to_group(@initial_student, group_three, true)
|
|
|
|
assignment = @course.assignments.create!(
|
|
assignment_valid_attributes.merge(
|
|
group_category: group_category,
|
|
grade_group_students_individually: false
|
|
)
|
|
)
|
|
|
|
expect(Canvas::ICU).to receive(:collate_by).and_call_original
|
|
|
|
representatives = assignment.representatives(user: @teacher)
|
|
|
|
expect(representatives[0].name).to eql(group_two.name)
|
|
expect(representatives[1].name).to eql(group_one.name)
|
|
expect(representatives[2].name).to eql(group_three.name)
|
|
end
|
|
end
|
|
|
|
context "group assignments with no students assigned to a group" do
|
|
it "sorts by sortable_name" do
|
|
student_one = student_in_course(
|
|
active_all: true, name: 'Frodo Bravo', sortable_name: 'Bravo, Frodo'
|
|
).user
|
|
student_two = student_in_course(
|
|
active_all: true, name: 'Alfred Charlie', sortable_name: 'Charlie, Alfred'
|
|
).user
|
|
student_three = student_in_course(
|
|
active_all: true, name: 'Beauregard Alpha', sortable_name: 'Alpha, Beauregard'
|
|
).user
|
|
|
|
group_category = @course.group_categories.create!(name: "Test Group Set")
|
|
|
|
assignment = @course.assignments.create!(
|
|
assignment_valid_attributes.merge(
|
|
group_category: group_category,
|
|
grade_group_students_individually: false
|
|
)
|
|
)
|
|
|
|
expect(Canvas::ICU).to receive(:collate_by).and_call_original
|
|
|
|
representatives = assignment.representatives(user: @teacher)
|
|
|
|
expect(representatives[0].name).to eql(student_three.name)
|
|
expect(representatives[1].name).to eql(student_one.name)
|
|
expect(representatives[2].name).to eql(student_two.name)
|
|
expect(representatives[3].name).to eql(@initial_student.name)
|
|
end
|
|
end
|
|
|
|
context "group assignments with some students assigned to a group and some not" do
|
|
include GroupsCommon
|
|
it "sorts by student name and group name" do
|
|
student_one = student_in_course(
|
|
active_all: true, name: 'Frodo Bravo', sortable_name: 'Bravo, Frodo'
|
|
).user
|
|
student_two = student_in_course(
|
|
active_all: true, name: 'Alfred Charlie', sortable_name: 'Charlie, Alfred'
|
|
).user
|
|
student_three = student_in_course(
|
|
active_all: true, name: 'Beauregard Alpha', sortable_name: 'Alpha, Beauregard'
|
|
).user
|
|
|
|
group_category = @course.group_categories.create!(name: "Test Group Set")
|
|
group_one = @course.groups.create!(name: "Group B", group_category: group_category)
|
|
group_two = @course.groups.create!(name: "Group A", group_category: group_category)
|
|
|
|
add_user_to_group(student_one, group_one, true)
|
|
add_user_to_group(student_two, group_two, true)
|
|
|
|
assignment = @course.assignments.create!(
|
|
assignment_valid_attributes.merge(
|
|
group_category: group_category,
|
|
grade_group_students_individually: false
|
|
)
|
|
)
|
|
|
|
expect(Canvas::ICU).to receive(:collate_by).and_call_original
|
|
|
|
representatives = assignment.representatives(user: @teacher)
|
|
|
|
expect(representatives[0].name).to eql(student_three.name)
|
|
expect(representatives[1].name).to eql(group_two.name)
|
|
expect(representatives[2].name).to eql(group_one.name)
|
|
expect(representatives[3].name).to eql(@initial_student.name)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "group assignments with all students assigned to a group and grade_group_students_individually set to true" do
|
|
include GroupsCommon
|
|
it "sorts by sortable_name" do
|
|
student_one = student_in_course(
|
|
active_all: true, name: 'Frodo Bravo', sortable_name: 'Bravo, Frodo'
|
|
).user
|
|
student_two = student_in_course(
|
|
active_all: true, name: 'Alfred Charlie', sortable_name: 'Charlie, Alfred'
|
|
).user
|
|
student_three = student_in_course(
|
|
active_all: true, name: 'Beauregard Alpha', sortable_name: 'Alpha, Beauregard'
|
|
).user
|
|
|
|
group_category = @course.group_categories.create!(name: "Test Group Set")
|
|
group_one = @course.groups.create!(name: "Group B", group_category: group_category)
|
|
group_two = @course.groups.create!(name: "Group A", group_category: group_category)
|
|
group_three = @course.groups.create!(name: "Group C", group_category: group_category)
|
|
|
|
add_user_to_group(student_one, group_one, true)
|
|
add_user_to_group(student_two, group_two, true)
|
|
add_user_to_group(student_three, group_three, true)
|
|
add_user_to_group(@initial_student, group_three, true)
|
|
|
|
assignment = @course.assignments.create!(
|
|
assignment_valid_attributes.merge(
|
|
group_category: group_category,
|
|
grade_group_students_individually: true
|
|
)
|
|
)
|
|
|
|
expect(User).to receive(:best_unicode_collation_key).with('sortable_name').and_call_original
|
|
|
|
representatives = assignment.representatives(user: @teacher)
|
|
|
|
expect(representatives[0].name).to eql(student_three.name)
|
|
expect(representatives[1].name).to eql(student_one.name)
|
|
expect(representatives[2].name).to eql(student_two.name)
|
|
expect(representatives[3].name).to eql(@initial_student.name)
|
|
end
|
|
end
|
|
|
|
describe "#has_student_submissions?" do
|
|
before :once do
|
|
setup_assignment_with_students
|
|
end
|
|
|
|
it "does not allow itself to be unpublished if it has student submissions" do
|
|
@assignment.submit_homework @stu1, :submission_type => "online_text_entry"
|
|
expect(@assignment).not_to be_can_unpublish
|
|
|
|
@assignment.unpublish
|
|
expect(@assignment).not_to be_valid
|
|
expect(@assignment.errors['workflow_state']).to eq ["Can't unpublish if there are student submissions"]
|
|
end
|
|
|
|
it "does allow itself to be unpublished if it has nil submissions" do
|
|
@assignment.submit_homework @stu1, :submission_type => nil
|
|
expect(@assignment).to be_can_unpublish
|
|
@assignment.unpublish
|
|
expect(@assignment.workflow_state).to eq "unpublished"
|
|
end
|
|
end
|
|
|
|
describe '#secure_params' do
|
|
before { setup_assignment_without_submission }
|
|
|
|
it 'contains the lti_context_id' do
|
|
assignment = Assignment.new
|
|
|
|
new_lti_assignment_id = Canvas::Security.decode_jwt(assignment.secure_params)[:lti_assignment_id]
|
|
old_lti_assignment_id = Canvas::Security.decode_jwt(@assignment.secure_params)[:lti_assignment_id]
|
|
|
|
expect(new_lti_assignment_id).to be_present
|
|
expect(old_lti_assignment_id).to be_present
|
|
end
|
|
|
|
it 'uses the existing lti_context_id if present' do
|
|
lti_context_id = SecureRandom.uuid
|
|
assignment = Assignment.new(lti_context_id: lti_context_id)
|
|
decoded = Canvas::Security.decode_jwt(assignment.secure_params)
|
|
expect(decoded[:lti_assignment_id]).to eq(lti_context_id)
|
|
end
|
|
|
|
it 'returns a jwt' do
|
|
expect(Canvas::Security.decode_jwt(@assignment.secure_params)).to be
|
|
end
|
|
end
|
|
|
|
describe '#grade_to_score' do
|
|
before(:once) { setup_assignment_without_submission }
|
|
|
|
let(:set_type_and_save) do
|
|
lambda do |type|
|
|
@assignment.grading_type = type
|
|
@assignment.save
|
|
end
|
|
end
|
|
|
|
# The test cases for grading_type of points, percent,
|
|
# letter_grade, and gpa_scale are covered by the tests of
|
|
# interpret_grade as that is doing the work. The cases tested
|
|
# here are all contained solely within grade_to_score
|
|
|
|
it 'returns nil for a nil grade' do
|
|
expect(@assignment.grade_to_score(nil)).to be_nil
|
|
end
|
|
|
|
it 'returns nil for a not_graded assignment' do
|
|
set_type_and_save.call('not_graded')
|
|
expect(@assignment.grade_to_score("3")).to be_nil
|
|
end
|
|
|
|
it 'returns an exception for an unknown grading type' do
|
|
set_type_and_save.call("totally_fake_grading")
|
|
expect{@assignment.grade_to_score("3")}.to raise_error("oops, we need to interpret a new grading_type. get coding.")
|
|
end
|
|
|
|
context 'with a pass/fail assignment' do
|
|
before(:once) do
|
|
@assignment.grading_type = 'pass_fail'
|
|
@assignment.points_possible = 6.0
|
|
@assignment.save
|
|
end
|
|
|
|
let(:points_possible) { @assignment.points_possible }
|
|
|
|
it "returns points possible for maximum points" do
|
|
expect(@assignment.grade_to_score(points_possible.to_s)).to eql(points_possible)
|
|
end
|
|
|
|
it "returns nil for partial points" do
|
|
expect(@assignment.grade_to_score("3")).to be_nil
|
|
end
|
|
|
|
it "returns 0.0 for 0 points" do
|
|
expect(@assignment.grade_to_score("0")).to eql(0.0)
|
|
end
|
|
|
|
it "returns nil for an empty string" do
|
|
expect(@assignment.grade_to_score("")).to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#grade_student' do
|
|
let_once(:now) { Time.zone.now }
|
|
let_once(:student) { User.create!.tap {|u| course.enroll_student(u, enrollment_state: 'active') } }
|
|
let_once(:teacher) { User.create!.tap {|u| course.enroll_teacher(u, enrollment_state: 'active') } }
|
|
let_once(:assignment) { course.assignments.create! }
|
|
let_once(:course) do
|
|
course = Account.default.courses.create!
|
|
course.offer!
|
|
course
|
|
end
|
|
|
|
describe 'grade_posting_in_progress' do
|
|
let(:submission) { instance_double("Submission") }
|
|
|
|
before do
|
|
allow(assignment).to receive(:find_or_create_submissions).
|
|
with([student], Submission.preload(:grading_period, :stream_item)).
|
|
and_yield([submission])
|
|
end
|
|
|
|
it 'sets grade_posting_in_progress to false when absent' do
|
|
expect(assignment).to receive(:save_grade_to_submission).
|
|
with([submission], student, nil, grade: 10, grader: teacher)
|
|
assignment.grade_student(student, grade: 10, grader: teacher)
|
|
end
|
|
|
|
it 'sets grade_posting_in_progress to true when present' do
|
|
expect(assignment).to receive(:save_grade_to_submission).
|
|
with([submission], student, nil, grade: 10, grader: teacher, grade_posting_in_progress: true)
|
|
assignment.grade_student(student, grade: 10, grader: teacher, grade_posting_in_progress: true)
|
|
end
|
|
|
|
it 'sets grade_posting_in_progress to false when present' do
|
|
expect(assignment).to receive(:save_grade_to_submission).
|
|
with([submission], student, nil, grade: 10, grader: teacher, grade_posting_in_progress: false)
|
|
assignment.grade_student(student, grade: 10, grader: teacher, grade_posting_in_progress: false)
|
|
end
|
|
end
|
|
|
|
it 'raises a GradeError when grader does not have permission' do
|
|
expect {
|
|
assignment.grade_student(student, grade: 42, grader: student)
|
|
}.to raise_error(Assignment::GradeError)
|
|
end
|
|
|
|
context 'with a submission that has an existing grade' do
|
|
around(:once) do |block|
|
|
Timecop.freeze(now) do
|
|
block.call
|
|
end
|
|
end
|
|
|
|
before(:once) do
|
|
assignment.update!(points_possible: 100, due_at: 36.hours.ago(now), submission_types: %w[online_text_entry])
|
|
late_policy_factory(course: course, deduct: 15.0, every: :day, missing: 80.0)
|
|
assignment.submit_homework(student, submission_type: :online_text_entry, body: :foo)
|
|
end
|
|
|
|
it 'applies the late penalty to a full credit grade' do
|
|
submission, * = assignment.grade_student(student, grade: "100", grader: teacher)
|
|
expect(submission.grade).to eql('70')
|
|
end
|
|
|
|
it 'applies the late penalty to a grade less than full credit' do
|
|
submission, * = assignment.grade_student(student, grade: '70', grader: teacher)
|
|
expect(submission.grade).to eql('40')
|
|
end
|
|
end
|
|
|
|
context 'with a submission' do
|
|
subject_once { submissions }
|
|
let_once(:submissions) { assignment.grade_student(student, grade: "10", grader: teacher) }
|
|
|
|
it { is_expected.to be_an Array }
|
|
|
|
it 'now has a submission' do
|
|
assignment.reload
|
|
expect(assignment.submissions.count).to be 1
|
|
end
|
|
|
|
describe 'the submission after grading' do
|
|
subject_once(:submission) { submissions.first }
|
|
|
|
describe '#state' do
|
|
it { expect(submission.state).to be :graded }
|
|
end
|
|
|
|
describe '#score' do
|
|
it { expect(submission.score).to eq 10.0 }
|
|
end
|
|
|
|
describe '#user_id' do
|
|
it { expect(submission.user_id).to eq student.id }
|
|
end
|
|
|
|
it 'has a version length of one' do
|
|
expect(submission.versions.length).to eq 1
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'with no student' do
|
|
it 'raises an error' do
|
|
expect { assignment.grade_student(nil) }.to raise_error(Assignment::GradeError, 'Student is required')
|
|
end
|
|
end
|
|
|
|
context 'with a student that does not belong' do
|
|
it 'raises an error' do
|
|
expect { assignment.grade_student(User.new) }.to raise_error(Assignment::GradeError, 'Student must be enrolled in the course as a student to be graded')
|
|
end
|
|
end
|
|
|
|
context 'with an invalid initial grade' do
|
|
before :once do
|
|
@result = assignment.grade_student(student, grade: "{", grader: teacher)
|
|
assignment.reload
|
|
end
|
|
|
|
it 'does not change the workflow_state to graded' do
|
|
expect(@result.first.grade).to be_nil
|
|
expect(@result.first.workflow_state).not_to eq 'graded'
|
|
end
|
|
end
|
|
|
|
context "moderated assignment" do
|
|
let_once(:assignment) do
|
|
course.assignments.create!(moderated_grading: true, grader_count: 1, final_grader: teacher)
|
|
end
|
|
let_once(:ta) { course_with_user("TaEnrollment", course: course, active_all: true, name: "Ta").user }
|
|
let(:pg) { @result.first.provisional_grades.find_by!(scorer: ta) }
|
|
|
|
before(:each) do
|
|
@result = assignment.grade_student(student, grade: "10", grader: ta, provisional: true)
|
|
end
|
|
|
|
it "allows for grades to be deleted" do
|
|
expect{
|
|
assignment.grade_student(student, grade: "", grader: ta, provisional: true)
|
|
}.to change{
|
|
pg.reload.grade
|
|
}.from("10").to(nil)
|
|
end
|
|
|
|
it "keeps the provisional grader's slot after grade deletion" do
|
|
assignment.grade_student(student, grade: "10", grader: ta, provisional: true)
|
|
expect{
|
|
assignment.grade_student(student, grade: "", grader: ta, provisional: true)
|
|
}.not_to change{
|
|
assignment.provisional_moderation_graders.first.slot_taken
|
|
}
|
|
end
|
|
|
|
it "does not allow grade to be deleted if grade was selected" do
|
|
selection = assignment.moderated_grading_selections.where(student_id: student.id).first
|
|
selection.provisional_grade = pg
|
|
selection.save!
|
|
|
|
expect{
|
|
assignment.grade_student(student, grade: "", grader: ta, provisional: true)
|
|
}.to raise_error(Assignment::GradeError) do |error|
|
|
expect(error.error_code).to eq Assignment::GradeError::PROVISIONAL_GRADE_MODIFY_SELECTED
|
|
end
|
|
end
|
|
|
|
it "does not allow grade to be changed if grade was selected" do
|
|
selection = assignment.moderated_grading_selections.where(student_id: student.id).first
|
|
selection.provisional_grade = pg
|
|
selection.save!
|
|
|
|
expect{
|
|
assignment.grade_student(student, grade: "23", grader: ta, provisional: true)
|
|
}.to raise_error(Assignment::GradeError) do |error|
|
|
expect(error.error_code).to eq Assignment::GradeError::PROVISIONAL_GRADE_MODIFY_SELECTED
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'with an excused assignment' do
|
|
before :once do
|
|
@result = assignment.grade_student(student, grader: teacher, excuse: true)
|
|
assignment.reload
|
|
end
|
|
|
|
it 'excuses the assignment and marks it as graded' do
|
|
expect(@result.first.grade).to be_nil
|
|
expect(@result.first.workflow_state).to eql 'graded'
|
|
expect(@result.first.excused?).to eql true
|
|
end
|
|
end
|
|
|
|
context 'with anonymous grading' do
|
|
it 'explicitly sets anonymous grading if given' do
|
|
assignment.grade_student(student, graded_anonymously: true, grade: "10", grader: teacher)
|
|
assignment.reload
|
|
expect(assignment.submissions.first.graded_anonymously).to be_truthy
|
|
end
|
|
|
|
it 'does not set anonymous grading if not given' do
|
|
assignment.grade_student(student, graded_anonymously: true, grade: "10", grader: teacher)
|
|
assignment.reload
|
|
assignment.grade_student(student, grade: "10", grader: teacher)
|
|
assignment.reload
|
|
# should still true because grade didn't actually change
|
|
expect(assignment.submissions.first.graded_anonymously).to be_truthy
|
|
end
|
|
end
|
|
|
|
context 'for a moderated assignment' do
|
|
before(:once) do
|
|
student_in_course
|
|
teacher_in_course
|
|
@first_teacher = @teacher
|
|
|
|
teacher_in_course
|
|
@second_teacher = @teacher
|
|
|
|
assignment_model(course: @course, moderated_grading: true, grader_count: 2)
|
|
end
|
|
|
|
it 'allows addition of provisional graders up to the set grader count' do
|
|
@assignment.grade_student(@student, grader: @first_teacher, provisional: true, score: 1)
|
|
@assignment.grade_student(@student, grader: @second_teacher, provisional: true, score: 2)
|
|
|
|
expect(@assignment.moderation_graders).to have(2).items
|
|
end
|
|
|
|
it 'does not allow provisional graders beyond the set grader count' do
|
|
@assignment.grade_student(@student, grader: @first_teacher, provisional: true, score: 1)
|
|
@assignment.grade_student(@student, grader: @second_teacher, provisional: true, score: 2)
|
|
|
|
teacher_in_course
|
|
@superfluous_teacher = @teacher
|
|
|
|
expect { @assignment.grade_student(@student, grader: @superfluous_teacher, provisional: true, score: 2) }.
|
|
to raise_error(Assignment::MaxGradersReachedError)
|
|
end
|
|
|
|
it 'allows the same grader to re-grade an assignment' do
|
|
@assignment.grade_student(@student, grader: @first_teacher, provisional: true, score: 1)
|
|
|
|
expect(@assignment.moderation_graders).to have(1).item
|
|
end
|
|
|
|
it 'creates at most one entry per grader' do
|
|
first_student = @student
|
|
|
|
student_in_course
|
|
second_student = @student
|
|
|
|
@assignment.grade_student(first_student, grader: @first_teacher, provisional: true, score: 1)
|
|
@assignment.grade_student(second_student, grader: @first_teacher, provisional: true, score: 2)
|
|
|
|
expect(@assignment.moderation_graders).to have(1).item
|
|
end
|
|
|
|
it 'raises an error if an invalid score is passed for a provisional grade' do
|
|
expect { @assignment.grade_student(@student, grader: @first_teacher, provisional: true, grade: 'bad') }.
|
|
to raise_error(Assignment::GradeError) do |error|
|
|
expect(error.error_code).to eq Assignment::GradeError::PROVISIONAL_GRADE_INVALID_SCORE
|
|
end
|
|
end
|
|
|
|
context 'with a final grader' do
|
|
before(:once) do
|
|
teacher_in_course(active_all: true)
|
|
@final_grader = @teacher
|
|
|
|
@assignment.update!(final_grader: @final_grader)
|
|
end
|
|
|
|
it 'allows the moderator to issue a grade regardless of the current grader count' do
|
|
@assignment.grade_student(@student, grader: @first_teacher, provisional: true, score: 1)
|
|
@assignment.grade_student(@student, grader: @second_teacher, provisional: true, score: 2)
|
|
@assignment.grade_student(@student, grader: @final_grader, provisional: true, score: 10)
|
|
|
|
expect(@assignment.moderation_graders).to have(3).items
|
|
end
|
|
|
|
it 'excludes the moderator from the current grader count when considering provisional graders' do
|
|
@assignment.grade_student(@student, grader: @final_grader, provisional: true, score: 10)
|
|
@assignment.grade_student(@student, grader: @first_teacher, provisional: true, score: 1)
|
|
@assignment.grade_student(@student, grader: @second_teacher, provisional: true, score: 2)
|
|
|
|
expect(@assignment.moderation_graders).to have(3).items
|
|
end
|
|
|
|
describe 'excusing a moderated assignment' do
|
|
it 'does not accept an excusal from a provisional grader' do
|
|
expect { @assignment.grade_student(@student, grader: @first_teacher, provisional: true, excused: true) }.
|
|
to raise_error(Assignment::GradeError)
|
|
end
|
|
|
|
it 'does not allow a provisional grader to un-excuse an assignment' do
|
|
@assignment.grade_student(@student, grader: @final_grader, provisional: true, excused: true)
|
|
@assignment.grade_student(@student, grader: @first_teacher, provisional: true, excused: false)
|
|
expect(@assignment).to be_excused_for(@student)
|
|
end
|
|
|
|
it 'accepts an excusal from the final grader' do
|
|
@assignment.grade_student(@student, grader: @final_grader, provisional: true, excused: true)
|
|
expect(@assignment).to be_excused_for(@student)
|
|
end
|
|
|
|
it 'allows the final grader to un-excuse an assignment if a score is provided' do
|
|
@assignment.grade_student(@student, grader: @final_grader, provisional: true, excused: true)
|
|
@assignment.grade_student(@student, grader: @final_grader, provisional: true, excused: false, score: 100)
|
|
expect(@assignment).not_to be_excused_for(@student)
|
|
end
|
|
|
|
it 'accepts an excusal from an admin' do
|
|
admin = account_admin_user
|
|
@assignment.grade_student(@student, grader: admin, provisional: true, excused: true)
|
|
expect(@assignment).to be_excused_for(@student)
|
|
end
|
|
|
|
it 'allows an admin to un-excuse an assignment if a score is provided' do
|
|
admin = account_admin_user
|
|
@assignment.grade_student(@student, grader: @final_grader, provisional: true, excused: true)
|
|
@assignment.grade_student(@student, grader: admin, provisional: true, excused: false, score: 100)
|
|
expect(@assignment).not_to be_excused_for(@student)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'AnonymousOrModerationEvent creation on grading a submission' do
|
|
let_once(:assignment) do
|
|
course.assignments.create!(
|
|
anonymous_grading: true,
|
|
grading_type: 'letter_grade',
|
|
points_possible: 100
|
|
)
|
|
end
|
|
|
|
let(:last_event) do
|
|
AnonymousOrModerationEvent.where(assignment: assignment, event_type: 'submission_updated').last
|
|
end
|
|
|
|
it 'creates an event when a grader changes a grade' do
|
|
expect {
|
|
assignment.grade_student(student, grader: teacher, grade: 'C-')
|
|
}.to change {
|
|
AnonymousOrModerationEvent.where(assignment: assignment, event_type: 'submission_updated').count
|
|
}.by(1)
|
|
end
|
|
|
|
it 'creates an event when a grader changes a score' do
|
|
expect {
|
|
assignment.grade_student(student, grader: teacher, score: 60)
|
|
}.to change {
|
|
AnonymousOrModerationEvent.where(assignment: assignment, event_type: 'submission_updated').count
|
|
}.by(1)
|
|
end
|
|
|
|
it 'creates an event when a grader excuses a submission' do
|
|
expect {
|
|
assignment.grade_student(student, grader: teacher, excused: true)
|
|
}.to change {
|
|
AnonymousOrModerationEvent.where(assignment: assignment, event_type: 'submission_updated').count
|
|
}.by(1)
|
|
end
|
|
|
|
it 'includes the affected submission on the event' do
|
|
submission, * = assignment.grade_student(student, grader: teacher, score: 75)
|
|
expect(last_event.submission_id).to eq submission.id
|
|
end
|
|
|
|
it 'includes the grader as the user on the event' do
|
|
assignment.grade_student(student, grader: teacher, score: 91)
|
|
expect(last_event.user_id).to eq teacher.id
|
|
end
|
|
|
|
it 'includes an event type of submission_updated' do
|
|
assignment.grade_student(student, grader: teacher, score: -10)
|
|
expect(last_event.event_type).to eq 'submission_updated'
|
|
end
|
|
|
|
describe 'payload contents' do
|
|
it 'includes changes to "score" in the payload if changed' do
|
|
assignment.grade_student(student, grader: teacher, score: 22)
|
|
assignment.grade_student(student, grader: teacher, score: 11)
|
|
expect(last_event.payload['score']).to eq [22, 11]
|
|
end
|
|
|
|
it 'includes changes to "grade" in the payload if changed' do
|
|
assignment.grade_student(student, grader: teacher, grade: 'B+')
|
|
assignment.grade_student(student, grader: teacher, grade: 'C+')
|
|
expect(last_event.payload['grade']).to eq ['B+', 'C+']
|
|
end
|
|
|
|
it 'includes changes to "excused" in the payload if changed' do
|
|
assignment.grade_student(student, grader: teacher, excused: true)
|
|
assignment.grade_student(student, grader: teacher, grade: 'F')
|
|
expect(last_event.payload['excused']).to eq [true, false]
|
|
end
|
|
end
|
|
|
|
context "for a moderated assignment" do
|
|
let(:moderated_assignment) do
|
|
course.assignments.create!(
|
|
title: 'zzz',
|
|
points_possible: 100,
|
|
moderated_grading: true,
|
|
final_grader: teacher,
|
|
grader_count: 1
|
|
)
|
|
end
|
|
let(:last_event) do
|
|
AnonymousOrModerationEvent.where(
|
|
assignment: moderated_assignment,
|
|
event_type: 'submission_updated'
|
|
).last
|
|
end
|
|
|
|
context "when changing the assignment's excused status as a moderator" do
|
|
it "creates a submission_changed event when excusing the assignment" do
|
|
moderated_assignment.grade_student(student, grader: teacher, provisional: true, excuse: true)
|
|
|
|
expect(last_event.payload['excused']).to eq [nil, true]
|
|
end
|
|
|
|
it "creates a submission_changed event when unexcusing the assignment" do
|
|
moderated_assignment.grade_student(student, grader: teacher, provisional: true, excuse: true)
|
|
moderated_assignment.grade_student(student, grader: teacher, provisional: true, score: 40)
|
|
|
|
expect(last_event.payload['excused']).to eq [true, false]
|
|
end
|
|
|
|
it "does not capture score changes in the submission_changed event" do
|
|
moderated_assignment.grade_student(student, grader: teacher, provisional: true, excuse: true)
|
|
moderated_assignment.grade_student(student, grader: teacher, provisional: true, score: 40)
|
|
|
|
expect(last_event.payload).not_to include('score')
|
|
end
|
|
end
|
|
|
|
it "does not create a submission_changed event when issuing a score via a provisional grade" do
|
|
expect {
|
|
moderated_assignment.grade_student(student, grader: teacher, provisional: true, score: 80)
|
|
}.not_to change {
|
|
AnonymousOrModerationEvent.where(event_type: 'submission_changed').count
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "submission posting" do
|
|
context "when the submission is unposted" do
|
|
it "posts the submission if a grade is assigned" do
|
|
submission, * = assignment.grade_student(student, grader: teacher, score: 50)
|
|
expect(submission).to be_posted
|
|
end
|
|
|
|
it "posts the submission if an excusal is granted" do
|
|
submission, * = assignment.grade_student(student, grader: teacher, excused: true)
|
|
expect(submission).to be_posted
|
|
end
|
|
|
|
it "does not post the submission for a manually-posted assignment" do
|
|
assignment.post_policy.update!(post_manually: true)
|
|
submission, * = assignment.grade_student(student, grader: teacher, score: 50)
|
|
expect(submission).not_to be_posted
|
|
end
|
|
|
|
it "does not post the submission if the grade is provisional" do
|
|
moderated_assignment = course.assignments.create!(
|
|
title: "hi",
|
|
moderated_grading: true,
|
|
final_grader: teacher,
|
|
grader_count: 2
|
|
)
|
|
submission, * = moderated_assignment.grade_student(student, grader: teacher, provisional: true, score: 40)
|
|
expect(submission).not_to be_posted
|
|
end
|
|
end
|
|
|
|
it "does not update the posted_at date for a previously-posted submission" do
|
|
submission = assignment.submissions.find_by!(user: student)
|
|
submission.update!(posted_at: 1.day.ago)
|
|
|
|
expect {
|
|
assignment.grade_student(student, grader: teacher, score: 50)
|
|
}.not_to change {
|
|
submission.reload.posted_at
|
|
}
|
|
end
|
|
end
|
|
|
|
describe "grade change audit records" do
|
|
context "when assignment posts manually" do
|
|
before(:each) do
|
|
assignment.ensure_post_policy(post_manually: true)
|
|
end
|
|
|
|
it "inserts a record" do
|
|
expect(Auditors::GradeChange::Stream).to receive(:insert).once
|
|
assignment.grade_student(student, grade: 10, grader: teacher)
|
|
end
|
|
end
|
|
|
|
context "when assignment posts automatically" do
|
|
before(:each) do
|
|
assignment.ensure_post_policy(post_manually: false)
|
|
end
|
|
|
|
it "inserts a record" do
|
|
expect(Auditors::GradeChange::Stream).to receive(:insert).once
|
|
assignment.grade_student(student, grade: 10, grader: teacher)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "grade change live events" do
|
|
let(:student) { course.enroll_student(User.create!, enrollment_state: :active).user }
|
|
let(:teacher) { course.enroll_teacher(User.create!, enrollment_state: :active).user }
|
|
|
|
context "when assignment posts manually" do
|
|
before(:each) do
|
|
assignment.ensure_post_policy(post_manually: true)
|
|
end
|
|
|
|
it "emits an event" do
|
|
expect(Canvas::LiveEvents).to receive(:grade_changed).once
|
|
assignment.grade_student(student, grade: 10, grader: teacher)
|
|
end
|
|
end
|
|
|
|
context "when assignment posts automatically" do
|
|
before(:each) do
|
|
assignment.ensure_post_policy(post_manually: false)
|
|
end
|
|
|
|
it "emits two events when grading: one for grading and one for posting" do
|
|
expect(Canvas::LiveEvents).to receive(:grade_changed).twice
|
|
assignment.grade_student(student, grade: 10, grader: teacher)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#all_context_module_tags" do
|
|
let(:assignment) { Assignment.new }
|
|
let(:content_tag) { ContentTag.new }
|
|
|
|
it "returns the context module tags for a 'normal' assignment " \
|
|
"(non-quiz and non-discussion topic)" do
|
|
assignment.submission_types = "online_text_entry"
|
|
assignment.context_module_tags << content_tag
|
|
expect(assignment.all_context_module_tags).to eq [content_tag]
|
|
end
|
|
|
|
it "returns the context_module_tags on the quiz if the assignment is " \
|
|
"associated with a quiz" do
|
|
quiz = assignment.build_quiz
|
|
quiz.context_module_tags << content_tag
|
|
assignment.submission_types = "online_quiz"
|
|
expect(assignment.all_context_module_tags).to eq([content_tag])
|
|
end
|
|
|
|
it "returns the context_module_tags on the discussion topic if the " \
|
|
"assignment is associated with a discussion topic" do
|
|
assignment.submission_types = "discussion_topic"
|
|
discussion_topic = assignment.build_discussion_topic
|
|
discussion_topic.context_module_tags << content_tag
|
|
expect(assignment.all_context_module_tags).to eq([content_tag])
|
|
end
|
|
|
|
it "doesn't return the context_module_tags on the wiki page if the " \
|
|
"assignment is associated with a wiki page" do
|
|
assignment.submission_types = "wiki_page"
|
|
wiki_page = assignment.build_wiki_page
|
|
wiki_page.context_module_tags << content_tag
|
|
expect(assignment.all_context_module_tags).to eq([])
|
|
end
|
|
end
|
|
|
|
describe "#submission_type?" do
|
|
shared_examples_for "submittable" do
|
|
subject(:assignment) { Assignment.new }
|
|
let(:be_type) { "be_#{submission_type}".to_sym }
|
|
let(:build_type) { "build_#{submission_type}".to_sym }
|
|
|
|
it "returns false if an assignment does not have a submission" \
|
|
"or matching submission_types" do
|
|
is_expected.not_to send(be_type)
|
|
end
|
|
|
|
it "returns true if the assignment has an associated submission, " \
|
|
"and it has matching submission_types" do
|
|
assignment.submission_types = submission_type
|
|
assignment.send(build_type)
|
|
expect(assignment).to send(be_type)
|
|
end
|
|
|
|
it "returns false if an assignment does not have its submission_types" \
|
|
"set, even if it has an associated submission" do
|
|
assignment.send(build_type)
|
|
expect(assignment).not_to send(be_type)
|
|
end
|
|
|
|
it "returns false if an assignment does not have an associated" \
|
|
"submission even if it has submission_types set" do
|
|
assignment.submission_types = submission_type
|
|
expect(assignment).not_to send(be_type)
|
|
end
|
|
end
|
|
|
|
context "topics" do
|
|
let(:submission_type) { "discussion_topic" }
|
|
|
|
include_examples "submittable"
|
|
end
|
|
|
|
context "pages" do
|
|
let(:submission_type) { "wiki_page" }
|
|
|
|
include_examples "submittable"
|
|
end
|
|
end
|
|
|
|
it "should update a submission's graded_at when grading it" do
|
|
setup_assignment_with_homework
|
|
@assignment.grade_student(@user, grade: 1, grader: @teacher)
|
|
@submission = @assignment.submissions.first
|
|
original_graded_at = @submission.graded_at
|
|
new_time = Time.zone.now + 1.hour
|
|
allow(Time).to receive(:now).and_return(new_time)
|
|
@assignment.grade_student(@user, grade: 2, grader: @teacher)
|
|
@submission.reload
|
|
expect(@submission.graded_at).not_to eql original_graded_at
|
|
end
|
|
|
|
describe "#update_submission" do
|
|
before :once do
|
|
setup_assignment_with_homework
|
|
@assignment.unmute!
|
|
end
|
|
|
|
it "should hide grading comments if assignment is muted and commenter is teacher" do
|
|
@assignment.mute!
|
|
@assignment.update_submission(@user, comment: 'hi', author: @teacher)
|
|
submission = @assignment.submissions.first
|
|
comment = submission.submission_comments.first
|
|
expect(comment).to be_hidden
|
|
end
|
|
|
|
it "hides grading comments if commenter is teacher and assignment is muted after commenting" do
|
|
@assignment.update_submission(@user, comment: 'hi', author: @teacher)
|
|
@assignment.mute!
|
|
submission = @assignment.submissions.first
|
|
comment = submission.submission_comments.first
|
|
expect(comment).to be_hidden
|
|
end
|
|
|
|
it "should not hide grading comments if assignment is not muted even if commenter is teacher" do
|
|
@assignment.update_submission(@user, comment: 'hi', author: @teacher)
|
|
submission = @assignment.submissions.first
|
|
comment = submission.submission_comments.first
|
|
expect(comment).not_to be_hidden
|
|
end
|
|
|
|
it "should not hide grading comments if assignment is muted and commenter is student" do
|
|
@assignment.mute!
|
|
@assignment.update_submission(@user, comment: 'hi', author: @student1)
|
|
submission = @assignment.submissions.first
|
|
comment = submission.submission_comments.first
|
|
expect(comment).not_to be_hidden
|
|
end
|
|
|
|
it "does not hide grading comments if commenter is student and assignment is muted after commenting" do
|
|
@assignment.update_submission(@user, comment: 'hi', author: @student1)
|
|
@assignment.mute!
|
|
submission = @assignment.submissions.first
|
|
comment = submission.submission_comments.first
|
|
expect(comment).not_to be_hidden
|
|
end
|
|
|
|
it "should not hide grading comments if assignment is muted and no commenter is provided" do
|
|
@assignment.mute!
|
|
@assignment.update_submission(@user, comment: 'hi')
|
|
submission = @assignment.submissions.first
|
|
comment = submission.submission_comments.first
|
|
expect(comment).not_to be_hidden
|
|
end
|
|
|
|
it "should hide grading comments if hidden is true" do
|
|
@assignment.update_submission(@user, comment: 'hi', hidden: true)
|
|
submission = @assignment.submissions.first
|
|
comment = submission.submission_comments.first
|
|
expect(comment).to be_hidden
|
|
end
|
|
|
|
it "should not hide grading comments even if muted and posted by teacher if hidden is nil" do
|
|
@assignment.mute!
|
|
@assignment.update_submission(@user, comment: 'hi', author: @teacher, hidden: nil)
|
|
submission = @assignment.submissions.first
|
|
comment = submission.submission_comments.first
|
|
expect(comment).not_to be_hidden
|
|
end
|
|
|
|
context 'for moderated assignments' do
|
|
before(:once) do
|
|
teacher_in_course
|
|
@first_teacher = @teacher
|
|
|
|
teacher_in_course
|
|
@second_teacher = @teacher
|
|
|
|
assignment_model(course: @course, moderated_grading: true, grader_count: 2)
|
|
end
|
|
|
|
let(:submission) { @assignment.submissions.first }
|
|
|
|
it 'allows graders to submit comments up to the set grader count' do
|
|
@assignment.update_submission(@student, commenter: @first_teacher, comment: 'hi', provisional: true)
|
|
@assignment.update_submission(@student, commenter: @second_teacher, comment: 'hi', provisional: true)
|
|
|
|
expect(@assignment.moderation_graders).to have(2).items
|
|
end
|
|
|
|
it 'does not allow graders to comment beyond the set grader count' do
|
|
@assignment.update_submission(@student, commenter: @first_teacher, comment: 'hi', provisional: true)
|
|
@assignment.update_submission(@student, commenter: @second_teacher, comment: 'hi', provisional: true)
|
|
|
|
teacher_in_course
|
|
@superfluous_teacher = @teacher
|
|
|
|
expect { @assignment.update_submission(@student, commenter: @superfluous_teacher, comment: 'hi', provisional: true) }.
|
|
to raise_error(Assignment::MaxGradersReachedError)
|
|
end
|
|
|
|
it 'allows the same grader to issue multiple comments' do
|
|
@assignment.update_submission(@student, commenter: @first_teacher, comment: 'hi', provisional: true)
|
|
|
|
expect(@assignment.moderation_graders).to have(1).item
|
|
end
|
|
|
|
it 'creates at most one entry per grader' do
|
|
first_student = @student
|
|
|
|
student_in_course
|
|
second_student = @student
|
|
|
|
@assignment.update_submission(first_student, commenter: @first_teacher, comment: 'hi', provisional: true)
|
|
@assignment.update_submission(second_student, commenter: @first_teacher, comment: 'hi', provisional: true)
|
|
|
|
expect(@assignment.moderation_graders).to have(1).item
|
|
end
|
|
|
|
it 'creates at most one entry when a grader both grades and comments' do
|
|
@assignment.update_submission(@student, commenter: @first_teacher, comment: 'hi', provisional: true)
|
|
@assignment.grade_student(@student, grader: @first_teacher, provisional: true, score: 10)
|
|
|
|
expect(@assignment.moderation_graders).to have(1).item
|
|
end
|
|
|
|
context 'with a final grader' do
|
|
before(:once) do
|
|
teacher_in_course(active_all: true)
|
|
@final_grader = @teacher
|
|
|
|
@assignment.update!(final_grader: @final_grader)
|
|
end
|
|
|
|
it 'allows the moderator to comment regardless of the current grader count' do
|
|
@assignment.update_submission(@student, commenter: @first_teacher, comment: 'hi', provisional: true)
|
|
@assignment.update_submission(@student, commenter: @second_teacher, comment: 'hi', provisional: true)
|
|
@assignment.update_submission(@student, commenter: @final_grader, comment: 'hi', provisional: true)
|
|
|
|
expect(@assignment.moderation_graders).to have(3).items
|
|
end
|
|
|
|
it 'excludes the moderator from the current grader count when considering provisional graders' do
|
|
@assignment.update_submission(@student, commenter: @final_grader, comment: 'hi', provisional: true)
|
|
@assignment.update_submission(@student, commenter: @first_teacher, comment: 'hi', provisional: true)
|
|
@assignment.update_submission(@student, commenter: @second_teacher, comment: 'hi', provisional: true)
|
|
|
|
expect(@assignment.moderation_graders).to have(3).items
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#infer_grading_type" do
|
|
before do
|
|
setup_assignment_without_submission
|
|
end
|
|
|
|
it "infers points if none is set" do
|
|
@assignment.grading_type = nil
|
|
@assignment.infer_grading_type
|
|
expect(@assignment.grading_type).to eq 'points'
|
|
end
|
|
|
|
it "maintains existing type for vanilla assignments" do
|
|
@assignment.grading_type = 'letter_grade'
|
|
@assignment.infer_grading_type
|
|
expect(@assignment.grading_type).to eq 'letter_grade'
|
|
end
|
|
|
|
it "infers pass_fail for attendance assignments" do
|
|
@assignment.grading_type = 'letter_grade'
|
|
@assignment.submission_types = 'attendance'
|
|
@assignment.infer_grading_type
|
|
expect(@assignment.grading_type).to eq 'pass_fail'
|
|
end
|
|
|
|
it "infers not_graded for page assignments" do
|
|
wiki_page_assignment_model course: @course
|
|
@assignment.grading_type = 'letter_grade'
|
|
@assignment.infer_grading_type
|
|
expect(@assignment.grading_type).to eq 'not_graded'
|
|
end
|
|
end
|
|
|
|
context "needs_grading_count" do
|
|
before :once do
|
|
setup_assignment_with_homework
|
|
end
|
|
|
|
it "should delegate to NeedsGradingCountQuery" do
|
|
query = double('Assignments::NeedsGradingCountQuery')
|
|
expect(query).to receive(:manual_count)
|
|
expect(Assignments::NeedsGradingCountQuery).to receive(:new).with(@assignment).and_return(query)
|
|
@assignment.needs_grading_count
|
|
end
|
|
|
|
it "should update when section (and its enrollments) are moved" do
|
|
@assignment.update_attribute(:updated_at, 1.minute.ago)
|
|
expect(@assignment.needs_grading_count).to eql(1)
|
|
enable_cache do
|
|
expect(Assignments::NeedsGradingCountQuery.new(@assignment, nil).manual_count).to be(1)
|
|
course2 = @course.account.courses.create!
|
|
e = @course.enrollments.where(user_id: @user.id).first.course_section
|
|
e.move_to_course(course2)
|
|
@assignment.reload
|
|
expect(Assignments::NeedsGradingCountQuery.new(@assignment, nil).manual_count).to be(0)
|
|
end
|
|
expect(@assignment.needs_grading_count).to eql(0)
|
|
end
|
|
|
|
it "updated_at should be set when needs_grading_count changes due to a submission" do
|
|
expect(@assignment.needs_grading_count).to eql(1)
|
|
old_timestamp = Time.now.utc - 1.minute
|
|
Assignment.where(:id => @assignment).update_all(:updated_at => old_timestamp)
|
|
@assignment.grade_student(@user, grade: "0", grader: @teacher)
|
|
@assignment.reload
|
|
expect(@assignment.needs_grading_count).to eql(0)
|
|
expect(@assignment.updated_at).to be > old_timestamp
|
|
end
|
|
|
|
it "updated_at should be set when needs_grading_count changes due to an enrollment change" do
|
|
old_timestamp = Time.now.utc - 1.minute
|
|
expect(@assignment.needs_grading_count).to eql(1)
|
|
Assignment.where(:id => @assignment).update_all(:updated_at => old_timestamp)
|
|
@course.enrollments.where(user_id: @user).first.destroy
|
|
@assignment.reload
|
|
expect(@assignment.needs_grading_count).to eql(0)
|
|
expect(@assignment.updated_at).to be > old_timestamp
|
|
end
|
|
end
|
|
|
|
context "differentiated_assignment visibility" do
|
|
describe "students_with_visibility" do
|
|
before :once do
|
|
setup_differentiated_assignments
|
|
end
|
|
|
|
context "differentiated_assignment" do
|
|
it "should return assignments only when a student has overrides" do
|
|
expect(@assignment.students_with_visibility.include?(@student1)).to be_truthy
|
|
expect(@assignment.students_with_visibility.include?(@student2)).to be_falsey
|
|
end
|
|
|
|
it "should not return students outside the class" do
|
|
expect(@assignment.students_with_visibility.include?(@student3)).to be_falsey
|
|
end
|
|
end
|
|
|
|
context "permissions" do
|
|
before :once do
|
|
@assignment.submission_types = "online_text_entry"
|
|
@assignment.save!
|
|
end
|
|
|
|
it "should not allow students without visibility to submit" do
|
|
expect(@assignment.check_policy(@student1)).to include :submit
|
|
expect(@assignment.check_policy(@student2)).not_to include :submit
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context "grading" do
|
|
before :once do
|
|
setup_assignment_without_submission
|
|
end
|
|
|
|
context "pass fail assignments" do
|
|
before :once do
|
|
@assignment.grading_type = 'pass_fail'
|
|
@assignment.points_possible = 0.0
|
|
@assignment.save
|
|
end
|
|
|
|
let(:submission) { @assignment.submissions.first }
|
|
|
|
it "preserves pass with zero points possible" do
|
|
@assignment.grade_student(@user, grade: 'pass', grader: @teacher)
|
|
expect(submission.grade).to eql('complete')
|
|
end
|
|
|
|
it "preserves fail with zero points possible" do
|
|
@assignment.grade_student(@user, grade: 'fail', grader: @teacher)
|
|
expect(submission.grade).to eql('incomplete')
|
|
end
|
|
|
|
it "should properly compute pass/fail for nil" do
|
|
@assignment.points_possible = 10
|
|
grade = @assignment.score_to_grade(nil)
|
|
expect(grade).to eql("incomplete")
|
|
end
|
|
end
|
|
|
|
it "should preserve letter grades with zero points possible" do
|
|
@assignment.grading_type = 'letter_grade'
|
|
@assignment.points_possible = 0.0
|
|
@assignment.save!
|
|
|
|
s = @assignment.grade_student(@user, grade: 'C', grader: @teacher)
|
|
expect(s).to be_is_a(Array)
|
|
@assignment.reload
|
|
expect(@assignment.submissions.size).to eql(1)
|
|
@submission = @assignment.submissions.first
|
|
expect(@submission.state).to eql(:graded)
|
|
expect(@submission.score).to eql(0.0)
|
|
expect(@submission.grade).to eql('C')
|
|
expect(@submission.user_id).to eql(@user.id)
|
|
end
|
|
|
|
it "should properly calculate letter grades" do
|
|
@assignment.grading_type = 'letter_grade'
|
|
@assignment.points_possible = 10
|
|
grade = @assignment.score_to_grade(8.7)
|
|
expect(grade).to eql("B+")
|
|
end
|
|
|
|
it "should properly allow decimal points in grading" do
|
|
@assignment.grading_type = 'letter_grade'
|
|
@assignment.points_possible = 10
|
|
grade = @assignment.score_to_grade(8.6999)
|
|
expect(grade).to eql("B")
|
|
end
|
|
|
|
it "should preserve letter grades grades with nil points possible" do
|
|
@assignment.grading_type = 'letter_grade'
|
|
@assignment.points_possible = nil
|
|
@assignment.save!
|
|
|
|
s = @assignment.grade_student(@user, grade: 'C', grader: @teacher)
|
|
expect(s).to be_is_a(Array)
|
|
@assignment.reload
|
|
expect(@assignment.submissions.size).to eql(1)
|
|
@submission = @assignment.submissions.first
|
|
expect(@submission.state).to eql(:graded)
|
|
expect(@submission.score).to eql(0.0)
|
|
expect(@submission.grade).to eql('C')
|
|
expect(@submission.user_id).to eql(@user.id)
|
|
end
|
|
|
|
it "should preserve gpa scale grades with nil points possible" do
|
|
@assignment.grading_type = 'gpa_scale'
|
|
@assignment.points_possible = nil
|
|
@assignment.context.grading_standards.build({title: "GPA"})
|
|
gs = @assignment.context.grading_standards.last
|
|
gs.data = {"4.0" => 0.94,
|
|
"3.7" => 0.90,
|
|
"3.3" => 0.87,
|
|
"3.0" => 0.84,
|
|
"2.7" => 0.80,
|
|
"2.3" => 0.77,
|
|
"2.0" => 0.74,
|
|
"1.7" => 0.70,
|
|
"1.3" => 0.67,
|
|
"1.0" => 0.64,
|
|
"0" => 0.01,
|
|
"M" => 0.0 }
|
|
gs.assignments << @assignment
|
|
gs.save!
|
|
@assignment.save!
|
|
|
|
s = @assignment.grade_student(@user, grade: '3.0', grader: @teacher)
|
|
expect(s).to be_is_a(Array)
|
|
@assignment.reload
|
|
expect(@assignment.submissions.size).to eql(1)
|
|
@submission = @assignment.submissions.first
|
|
expect(@submission.state).to eql(:graded)
|
|
expect(@submission.score).to eql(0.0)
|
|
expect(@submission.grade).to eql('3.0')
|
|
expect(@submission.user_id).to eql(@user.id)
|
|
end
|
|
|
|
describe "#grading_standard_or_default" do
|
|
before do
|
|
@gs1 = @course.grading_standards.create! standard_data: {
|
|
a: {name: "OK", value: 100},
|
|
b: {name: "Bad", value: 0},
|
|
}
|
|
@gs2 = @course.grading_standards.create! standard_data: {
|
|
a: {name: "🚀", value: 100},
|
|
b: {name: "🚽", value: 0},
|
|
}
|
|
end
|
|
|
|
it "returns the assignment-specific grading standard if there is one" do
|
|
@assignment.update_attribute :grading_standard, @gs1
|
|
expect(@assignment.grading_standard_or_default).to eql @gs1
|
|
end
|
|
|
|
it "uses the course default if there is one" do
|
|
@course.update_attribute :default_grading_standard, @gs2
|
|
expect(@assignment.grading_standard_or_default).to eql @gs2
|
|
end
|
|
|
|
it "uses the canvas default" do
|
|
expect(@assignment.grading_standard_or_default.title).to eql "Default Grading Scheme"
|
|
end
|
|
end
|
|
|
|
it "converts using numbers sensitive to floating point errors" do
|
|
@assignment.grading_type = "letter_grade"
|
|
@assignment.points_possible = 100
|
|
gs = @assignment.context.grading_standards.build({title: "Numerical"})
|
|
gs.data = {"A" => 0.29, "F" => 0.00}
|
|
gs.assignments << @assignment
|
|
gs.save!
|
|
@assignment.save!
|
|
|
|
# 0.29 * 100 = 28.999999999999996 in ruby, which matches F instead of A
|
|
expect(@assignment.score_to_grade(29)).to eq("A")
|
|
end
|
|
|
|
it "should preserve gpa scale grades with zero points possible" do
|
|
@assignment.grading_type = 'gpa_scale'
|
|
@assignment.points_possible = 0.0
|
|
@assignment.context.grading_standards.build({title: "GPA"})
|
|
gs = @assignment.context.grading_standards.last
|
|
gs.data = {"4.0" => 0.94,
|
|
"3.7" => 0.90,
|
|
"3.3" => 0.87,
|
|
"3.0" => 0.84,
|
|
"2.7" => 0.80,
|
|
"2.3" => 0.77,
|
|
"2.0" => 0.74,
|
|
"1.7" => 0.70,
|
|
"1.3" => 0.67,
|
|
"1.0" => 0.64,
|
|
"0" => 0.01,
|
|
"M" => 0.0 }
|
|
gs.assignments << @assignment
|
|
gs.save!
|
|
@assignment.save!
|
|
|
|
s = @assignment.grade_student(@user, grade: '3.0', grader: @teacher)
|
|
expect(s).to be_is_a(Array)
|
|
@assignment.reload
|
|
expect(@assignment.submissions.size).to eql(1)
|
|
@submission = @assignment.submissions.first
|
|
expect(@submission.state).to eql(:graded)
|
|
expect(@submission.score).to eql(0.0)
|
|
expect(@submission.grade).to eql('3.0')
|
|
expect(@submission.user_id).to eql(@user.id)
|
|
end
|
|
|
|
it "should handle percent grades with nil points possible" do
|
|
@assignment.grading_type = "percent"
|
|
@assignment.points_possible = nil
|
|
grade = @assignment.score_to_grade(5.0)
|
|
expect(grade).to eql('5%')
|
|
end
|
|
|
|
it "should round down percent grades to 2 decimal places" do
|
|
@assignment.grading_type = 'percent'
|
|
@assignment.points_possible = 100
|
|
grade = @assignment.score_to_grade(57.8934)
|
|
expect(grade).to eql('57.89%')
|
|
end
|
|
|
|
it "should round up percent grades to 2 decimal places" do
|
|
@assignment.grading_type = 'percent'
|
|
@assignment.points_possible = 100
|
|
grade = @assignment.score_to_grade(57.895)
|
|
expect(grade).to eql('57.9%')
|
|
end
|
|
|
|
it "should give a grade to extra credit assignments" do
|
|
@assignment.grading_type = 'points'
|
|
@assignment.points_possible = 0.0
|
|
@assignment.save
|
|
s = @assignment.grade_student(@user, grade: "1", grader: @teacher)
|
|
expect(s).to be_is_a(Array)
|
|
@assignment.reload
|
|
expect(@assignment.submissions.size).to eql(1)
|
|
@submission = @assignment.submissions.first
|
|
expect(@submission.state).to eql(:graded)
|
|
expect(@submission).to eql(s[0])
|
|
expect(@submission.score).to eql(1.0)
|
|
expect(@submission.grade).to eql("1")
|
|
expect(@submission.user_id).to eql(@user.id)
|
|
|
|
@submission.score = 2.0
|
|
@submission.save
|
|
@submission.reload
|
|
expect(@submission.grade).to eql("2")
|
|
end
|
|
|
|
it "should be able to grade an already-existing submission" do
|
|
s = @a.submit_homework(@user)
|
|
s2 = @a.grade_student(@user, grade: "10", grader: @teacher)
|
|
s.reload
|
|
expect(s).to eql(s2[0])
|
|
# there should only be one version, even though the grade changed
|
|
expect(s.versions.length).to eql(1)
|
|
expect(s2[0].state).to eql(:graded)
|
|
end
|
|
|
|
context "group assignments" do
|
|
before :once do
|
|
@student1, @student2 = n_students_in_course(2, course: @course)
|
|
gc = @course.group_categories.create! name: "a name"
|
|
group = gc.groups.create! name: "zxcv", context: @course
|
|
[@student1, @student2].each { |u|
|
|
group.group_memberships.create! user: u, workflow_state: "accepted"
|
|
}
|
|
@assignment.update_attribute :group_category, gc
|
|
end
|
|
|
|
context "when excusing an assignment" do
|
|
it "marks the assignment as excused" do
|
|
submission, = @assignment.grade_student(@student, grader: @teacher, excuse: true)
|
|
expect(submission).to be_excused
|
|
end
|
|
|
|
it "doesn't mark everyone in the group excused" do
|
|
sub1, sub2 = @assignment.grade_student(@student1, grader: @teacher, excuse: true)
|
|
|
|
expect(sub1.user).to eq @student1
|
|
expect(sub1).to be_excused
|
|
expect(sub2).to be_nil
|
|
end
|
|
|
|
context "when trying to grade and excuse simultaneously" do
|
|
it "raises an error" do
|
|
expect(lambda {
|
|
@assignment.grade_student(
|
|
@student1,
|
|
grade: 0,
|
|
excuse: true
|
|
)
|
|
}).to raise_error("Cannot simultaneously grade and excuse an assignment")
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when not excusing an assignment" do
|
|
it "grades every member of the group" do
|
|
sub1, sub2 = @assignment.grade_student(
|
|
@student1,
|
|
grade: 38,
|
|
grader: @teacher,
|
|
excuse: false,
|
|
)
|
|
|
|
expect(sub1.user).to eq @student1
|
|
expect(sub1.grade).to eq "38"
|
|
expect(sub2.user).to eq @student2
|
|
expect(sub2.grade).to eq "38"
|
|
end
|
|
|
|
it "doesn't overwrite the grades of group members who have been excused" do
|
|
sub1 = @assignment.grade_student(@student1, grader: @teacher, excuse: true).first
|
|
expect(sub1).to be_excused
|
|
|
|
sub2, sub3 = @assignment.grade_student(@student2, grade: 10, grader: @teacher)
|
|
expect(sub1.reload).to be_excused
|
|
expect(sub2.user).to eq @student2
|
|
expect(sub2.grade).to eq "10"
|
|
expect(sub3).to be_nil
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
describe "interpret_grade" do
|
|
before :once do
|
|
setup_assignment_without_submission
|
|
end
|
|
|
|
it "should return nil when no grade was entered and assignment uses a grading standard (letter grade)" do
|
|
@assignment.points_possible = 100
|
|
expect(@assignment.interpret_grade("")).to be_nil
|
|
end
|
|
|
|
it "should allow grading an assignment with nil points_possible" do
|
|
@assignment.points_possible = nil
|
|
expect(@assignment.interpret_grade("100%")).to eq 0
|
|
end
|
|
|
|
it "should not round scores" do
|
|
@assignment.points_possible = 15
|
|
expect(@assignment.interpret_grade("88.75%")).to eq 13.3125
|
|
end
|
|
|
|
context "with alphanumeric grades" do
|
|
before(:once) do
|
|
@assignment.update!(grading_type: 'letter_grade', points_possible: 10.0)
|
|
grading_standard = @course.grading_standards.build(title: "Number Before Letter")
|
|
grading_standard.data = {
|
|
"1A" => 0.9,
|
|
"2B" => 0.8,
|
|
"3C" => 0.7,
|
|
"4D" => 0.6,
|
|
"5+" => 0.5,
|
|
"5F" => 0
|
|
}
|
|
grading_standard.assignments << @assignment
|
|
grading_standard.save!
|
|
end
|
|
|
|
it "does not treat maximum grade as a number" do
|
|
expect(@assignment.interpret_grade("1A")).to eq 10.0
|
|
end
|
|
|
|
it "does not treat lower grade as a number" do
|
|
expect(@assignment.interpret_grade("2B")).to eq 8.9
|
|
end
|
|
|
|
it "does not treat number followed by plus symbol as a number" do
|
|
expect(@assignment.interpret_grade("5+")).to eq 5.9
|
|
end
|
|
|
|
it "treats unsigned integer score as a number" do
|
|
expect(@assignment.interpret_grade("7")).to eq 7.0
|
|
end
|
|
|
|
it "treats negative score with decimals as a number" do
|
|
expect(@assignment.interpret_grade("-.2")).to eq (-0.2)
|
|
end
|
|
|
|
it "treats positive score with decimals as a number" do
|
|
expect(@assignment.interpret_grade("+0.35")).to eq 0.35
|
|
end
|
|
|
|
it "treats number with percent symbol as a percentage" do
|
|
expect(@assignment.interpret_grade("75.2%")).to eq 7.52
|
|
end
|
|
end
|
|
|
|
context "with gpa_scale" do
|
|
before(:once) do
|
|
@assignment.update!(grading_type: 'gpa_scale', points_possible: 10.0)
|
|
end
|
|
|
|
it "accepts numbers" do
|
|
expect(@assignment.interpret_grade("9.5")).to eq 9.5
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#submit_homework' do
|
|
before(:once) do
|
|
course_with_student(active_all: true)
|
|
@a = @course.assignments.create! title: "blah",
|
|
submission_types: "online_text_entry,online_url",
|
|
points_possible: 10
|
|
end
|
|
|
|
it "sets the 'eula_agreement_timestamp'" do
|
|
setup_assignment_without_submission
|
|
timestamp = Time.now.to_i.to_s
|
|
@a.submit_homework(@user, {eula_agreement_timestamp: timestamp})
|
|
expect(@a.submissions.first.turnitin_data[:eula_agreement_timestamp]).to eq timestamp
|
|
end
|
|
|
|
it "creates a new version for each submission" do
|
|
setup_assignment_without_submission
|
|
@a.submit_homework(@user)
|
|
@a.submit_homework(@user)
|
|
@a.submit_homework(@user)
|
|
@a.reload
|
|
expect(@a.submissions.first.versions.length).to eql(3)
|
|
end
|
|
|
|
it "doesn't mark as submitted if no submission" do
|
|
s = @a.submit_homework(@user)
|
|
expect(s.workflow_state).to eq "unsubmitted"
|
|
end
|
|
|
|
it "clears out stale submission information" do
|
|
@a.submissions.find_by(user: @user).update(
|
|
late_policy_status: 'late',
|
|
seconds_late_override: 120
|
|
)
|
|
s = @a.submit_homework(@user, submission_type: "online_url",
|
|
url: "http://example.com")
|
|
expect(s.submission_type).to eq "online_url"
|
|
expect(s.url).to eq "http://example.com"
|
|
expect(s.late_policy_status).to be nil
|
|
expect(s.seconds_late_override).to be nil
|
|
|
|
s2 = @a.submit_homework(@user, submission_type: "online_text_entry",
|
|
body: "blah blah blah blah blah blah blah")
|
|
expect(s2.submission_type).to eq "online_text_entry"
|
|
expect(s2.body).to eq "blah blah blah blah blah blah blah"
|
|
expect(s2.url).to be_nil
|
|
expect(s2.workflow_state).to eq "submitted"
|
|
|
|
@a.submissions.find_by(user: @user).update(
|
|
late_policy_status: 'late',
|
|
seconds_late_override: 120
|
|
)
|
|
# comments shouldn't clear out submission data
|
|
s3 = @a.submit_homework(@user, comment: "BLAH BLAH")
|
|
expect(s3.body).to eq "blah blah blah blah blah blah blah"
|
|
expect(s3.submission_comments.first.comment).to eq "BLAH BLAH"
|
|
expect(s3.submission_type).to eq "online_text_entry"
|
|
expect(s3.late_policy_status).to eq "late"
|
|
expect(s3.seconds_late_override).to eq 120
|
|
end
|
|
|
|
it "sets the submission's 'lti_user_id'" do
|
|
setup_assignment_without_submission
|
|
submission = @a.submit_homework(@user)
|
|
expect(submission.lti_user_id).to eq @user.lti_context_id
|
|
end
|
|
end
|
|
|
|
describe "muting" do
|
|
before :once do
|
|
assignment_model(course: @course)
|
|
@student = @course.enroll_student(User.create!, enrollment_state: :active).user
|
|
@teacher = @course.enroll_teacher(User.create!, enrollment_state: :active).user
|
|
end
|
|
|
|
it "defaults to muted" do
|
|
expect(@course.assignments.create!).to be_muted
|
|
end
|
|
|
|
it "should be mutable" do
|
|
expect(@assignment.respond_to?(:mute!)).to eql true
|
|
@assignment.mute!
|
|
expect(@assignment.muted?).to eql true
|
|
end
|
|
|
|
it "should be unmutable" do
|
|
expect(@assignment.respond_to?(:unmute!)).to eql true
|
|
@assignment.mute!
|
|
@assignment.unmute!
|
|
expect(@assignment.muted?).to eql false
|
|
end
|
|
|
|
it 'mutes assignments when they are update from non-anonymous to anonymous' do
|
|
assignment = @course.assignments.create!
|
|
assignment.update!(muted: false)
|
|
expect { assignment.update!(anonymous_grading: true) }.to change {
|
|
assignment.muted?
|
|
}.from(false).to(true)
|
|
end
|
|
|
|
it 'does not mute assignments when they are updated from anonymous to non-anonymous' do
|
|
assignment = @course.assignments.create!(anonymous_grading: true)
|
|
assignment.update!(muted: false)
|
|
expect { assignment.update!(anonymous_grading: false) }.not_to change {
|
|
assignment.muted?
|
|
}.from(false)
|
|
end
|
|
|
|
describe "grade change audit records" do
|
|
it "continues to insert grade change records when assignment is muted" do
|
|
expect(Auditors::GradeChange::Stream).to receive(:insert).once
|
|
@assignment.grade_student(@student, grade: 10, grader: @teacher)
|
|
end
|
|
|
|
it "does not insert a grade change event when muting" do
|
|
@assignment.unmute!
|
|
@assignment.grade_student(@student, grade: 10, grader: @teacher)
|
|
expect(Auditors::GradeChange::Stream).not_to receive(:insert)
|
|
@assignment.mute!
|
|
end
|
|
end
|
|
|
|
describe "grade change live events" do
|
|
it "emits an event for graded submissions when muting" do
|
|
@assignment.unmute!
|
|
@assignment.grade_student(@student, grade: 10, grader: @teacher)
|
|
expect(Canvas::LiveEvents).to receive(:grade_changed).once
|
|
@assignment.mute!
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#unmute!" do
|
|
before :once do
|
|
@assignment = assignment_model(course: @course)
|
|
@student = @course.enroll_student(User.create!, enrollment_state: :active).user
|
|
@teacher = @course.enroll_teacher(User.create!, enrollment_state: :active).user
|
|
@assignment.unmute!
|
|
end
|
|
|
|
it "returns falsey when assignment is not muted" do
|
|
expect(@assignment.unmute!).to be_falsey
|
|
end
|
|
|
|
context "when assignment is anonymously graded" do
|
|
before :once do
|
|
@assignment.update(moderated_grading: true, anonymous_grading: true, grader_count: 1)
|
|
@assignment.mute!
|
|
end
|
|
|
|
context "when grades have not been published" do
|
|
it "does not unmute the assignment" do
|
|
@assignment.unmute!
|
|
expect(@assignment).to be_muted
|
|
end
|
|
|
|
it "adds an error for 'muted'" do
|
|
@assignment.unmute!
|
|
expect(@assignment.errors["muted"]).to eq(["Anonymous moderated assignments cannot be unmuted until grades are posted"])
|
|
end
|
|
|
|
it "returns false" do
|
|
expect(@assignment.unmute!).to eq(false)
|
|
end
|
|
end
|
|
|
|
context "when grades have been published" do
|
|
before :once do
|
|
@assignment.update_attribute(:grades_published_at, Time.now.utc)
|
|
end
|
|
|
|
it "unmutes the assignment" do
|
|
@assignment.unmute!
|
|
expect(@assignment).not_to be_muted
|
|
end
|
|
|
|
it "returns true" do
|
|
expect(@assignment.unmute!).to eq(true)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when assignment is anonymously graded and not moderated" do
|
|
before :once do
|
|
@assignment.update(moderated_grading: false, anonymous_grading: true)
|
|
@assignment.mute!
|
|
end
|
|
|
|
it "unmutes the assignment" do
|
|
@assignment.unmute!
|
|
expect(@assignment).not_to be_muted
|
|
end
|
|
|
|
it "returns true" do
|
|
expect(@assignment.unmute!).to eq(true)
|
|
end
|
|
end
|
|
|
|
context "when assignment is not anonymously graded" do
|
|
before :once do
|
|
@assignment.update(moderated_grading: true, anonymous_grading: false, grader_count: 1)
|
|
@assignment.mute!
|
|
end
|
|
|
|
it "unmutes the assignment" do
|
|
@assignment.unmute!
|
|
expect(@assignment).not_to be_muted
|
|
end
|
|
|
|
it "returns true" do
|
|
expect(@assignment.unmute!).to eq(true)
|
|
end
|
|
end
|
|
|
|
it "does not insert a grade change audit record when unmuting" do
|
|
@assignment.mute!
|
|
@assignment.grade_student(@student, grade: 10, grader: @teacher)
|
|
expect(Auditors::GradeChange::Stream).not_to receive(:insert)
|
|
@assignment.unmute!
|
|
end
|
|
|
|
it "emits a grade change live event for graded submissions when unmuting" do
|
|
@assignment.mute!
|
|
@assignment.grade_student(@student, grade: 10, grader: @teacher)
|
|
expect(Canvas::LiveEvents).to receive(:grade_changed).once
|
|
@assignment.unmute!
|
|
end
|
|
end
|
|
|
|
describe "infer_times" do
|
|
it "should set to all_day" do
|
|
assignment_model(:due_at => "Sep 3 2008 12:00am",
|
|
:lock_at => "Sep 3 2008 12:00am",
|
|
:unlock_at => "Sep 3 2008 12:00am",
|
|
:course => @course)
|
|
expect(@assignment.all_day).to eql(false)
|
|
@assignment.infer_times
|
|
@assignment.save!
|
|
expect(@assignment.all_day).to eql(true)
|
|
expect(@assignment.due_at.strftime("%H:%M")).to eql("23:59")
|
|
expect(@assignment.lock_at.strftime("%H:%M")).to eql("23:59")
|
|
expect(@assignment.unlock_at.strftime("%H:%M")).to eql("00:00")
|
|
expect(@assignment.all_day_date).to eql(Date.parse("Sep 3 2008"))
|
|
end
|
|
|
|
it "should not set to all_day without infer_times call" do
|
|
assignment_model(:due_at => "Sep 3 2008 12:00am",
|
|
:course => @course)
|
|
expect(@assignment.all_day).to eql(false)
|
|
expect(@assignment.due_at.strftime("%H:%M")).to eql("00:00")
|
|
expect(@assignment.all_day_date).to eql(Date.parse("Sep 3 2008"))
|
|
end
|
|
end
|
|
|
|
describe "all_day and all_day_date from due_at" do
|
|
def fancy_midnight(opts={})
|
|
zone = opts[:zone] || Time.zone
|
|
Time.use_zone(zone) do
|
|
time = opts[:time] || Time.zone.now
|
|
time.in_time_zone.midnight + 1.day - 1.minute
|
|
end
|
|
end
|
|
|
|
before :once do
|
|
@assignment = assignment_model(course: @course)
|
|
end
|
|
|
|
it "should interpret 11:59pm as all day with no prior value" do
|
|
@assignment.due_at = fancy_midnight(:zone => 'Alaska')
|
|
@assignment.time_zone_edited = 'Alaska'
|
|
@assignment.save!
|
|
expect(@assignment.all_day).to eq true
|
|
end
|
|
|
|
it "should interpret 11:59pm as all day with same-tz all-day prior value" do
|
|
@assignment.due_at = fancy_midnight(:zone => 'Alaska') + 1.day
|
|
@assignment.save!
|
|
@assignment.due_at = fancy_midnight(:zone => 'Alaska')
|
|
@assignment.time_zone_edited = 'Alaska'
|
|
@assignment.save!
|
|
expect(@assignment.all_day).to eq true
|
|
end
|
|
|
|
it "should interpret 11:59pm as all day with other-tz all-day prior value" do
|
|
@assignment.due_at = fancy_midnight(:zone => 'Baghdad')
|
|
@assignment.save!
|
|
@assignment.due_at = fancy_midnight(:zone => 'Alaska')
|
|
@assignment.time_zone_edited = 'Alaska'
|
|
@assignment.save!
|
|
expect(@assignment.all_day).to eq true
|
|
end
|
|
|
|
it "should interpret 11:59pm as all day with non-all-day prior value" do
|
|
@assignment.due_at = fancy_midnight(:zone => 'Alaska') + 1.hour
|
|
@assignment.save!
|
|
@assignment.due_at = fancy_midnight(:zone => 'Alaska')
|
|
@assignment.time_zone_edited = 'Alaska'
|
|
@assignment.save!
|
|
expect(@assignment.all_day).to eq true
|
|
end
|
|
|
|
it "should not interpret non-11:59pm as all day no prior value" do
|
|
@assignment.due_at = fancy_midnight(:zone => 'Alaska').in_time_zone('Baghdad')
|
|
@assignment.time_zone_edited = 'Baghdad'
|
|
@assignment.save!
|
|
expect(@assignment.all_day).to eq false
|
|
end
|
|
|
|
it "should not interpret non-11:59pm as all day with same-tz all-day prior value" do
|
|
@assignment.due_at = fancy_midnight(:zone => 'Alaska')
|
|
@assignment.save!
|
|
@assignment.due_at = fancy_midnight(:zone => 'Alaska') + 1.hour
|
|
@assignment.time_zone_edited = 'Alaska'
|
|
@assignment.save!
|
|
expect(@assignment.all_day).to eq false
|
|
end
|
|
|
|
it "should not interpret non-11:59pm as all day with other-tz all-day prior value" do
|
|
@assignment.due_at = fancy_midnight(:zone => 'Baghdad')
|
|
@assignment.save!
|
|
@assignment.due_at = fancy_midnight(:zone => 'Alaska') + 1.hour
|
|
@assignment.time_zone_edited = 'Alaska'
|
|
@assignment.save!
|
|
expect(@assignment.all_day).to eq false
|
|
end
|
|
|
|
it "should not interpret non-11:59pm as all day with non-all-day prior value" do
|
|
@assignment.due_at = fancy_midnight(:zone => 'Alaska') + 1.hour
|
|
@assignment.save!
|
|
@assignment.due_at = fancy_midnight(:zone => 'Alaska') + 2.hour
|
|
@assignment.time_zone_edited = 'Alaska'
|
|
@assignment.save!
|
|
expect(@assignment.all_day).to eq false
|
|
end
|
|
|
|
it "should preserve all-day when only changing time zone" do
|
|
@assignment.due_at = fancy_midnight(:zone => 'Alaska')
|
|
@assignment.time_zone_edited = 'Alaska'
|
|
@assignment.save!
|
|
@assignment.due_at = fancy_midnight(:zone => 'Alaska').in_time_zone('Baghdad')
|
|
@assignment.time_zone_edited = 'Baghdad'
|
|
@assignment.save!
|
|
expect(@assignment.all_day).to eq true
|
|
end
|
|
|
|
it "should preserve non-all-day when only changing time zone" do
|
|
@assignment.due_at = fancy_midnight(:zone => 'Alaska').in_time_zone('Baghdad')
|
|
@assignment.save!
|
|
@assignment.due_at = fancy_midnight(:zone => 'Alaska')
|
|
@assignment.time_zone_edited = 'Alaska'
|
|
@assignment.save!
|
|
expect(@assignment.all_day).to eq false
|
|
end
|
|
|
|
it "should determine date from due_at's timezone" do
|
|
@assignment.due_at = Date.today.in_time_zone('Baghdad') + 1.hour # 01:00:00 AST +03:00 today
|
|
@assignment.time_zone_edited = 'Baghdad'
|
|
@assignment.save!
|
|
expect(@assignment.all_day_date).to eq Date.today
|
|
|
|
@assignment.due_at = @assignment.due_at.in_time_zone('Alaska') - 2.hours # 12:00:00 AKDT -08:00 previous day
|
|
@assignment.time_zone_edited = 'Alaska'
|
|
@assignment.save!
|
|
expect(@assignment.all_day_date).to eq Date.today - 1.day
|
|
end
|
|
|
|
it "should preserve all-day date when only changing time zone" do
|
|
@assignment.due_at = Date.today.in_time_zone('Baghdad') # 00:00:00 AST +03:00 today
|
|
@assignment.time_zone_edited = 'Baghdad'
|
|
@assignment.save!
|
|
@assignment.due_at = @assignment.due_at.in_time_zone('Alaska') # 13:00:00 AKDT -08:00 previous day
|
|
@assignment.time_zone_edited = 'Alaska'
|
|
@assignment.save!
|
|
expect(@assignment.all_day_date).to eq Date.today
|
|
end
|
|
|
|
it "should preserve non-all-day date when only changing time zone" do
|
|
@assignment.due_at = Date.today.in_time_zone('Alaska') - 11.hours # 13:00:00 AKDT -08:00 previous day
|
|
@assignment.save!
|
|
@assignment.due_at = @assignment.due_at.in_time_zone('Baghdad') # 00:00:00 AST +03:00 today
|
|
@assignment.time_zone_edited = 'Baghdad'
|
|
@assignment.save!
|
|
expect(@assignment.all_day_date).to eq Date.today - 1.day
|
|
end
|
|
end
|
|
|
|
it "should destroy group overrides when the group category changes" do
|
|
@assignment = assignment_model(course: @course)
|
|
@assignment.group_category = group_category(context: @assignment.context)
|
|
@assignment.save!
|
|
|
|
overrides = 5.times.map do
|
|
override = @assignment.assignment_overrides.scope.new
|
|
override.set = @assignment.group_category.groups.create!(context: @assignment.context)
|
|
override.save!
|
|
|
|
expect(override.workflow_state).to eq 'active'
|
|
override
|
|
end
|
|
old_version_number = @assignment.version_number
|
|
|
|
@assignment.group_category = group_category(context: @assignment.context, name: "bar")
|
|
@assignment.save!
|
|
|
|
overrides.each do |override|
|
|
override.reload
|
|
|
|
expect(override.workflow_state).to eq 'deleted'
|
|
expect(override.versions.size).to eq 2
|
|
expect(override.assignment_version).to eq old_version_number
|
|
end
|
|
end
|
|
|
|
context "concurrent inserts" do
|
|
before :once do
|
|
assignment_model(course: @course)
|
|
@assignment.context.reload
|
|
|
|
@assignment.submissions.scope.delete_all
|
|
end
|
|
|
|
def concurrent_inserts
|
|
real_sub = @assignment.submissions.build(user: @user)
|
|
|
|
mock_submissions = Submission.none
|
|
allow(mock_submissions).to receive(:build).and_return(real_sub).once
|
|
allow(@assignment).to receive(:submissions).and_return(mock_submissions)
|
|
|
|
sub = nil
|
|
expect {
|
|
sub = yield(@assignment, @user)
|
|
}.not_to raise_error
|
|
|
|
expect(sub).not_to be_new_record
|
|
expect(sub).to eql real_sub
|
|
end
|
|
|
|
it "should handle them gracefully in find_or_create_submission" do
|
|
concurrent_inserts do |assignment, user|
|
|
assignment.find_or_create_submission(user)
|
|
end
|
|
end
|
|
|
|
it "should handle them gracefully in submit_homework" do
|
|
concurrent_inserts do |assignment, user|
|
|
assignment.submit_homework(user, :body => "test")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#peer_reviews_assigned" do
|
|
before :once do
|
|
@assignment = assignment_model(course: @course)
|
|
@assignment.peer_reviews = true
|
|
@assignment.automatic_peer_reviews = true
|
|
@assignment.due_at = 1.day.ago
|
|
@assignment.peer_reviews_assigned = true
|
|
@assignment.save!
|
|
end
|
|
|
|
it "is set to `true` when all peer reviews have been assigned" do
|
|
@assignment.assign_peer_reviews
|
|
expect(@assignment.peer_reviews_assigned).to be true
|
|
end
|
|
|
|
it "is set to `false` when the #assign_at time changes" do
|
|
@assignment.assign_peer_reviews
|
|
@assignment.peer_reviews_assign_at = 1.day.from_now
|
|
@assignment.save!
|
|
expect(@assignment.peer_reviews_assigned).to be false
|
|
end
|
|
end
|
|
|
|
describe "#peer_reviews_assign_at" do
|
|
it "is writeable" do
|
|
assignment_model(course: @course)
|
|
now = Time.zone.now
|
|
@assignment.peer_reviews_assign_at = now
|
|
expect(@assignment.peer_reviews_assign_at).to eq now
|
|
end
|
|
end
|
|
|
|
context "peer reviews" do
|
|
before :once do
|
|
assignment_model(course: @course)
|
|
end
|
|
|
|
context "basic assignment" do
|
|
before :once do
|
|
@users = create_users_in_course(@course, 10.times.map{ |i| {name: "user #{i}"} }, return_type: :record)
|
|
@a.reload
|
|
@submissions = @users.map do |u|
|
|
@a.submit_homework(u, :submission_type => "online_url", :url => "http://www.google.com")
|
|
end
|
|
end
|
|
|
|
it "should assign peer reviews" do
|
|
@a.peer_review_count = 1
|
|
res = @a.assign_peer_reviews
|
|
expect(res.length).to eql(@submissions.length)
|
|
@submissions.each do |s|
|
|
expect(res.map(&:asset)).to be_include(s)
|
|
expect(res.map(&:assessor_asset)).to be_include(s)
|
|
end
|
|
end
|
|
|
|
it "should not assign peer reviews to fake students" do
|
|
fake_student = @course.student_view_student
|
|
fake_sub = @a.submit_homework(fake_student, :submission_type => "online_url", :url => "http://www.google.com")
|
|
|
|
@a.peer_review_count = 1
|
|
res = @a.assign_peer_reviews
|
|
expect(res.length).to eql(@submissions.length)
|
|
expect(res.map(&:asset)).not_to be_include(fake_sub)
|
|
expect(res.map(&:assessor_asset)).not_to be_include(fake_sub)
|
|
end
|
|
|
|
it "should assign when already graded" do
|
|
@users.each do |u|
|
|
@a.grade_student(u, :grader => @teacher, :grade => '100')
|
|
end
|
|
@a.peer_review_count = 1
|
|
res = @a.assign_peer_reviews
|
|
expect(res.length).to eql(@submissions.length)
|
|
@submissions.each do |s|
|
|
expect(res.map{|a| a.asset}).to be_include(s)
|
|
expect(res.map{|a| a.assessor_asset}).to be_include(s)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '"auto-assign" scheduling' do
|
|
before :each do
|
|
@a.peer_reviews = true
|
|
@a.automatic_peer_reviews = true
|
|
@a.due_at = Time.zone.now
|
|
end
|
|
|
|
it "schedules a 'do_auto_peer_review' job when saved" do
|
|
expects_job_with_tag('Assignment#do_auto_peer_review', 1) {
|
|
@a.save!
|
|
}
|
|
end
|
|
|
|
it "schedules review assignment using the assignment due date" do
|
|
@a.peer_reviews_due_at = 1.day.from_now
|
|
@a.save!
|
|
job = Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last
|
|
expect(job.run_at).to eq @a.peer_reviews_due_at
|
|
end
|
|
|
|
it "re-schedules the existing job when the assignment due date changes" do
|
|
@a.peer_reviews_due_at = 1.day.from_now
|
|
@a.save!
|
|
job = Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last
|
|
@a.peer_reviews_due_at = 2.days.from_now
|
|
@a.save!
|
|
job.reload
|
|
expect(job.run_at).to eq @a.peer_reviews_due_at
|
|
end
|
|
|
|
it "does not schedule a job when #skip_schedule_peer_reviews is set" do
|
|
@a.skip_schedule_peer_reviews = true
|
|
|
|
expects_job_with_tag('Assignment#do_auto_peer_review', 0) {
|
|
@a.save!
|
|
}
|
|
end
|
|
end
|
|
|
|
it "should assign multiple peer reviews" do
|
|
@a.reload
|
|
@submissions = []
|
|
users = create_users_in_course(@course, 4.times.map{ |i| {name: "user #{i}"} }, return_type: :record)
|
|
users.each do |u|
|
|
@submissions << @a.submit_homework(u, :submission_type => "online_url", :url => "http://www.google.com")
|
|
end
|
|
@a.peer_review_count = 2
|
|
res = @a.assign_peer_reviews
|
|
expect(res.length).to eql(@submissions.length * @a.peer_review_count)
|
|
@submissions.each do |s|
|
|
assets = res.select{|a| a.asset == s}
|
|
expect(assets.length).to eql(@a.peer_review_count)
|
|
expect(assets.map{|a| a.assessor_id}.uniq.length).to eql(assets.length)
|
|
|
|
assessors = res.select{|a| a.assessor_asset == s}
|
|
expect(assessors.length).to eql(@a.peer_review_count)
|
|
expect(assessors.map(&:asset_id).uniq.length).to eq @a.peer_review_count
|
|
end
|
|
end
|
|
|
|
it "should assign late peer reviews" do
|
|
@submissions = []
|
|
users = create_users_in_course(@course, 5.times.map{ |i| {name: "user #{i}"} }, return_type: :record)
|
|
users.each do |u|
|
|
#@a.context.reload
|
|
@submissions << @a.submit_homework(u, :submission_type => "online_url", :url => "http://www.google.com")
|
|
end
|
|
@a.peer_review_count = 2
|
|
res = @a.assign_peer_reviews
|
|
expect(res.length).to eql(@submissions.length * 2)
|
|
user = create_users_in_course(@course, [{name: "new user"}], return_type: :record).first
|
|
@a.reload
|
|
s = @a.submit_homework(user, :submission_type => "online_url", :url => "http://www.google.com")
|
|
res = @a.assign_peer_reviews
|
|
expect(res.length).to be >= 2
|
|
expect(res.any?{|a| a.assessor_asset == s}).to eql(true)
|
|
end
|
|
|
|
it "should assign late peer reviews to each other if there is more than one" do
|
|
@a.reload
|
|
@submissions = []
|
|
users = create_users_in_course(@course, 10.times.map{ |i| {name: "user #{i}"} }, return_type: :record)
|
|
users.each do |u|
|
|
@submissions << @a.submit_homework(u, :submission_type => "online_url", :url => "http://www.google.com")
|
|
end
|
|
@a.peer_review_count = 2
|
|
res = @a.assign_peer_reviews
|
|
expect(res.length).to eql(@submissions.length * 2)
|
|
|
|
@late_submissions = []
|
|
users = create_users_in_course(@course, 3.times.map{ |i| {name: "user #{i}"} }, return_type: :record)
|
|
users.each do |u|
|
|
@late_submissions << @a.submit_homework(u, :submission_type => "online_url", :url => "http://www.google.com")
|
|
end
|
|
res = @a.assign_peer_reviews
|
|
expect(res.length).to be >= 6
|
|
end
|
|
|
|
it "should not assign out of group for graded group-discussions" do
|
|
# (as opposed to group assignments)
|
|
group_discussion_assignment
|
|
|
|
users = create_users_in_course(@course, 6.times.map{ |i| {name: "user #{i}"} }, return_type: :record)
|
|
[@group1, @group2].each do |group|
|
|
users.pop(3).each do |user|
|
|
group.add_user(user)
|
|
@topic.child_topic_for(user).reply_from(:user => user, :text => "entry from #{user.name}")
|
|
end
|
|
end
|
|
|
|
@assignment.reload
|
|
@assignment.peer_review_count = 2
|
|
@assignment.save!
|
|
requests = @assignment.assign_peer_reviews
|
|
expect(requests.count).to eq 12
|
|
requests.each do |req|
|
|
group = @group1.users.include?(req.user) ? @group1 : @group2
|
|
expect(group.users).to include(req.assessor)
|
|
end
|
|
end
|
|
|
|
context "intra group peer reviews" do
|
|
it "should not assign peer reviews to members of the same group when disabled" do
|
|
@submissions = []
|
|
gc = @course.group_categories.create! name: "Groupy McGroupface"
|
|
@a.update group_category_id: gc.id,
|
|
grade_group_students_individually: false
|
|
users = create_users_in_course(@course, 8.times.map{ |i| {name: "user #{i}"} }, return_type: :record)
|
|
["group_1", "group_2"].each do |group_name|
|
|
group = gc.groups.create! name: group_name, context: @course
|
|
users.pop(4).each{|user| group.add_user(user)}
|
|
end
|
|
|
|
@a.submit_homework(gc.groups[0].users.first, :submission_type => "online_url", :url => "http://www.google.com")
|
|
@a.peer_review_count = 3
|
|
|
|
res = @a.assign_peer_reviews
|
|
expect(res.length).to be 0
|
|
end
|
|
|
|
it "should assign peer reviews to members of the same group when enabled" do
|
|
@submissions = []
|
|
gc = @course.group_categories.create! name: "Groupy McGroupface"
|
|
@a.update group_category_id: gc.id,
|
|
grade_group_students_individually: false
|
|
users = create_users_in_course(@course, 8.times.map{ |i| {name: "user #{i}"} }, return_type: :record)
|
|
["group_1", "group_2"].each do |group_name|
|
|
group = gc.groups.create! name: group_name, context: @course
|
|
users.pop(4).each{|user| group.add_user(user)}
|
|
end
|
|
|
|
@a.submit_homework(gc.groups[0].users.first, :submission_type => "online_url", :url => "http://www.google.com")
|
|
@a.peer_review_count = 3
|
|
@a.intra_group_peer_reviews = true
|
|
res = @a.assign_peer_reviews
|
|
expect(res.length).to be 12
|
|
expect((res.map(&:user_id) - gc.groups[1].users.map(&:id)).length).to be res.length
|
|
end
|
|
end
|
|
|
|
context "when using assignment overrides and manual peer review assignment" do
|
|
before :once do
|
|
@assignment = assignment_model(
|
|
automatic_peer_reviews: false,
|
|
course: @course,
|
|
due_at: 1.day.from_now,
|
|
only_visible_to_overrides: false,
|
|
peer_review_count: 1,
|
|
peer_reviews: true,
|
|
submission_types: "online_url",
|
|
workflow_state: "published"
|
|
)
|
|
|
|
@section1 = @course.course_sections.create!(name: 'Section One')
|
|
@section2 = @course.course_sections.create!(name: 'Section Two')
|
|
|
|
@override_s1 = @assignment.assignment_overrides.build
|
|
@override_s1.set = @section1
|
|
@override_s1.due_at = 2.days.from_now
|
|
@override_s1.save!
|
|
end
|
|
|
|
context "when the assignment is assigned to everyone" do
|
|
before :once do
|
|
@student1, @student2, @student3, @student4 = create_users(4, return_type: :record)
|
|
student_in_section(@section1, user: @student1)
|
|
student_in_section(@section2, user: @student2)
|
|
student_in_section(@section1, user: @student3)
|
|
student_in_section(@section2, user: @student4)
|
|
|
|
# Submit homework for review. Only submitted homework is considered for review.
|
|
@assignment.submit_homework(@student1, submission_type: 'online_url', url: 'http://www.google.com')
|
|
@assignment.submit_homework(@student2, submission_type: 'online_url', url: 'http://www.google.com')
|
|
@assignment.submit_homework(@student3, submission_type: 'online_url', url: 'http://www.google.com')
|
|
@assignment.submit_homework(@student4, submission_type: 'online_url', url: 'http://www.google.com')
|
|
end
|
|
|
|
it "assigns every student a peer for review" do
|
|
assessment_requests = @assignment.assign_peer_reviews
|
|
expect(assessment_requests.map(&:assessor_id)).to contain_exactly(
|
|
@student1.id,
|
|
@student2.id,
|
|
@student3.id,
|
|
@student4.id
|
|
)
|
|
end
|
|
|
|
it "assigns every student as a peer to review" do
|
|
assessment_requests = @assignment.assign_peer_reviews
|
|
expect(assessment_requests.map(&:user_id)).to contain_exactly(
|
|
@student1.id,
|
|
@student2.id,
|
|
@student3.id,
|
|
@student4.id
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when the assignment is assigned only to some students" do
|
|
before :once do
|
|
@assignment.only_visible_to_overrides = true
|
|
@assignment.save!
|
|
|
|
@student1, @student2, @student3, @student4 = create_users(4, return_type: :record)
|
|
student_in_section(@section1, user: @student1)
|
|
student_in_section(@section2, user: @student2)
|
|
student_in_section(@section1, user: @student3)
|
|
student_in_section(@section2, user: @student4)
|
|
|
|
# Submit homework for review. Only submitted homework is considered for review.
|
|
@assignment.submit_homework(@student1, submission_type: 'online_url', url: 'http://www.google.com')
|
|
@assignment.submit_homework(@student3, submission_type: 'online_url', url: 'http://www.google.com')
|
|
end
|
|
|
|
it "assigns only assigned students a peer for review" do
|
|
assessment_requests = @assignment.assign_peer_reviews
|
|
expect(assessment_requests.map(&:assessor_id)).to contain_exactly(
|
|
@student1.id,
|
|
@student3.id
|
|
)
|
|
end
|
|
|
|
it "assigns only assigned students as a peer to review" do
|
|
assessment_requests = @assignment.assign_peer_reviews
|
|
expect(assessment_requests.map(&:user_id)).to contain_exactly(
|
|
@student1.id,
|
|
@student3.id
|
|
)
|
|
end
|
|
end
|
|
|
|
it "does not assign peer reviews when not enough students have submitted" do
|
|
@student1, @student2 = create_users(2, return_type: :record)
|
|
student_in_section(@section1, user: @student1)
|
|
student_in_section(@section2, user: @student2)
|
|
|
|
# Submit homework for review. Only submitted homework is considered for review.
|
|
@assignment.submit_homework(@student1, submission_type: 'online_url', url: 'http://www.google.com')
|
|
|
|
assessment_requests = @assignment.assign_peer_reviews
|
|
expect(assessment_requests).to be_empty
|
|
end
|
|
end
|
|
|
|
describe '"auto-assign" scheduling with assignment overrides' do
|
|
before :once do
|
|
@assignment = assignment_model(
|
|
automatic_peer_reviews: false,
|
|
course: @course,
|
|
only_visible_to_overrides: false,
|
|
peer_review_count: 1,
|
|
peer_reviews: true,
|
|
submission_types: "online_url",
|
|
workflow_state: "published"
|
|
)
|
|
|
|
@assignment.automatic_peer_reviews = true
|
|
|
|
@section1 = @course.course_sections.create!(name: 'Section One')
|
|
@section2 = @course.course_sections.create!(name: 'Section Two')
|
|
|
|
@override_s1 = @assignment.assignment_overrides.create!(
|
|
due_at: 2.days.from_now,
|
|
due_at_overridden: true,
|
|
set: @section1
|
|
)
|
|
|
|
@student1, @student2, @student3, @student4 = create_users(4, return_type: :record)
|
|
student_in_section(@section1, user: @student1)
|
|
student_in_section(@section2, user: @student2)
|
|
student_in_section(@section1, user: @student3)
|
|
student_in_section(@section2, user: @student4)
|
|
|
|
# Submit homework for review. Only submitted homework is considered for review.
|
|
@sub1 = @assignment.submit_homework(@student1, submission_type: 'online_url', url: 'http://www.google.com')
|
|
@sub2 = @assignment.submit_homework(@student2, submission_type: 'online_url', url: 'http://www.google.com')
|
|
@sub3 = @assignment.submit_homework(@student3, submission_type: 'online_url', url: 'http://www.google.com')
|
|
@sub4 = @assignment.submit_homework(@student4, submission_type: 'online_url', url: 'http://www.google.com')
|
|
end
|
|
|
|
context "when reviews are automatically assigned using the 'assign_at' date" do
|
|
before :each do
|
|
@assignment.due_at = 1.day.from_now
|
|
@assignment.peer_reviews_assign_at = 1.day.from_now
|
|
end
|
|
|
|
it "schedules a 'do_auto_peer_review' job when saved" do
|
|
expects_job_with_tag('Assignment#do_auto_peer_review', 1) {
|
|
@assignment.save!
|
|
}
|
|
end
|
|
|
|
it "schedules the job using the 'assign_at' date" do
|
|
@assignment.save!
|
|
job = Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last
|
|
expect(job.run_at).to eq @assignment.peer_reviews_assign_at
|
|
end
|
|
|
|
context "when the job runs" do
|
|
it "assigns peer reviews to all students" do
|
|
@assignment.save!
|
|
job = Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last
|
|
Timecop.freeze(1.day.from_now) do
|
|
job.invoke_job
|
|
# assessment_requests = @assignment.assign_peer_reviews
|
|
expect(AssessmentRequest.count).to be(4)
|
|
end
|
|
end
|
|
|
|
it "does not schedule another job" do
|
|
@assignment.save!
|
|
job = Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last
|
|
expect do
|
|
Timecop.freeze(1.day.from_now) do
|
|
job.invoke_job
|
|
end
|
|
end.not_to change { Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last.run_at }
|
|
end
|
|
|
|
it "sets #peer_reviews_assigned to `true`" do
|
|
@assignment.save!
|
|
job = Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last
|
|
Timecop.freeze(1.day.from_now) do
|
|
job.invoke_job
|
|
expect(@assignment.reload.peer_reviews_assigned).to be(true)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when reviews are assigned using due dates" do
|
|
before :each do
|
|
@assignment.due_at = 1.day.from_now
|
|
@assignment.peer_reviews_assign_at = nil
|
|
end
|
|
|
|
# schedules a 'do_auto_peer_review' job for the earliest
|
|
# assigns all reviews when the 'assign_at' date has passed
|
|
# does not schedule additional jobs
|
|
# sets #peer_reviews_assigned to `true`
|
|
|
|
it "schedules a 'do_auto_peer_review' job when saved" do
|
|
expects_job_with_tag('Assignment#do_auto_peer_review', 1) {
|
|
@assignment.save!
|
|
}
|
|
end
|
|
|
|
context "when the 'due_at' date of the assignment is the earliest due date" do
|
|
it "schedules the job using the 'due_at' date of the assignment" do
|
|
@assignment.save!
|
|
job = Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last
|
|
expect(job.run_at).to eq @assignment.due_at
|
|
end
|
|
|
|
it "assigns peer reviews to all students assigned directly to the assignment" do
|
|
@assignment.save!
|
|
job = Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last
|
|
Timecop.freeze(1.day.from_now) do
|
|
job.invoke_job
|
|
assessment_requests = AssessmentRequest.all.to_a
|
|
expect(assessment_requests.map(&:assessor_id)).to contain_exactly(
|
|
@student2.id,
|
|
@student4.id
|
|
)
|
|
end
|
|
end
|
|
|
|
it "schedules another job" do
|
|
@assignment.save!
|
|
job = Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last
|
|
expect do
|
|
Timecop.freeze(1.day.from_now) do
|
|
job.invoke_job
|
|
end
|
|
end.to change { Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last.run_at }
|
|
end
|
|
|
|
it "uses the next due date when scheduling the next job" do
|
|
@assignment.save!
|
|
job = Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last
|
|
Timecop.freeze(1.day.from_now) do
|
|
job.invoke_job
|
|
end
|
|
expect(job.reload.run_at).to eq @override_s1.due_at
|
|
end
|
|
|
|
it "sets #peer_reviews_assigned to `false`" do
|
|
@assignment.save!
|
|
job = Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last
|
|
Timecop.freeze(1.day.from_now) do
|
|
job.invoke_job
|
|
end
|
|
expect(@assignment.reload.peer_reviews_assigned).to be(false)
|
|
end
|
|
end
|
|
|
|
context "when the 'due_at' date of an override is the earliest due date" do
|
|
before :each do
|
|
@assignment.due_at = 3.days.from_now
|
|
end
|
|
|
|
it "schedules the job using the 'due_at' date of the override" do
|
|
@assignment.save!
|
|
job = Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last
|
|
expect(job.run_at).to eq @override_s1.due_at
|
|
end
|
|
|
|
it "assigns peer reviews to all students assigned with the override" do
|
|
@assignment.save!
|
|
job = Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last
|
|
Timecop.freeze(2.days.from_now) do
|
|
job.invoke_job
|
|
assessment_requests = AssessmentRequest.all.to_a
|
|
expect(assessment_requests.map(&:assessor_id)).to contain_exactly(
|
|
@student1.id,
|
|
@student3.id
|
|
)
|
|
end
|
|
end
|
|
|
|
it "schedules another job" do
|
|
@assignment.save!
|
|
job = Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last
|
|
expect do
|
|
Timecop.freeze(2.days.from_now) do
|
|
job.invoke_job
|
|
end
|
|
end.to change { Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last.run_at }
|
|
end
|
|
|
|
it "uses the next due date when scheduling the next job" do
|
|
@assignment.save!
|
|
job = Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last
|
|
Timecop.freeze(2.days.from_now) do
|
|
job.invoke_job
|
|
end
|
|
job = Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last
|
|
expect(job.run_at).to eq @assignment.due_at
|
|
end
|
|
|
|
it "sets #peer_reviews_assigned to `false`" do
|
|
@assignment.save!
|
|
job = Delayed::Job.where(tag: "Assignment#do_auto_peer_review").last
|
|
Timecop.freeze(2.days.from_now) do
|
|
job.invoke_job
|
|
end
|
|
expect(@assignment.reload.peer_reviews_assigned).to be(false)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context "grading scales" do
|
|
before :once do
|
|
setup_assignment_without_submission
|
|
end
|
|
|
|
context "letter grades" do
|
|
before :once do
|
|
@assignment.update(:grading_type => 'letter_grade', :points_possible => 20)
|
|
end
|
|
|
|
it "should update grades when assignment changes" do
|
|
@enrollment = @student.enrollments.first
|
|
@assignment.reload
|
|
@sub = @assignment.grade_student(@student, :grader => @teacher, :grade => 'C').first
|
|
expect(@sub.grade).to eql('C')
|
|
expect(@sub.score).to eql(15.2)
|
|
expect(@enrollment.reload.computed_current_score).to eq 76
|
|
|
|
@assignment.points_possible = 30
|
|
@assignment.save!
|
|
@sub.reload
|
|
expect(@sub.score).to eql(15.2)
|
|
expect(@sub.grade).to eql('F')
|
|
expect(@enrollment.reload.computed_current_score).to eq 50.67
|
|
end
|
|
|
|
it "should accept lowercase letter grades" do
|
|
@assignment.reload
|
|
@sub = @assignment.grade_student(@student, :grader => @teacher, :grade => 'c').first
|
|
expect(@sub.grade).to eql('C')
|
|
expect(@sub.score).to eql(15.2)
|
|
end
|
|
end
|
|
|
|
context "gpa scale grades" do
|
|
before :once do
|
|
@assignment.update(:grading_type => 'gpa_scale', :points_possible => 20)
|
|
@course.grading_standards.build({title: "GPA"})
|
|
gs = @course.grading_standards.last
|
|
gs.data = {"4.0" => 0.94,
|
|
"3.7" => 0.90,
|
|
"3.3" => 0.87,
|
|
"3.0" => 0.84,
|
|
"2.7" => 0.80,
|
|
"2.3" => 0.77,
|
|
"2.0" => 0.74,
|
|
"1.7" => 0.70,
|
|
"1.3" => 0.67,
|
|
"1.0" => 0.64,
|
|
"0" => 0.01,
|
|
"M" => 0.0 }
|
|
gs.assignments << @a
|
|
gs.save!
|
|
end
|
|
|
|
it "should update grades when assignment changes" do
|
|
@enrollment = @student.enrollments.first
|
|
@assignment.reload
|
|
@sub = @assignment.grade_student(@student, :grader => @teacher, :grade => '2.0').first
|
|
expect(@sub.grade).to eql('2.0')
|
|
expect(@sub.score).to eql(15.2)
|
|
expect(@enrollment.reload.computed_current_score).to eq 76
|
|
|
|
@assignment.points_possible = 30
|
|
@assignment.save!
|
|
@sub.reload
|
|
expect(@sub.score).to eql(15.2)
|
|
expect(@sub.grade).to eql('0')
|
|
expect(@enrollment.reload.computed_current_score).to eq 50.67
|
|
end
|
|
|
|
it "should accept lowercase gpa grades" do
|
|
@assignment.reload
|
|
@sub = @assignment.grade_student(@student, :grader => @teacher, :grade => 'm').first
|
|
expect(@sub.grade).to eql('M')
|
|
expect(@sub.score).to eql(0.0)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#grants_right?" do
|
|
before(:once) do
|
|
assignment_model(course: @course)
|
|
@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.reload
|
|
|
|
@grading_period_group.grading_periods.create!({
|
|
title: "Closed Grading Period",
|
|
start_date: 5.weeks.ago,
|
|
end_date: 3.weeks.ago,
|
|
close_date: 1.week.ago
|
|
})
|
|
@grading_period_group.grading_periods.create!({
|
|
title: "Open Grading Period",
|
|
start_date: 3.weeks.ago,
|
|
end_date: 1.week.ago,
|
|
close_date: 1.week.from_now
|
|
})
|
|
end
|
|
|
|
context "to attach submission comment files" do
|
|
it 'is true when a student can read an assignment but the assignment is locked' do
|
|
@assignment.lock_at = 1.week.ago
|
|
@assignment.submission_types = 'online_upload'
|
|
@assignment.save!
|
|
expect(@assignment.grants_right?(@student, :attach_submission_comment_files)).to be true
|
|
end
|
|
end
|
|
|
|
context "to delete" do
|
|
context "when there are no grading periods" do
|
|
it "is true for admins" do
|
|
allow(@course).to receive(:grading_periods?).and_return false
|
|
expect(@assignment.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.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.reload.grants_right?(@admin, :delete)).to eql(true)
|
|
end
|
|
|
|
it "is false for teachers" do
|
|
expect(@assignment.reload.grants_right?(@teacher, :delete)).to eql(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.reload.grants_right?(@admin, :delete)).to eql(true)
|
|
end
|
|
|
|
it "is true for teachers" do
|
|
expect(@assignment.reload.grants_right?(@teacher, :delete)).to eql(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.reload.grants_right?(@admin, :delete)).to eql(true)
|
|
end
|
|
|
|
it "is true for teachers" do
|
|
expect(@assignment.reload.grants_right?(@teacher, :delete)).to eql(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.reload.grants_right?(@admin, :delete)).to eql(true)
|
|
end
|
|
|
|
it "is true for teachers" do
|
|
expect(@assignment.reload.grants_right?(@teacher, :delete)).to eql(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.reload.grants_right?(@admin, :delete)).to eql(true)
|
|
end
|
|
|
|
it "is true for teachers" do
|
|
expect(@assignment.reload.grants_right?(@teacher, :delete)).to eql(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.reload.grants_right?(@admin, :delete)).to eql(true)
|
|
end
|
|
|
|
it "is false for teachers" do
|
|
expect(@assignment.reload.grants_right?(@teacher, :delete)).to eql(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.reload.grants_right?(@admin, :delete)).to eql(true)
|
|
end
|
|
|
|
it "is true for teachers" do
|
|
expect(@assignment.reload.grants_right?(@teacher, :delete)).to eql(true)
|
|
end
|
|
end
|
|
|
|
context "when the assignment has a deleted override 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!
|
|
override.destroy
|
|
end
|
|
|
|
it "is true for admins" do
|
|
expect(@assignment.reload.grants_right?(@admin, :delete)).to eql(true)
|
|
end
|
|
|
|
it "is true for teachers" do
|
|
expect(@assignment.reload.grants_right?(@teacher, :delete)).to eql(true)
|
|
end
|
|
end
|
|
|
|
context "when the assignment is overridden with no due date and is only visible to overrides" do
|
|
before(:once) do
|
|
@assignment.update(due_at: 4.weeks.ago, only_visible_to_overrides: true)
|
|
override = @assignment.assignment_overrides.build
|
|
override.set = @course.default_section
|
|
override.save!
|
|
end
|
|
|
|
it "is true for admins" do
|
|
expect(@assignment.reload.grants_right?(@admin, :delete)).to eql(true)
|
|
end
|
|
|
|
it "is false for teachers" do
|
|
# since the override does not have the due date overridden, we fall
|
|
# back to using the assignment's due_at, which falls in a closed grading period
|
|
expect(@assignment.reload.grants_right?(@teacher, :delete)).to eql(false)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "to update" do
|
|
before(:each) do
|
|
@course.enable_feature!(:moderated_grading)
|
|
|
|
@ta = ta_in_course(course: @course, active_all: true).user
|
|
@moderator = teacher_in_course(course: @course, active_all: true).user
|
|
|
|
@moderated_assignment = @course.assignments.create!(
|
|
moderated_grading: true,
|
|
grader_count: 3,
|
|
final_grader: @moderator
|
|
)
|
|
end
|
|
|
|
it "allows the designated moderator to update a moderated assignment" do
|
|
expect(@moderated_assignment.grants_right?(@moderator, :update)).to eq(true)
|
|
end
|
|
|
|
it "allows non-moderators with Select Final Grade permission to update a moderated assignment" do
|
|
expect(@moderated_assignment.grants_right?(@ta, :update)).to eq(true)
|
|
end
|
|
|
|
it "allows an admin to update a moderated assignment" do
|
|
expect(@moderated_assignment.grants_right?(@admin, :update)).to eq(true)
|
|
end
|
|
|
|
it "does not allow users without Select Final Grade permission to update a moderated assignment" do
|
|
@course.account.role_overrides.create!(permission: :select_final_grade, role: ta_role, enabled: false)
|
|
expect(@moderated_assignment.grants_right?(@ta, :update)).to be false
|
|
end
|
|
|
|
it "allows an instructor to update a moderated assignment with no moderator selected" do
|
|
@course.account.role_overrides.create!(permission: :select_final_grade, role: ta_role, enabled: false)
|
|
@moderated_assignment.update!(final_grader: nil)
|
|
expect(@moderated_assignment.grants_right?(@ta, :update)).to eq(true)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "as_json" do
|
|
before :once do
|
|
assignment_model(course: @course)
|
|
end
|
|
|
|
it "should include permissions if specified" do
|
|
expect(@assignment.to_json).not_to match(/permissions/)
|
|
expect(@assignment.to_json(:permissions => {:user => nil})).to match(/\"permissions\"\s*:\s*\{/)
|
|
expect(@assignment.grants_right?(@teacher, :create)).to eql(true)
|
|
expect(@assignment.to_json(:permissions => {:user => @teacher, :session => nil})).to match(/\"permissions\"\s*:\s*\{\"/)
|
|
hash = @assignment.as_json(:permissions => {:user => @teacher, :session => nil})
|
|
expect(hash["assignment"]).not_to be_nil
|
|
expect(hash["assignment"]["permissions"]).not_to be_nil
|
|
expect(hash["assignment"]["permissions"]).not_to be_empty
|
|
expect(hash["assignment"]["permissions"]["read"]).to eql(true)
|
|
end
|
|
|
|
it "should serialize with roots included in nested elements" do
|
|
@course.assignments.create!(:title => "some assignment")
|
|
hash = @course.as_json(:include => :assignments)
|
|
expect(hash["course"]).not_to be_nil
|
|
expect(hash["course"]["assignments"]).not_to be_empty
|
|
expect(hash["course"]["assignments"][0]).not_to be_nil
|
|
expect(hash["course"]["assignments"][0]["assignment"]).not_to be_nil
|
|
end
|
|
|
|
it "should serialize with permissions" do
|
|
hash = @course.as_json(:permissions => {:user => @teacher, :session => nil} )
|
|
expect(hash["course"]).not_to be_nil
|
|
expect(hash["course"]["permissions"]).not_to be_nil
|
|
expect(hash["course"]["permissions"]).not_to be_empty
|
|
expect(hash["course"]["permissions"]["read"]).to eql(true)
|
|
end
|
|
|
|
it "should exclude root" do
|
|
hash = @course.as_json(:include_root => false, :permissions => {:user => @teacher, :session => nil} )
|
|
expect(hash["course"]).to be_nil
|
|
expect(hash["name"]).to eql(@course.name)
|
|
expect(hash["permissions"]).not_to be_nil
|
|
expect(hash["permissions"]).not_to be_empty
|
|
expect(hash["permissions"]["read"]).to eql(true)
|
|
end
|
|
|
|
it "should include group_category" do
|
|
assignment_model(:group_category => "Something", :course => @course)
|
|
hash = @assignment.as_json
|
|
expect(hash["assignment"]["group_category"]).to eq "Something"
|
|
end
|
|
end
|
|
|
|
context "ical" do
|
|
it ".to_ics should not fail for null due dates" do
|
|
assignment_model(:due_at => "", :course => @course)
|
|
res = @assignment.to_ics
|
|
expect(res).not_to be_nil
|
|
expect(res.match(/DTSTART/)).to be_nil
|
|
end
|
|
|
|
it ".to_ics should not return data for null due dates" do
|
|
assignment_model(:due_at => "", :course => @course)
|
|
res = @assignment.to_ics(in_own_calendar: false)
|
|
expect(res).to be_nil
|
|
end
|
|
|
|
it ".to_ics should return string data for assignments with due dates" do
|
|
Time.zone = 'UTC'
|
|
assignment_model(:due_at => "Sep 3 2008 11:55am", :course => @course)
|
|
# force known value so we can check serialization
|
|
@assignment.updated_at = Time.at(1220443500) # 3 Sep 2008 12:05pm (UTC)
|
|
res = @assignment.to_ics
|
|
expect(res).not_to be_nil
|
|
expect(res.match(/DTEND:20080903T115500Z/)).not_to be_nil
|
|
expect(res.match(/DTSTART:20080903T115500Z/)).not_to be_nil
|
|
expect(res.match(/DTSTAMP:20080903T120500Z/)).not_to be_nil
|
|
end
|
|
|
|
it ".to_ics should return correct dates even with different time_zone_edited" do
|
|
Time.zone = 'UTC'
|
|
assignment_model(:due_at => "Sep 3 2008 11:55am", :course => @course, :time_zone_edited => 'EST')
|
|
# force known value so we can check serialization
|
|
@assignment.updated_at = Time.at(1220443500) # 3 Sep 2008 12:05pm (UTC)
|
|
res = @assignment.to_ics
|
|
expect(res).not_to be_nil
|
|
expect(res.match(/DTEND:20080903T115500Z/)).not_to be_nil
|
|
expect(res.match(/DTSTART:20080903T115500Z/)).not_to be_nil
|
|
expect(res.match(/DTSTAMP:20080903T120500Z/)).not_to be_nil
|
|
end
|
|
|
|
it ".to_ics should return correct dates even with different timezone on call midnight" do
|
|
Time.zone = 'UTC'
|
|
assignment_model(:due_at => "Sep 3 2008 11:59pm", :course => @course, :time_zone_edited => 'EST')
|
|
# force known value so we can check serialization
|
|
@assignment.updated_at = Time.at(1220443500) # 3 Sep 2008 12:05pm (UTC)
|
|
Time.zone = 'HST'
|
|
res = @assignment.to_ics
|
|
expect(res).not_to be_nil
|
|
expect(res.match(/DTEND:20080903T235900Z/)).not_to be_nil
|
|
expect(res.match(/DTSTART:20080903T235900Z/)).not_to be_nil
|
|
expect(res.match(/DTSTAMP:20080903T120500Z/)).not_to be_nil
|
|
end
|
|
|
|
it ".to_ics should return string data for assignments with due dates in correct tz" do
|
|
Time.zone = 'Alaska' # -0800
|
|
assignment_model(:due_at => "Sep 3 2008 11:55am", :course => @course)
|
|
# force known value so we can check serialization
|
|
@assignment.updated_at = Time.at(1220472300) # 3 Sep 2008 12:05pm (AKDT)
|
|
res = @assignment.to_ics
|
|
expect(res).not_to be_nil
|
|
expect(res.match(/DTEND:20080903T195500Z/)).not_to be_nil
|
|
expect(res.match(/DTSTART:20080903T195500Z/)).not_to be_nil
|
|
expect(res.match(/DTSTAMP:20080903T200500Z/)).not_to be_nil
|
|
end
|
|
|
|
it ".to_ics should return data for assignments with due dates" do
|
|
Time.zone = 'UTC'
|
|
assignment_model(:due_at => "Sep 3 2008 11:55am", :course => @course)
|
|
# force known value so we can check serialization
|
|
@assignment.updated_at = Time.at(1220443500) # 3 Sep 2008 12:05pm (UTC)
|
|
res = @assignment.to_ics(in_own_calendar: false)
|
|
expect(res).not_to be_nil
|
|
expect(res.dtstart.tz_utc).to eq true
|
|
expect(res.dtstart.strftime('%Y-%m-%dT%H:%M:%S')).to eq Time.zone.parse("Sep 3 2008 11:55am").in_time_zone('UTC').strftime('%Y-%m-%dT%H:%M:00')
|
|
expect(res.dtend.tz_utc).to eq true
|
|
expect(res.dtend.strftime('%Y-%m-%dT%H:%M:%S')).to eq Time.zone.parse("Sep 3 2008 11:55am").in_time_zone('UTC').strftime('%Y-%m-%dT%H:%M:00')
|
|
expect(res.dtstamp.tz_utc).to eq true
|
|
expect(res.dtstamp.strftime('%Y-%m-%dT%H:%M:%S')).to eq Time.zone.parse("Sep 3 2008 12:05pm").in_time_zone('UTC').strftime('%Y-%m-%dT%H:%M:00')
|
|
end
|
|
|
|
it ".to_ics should return data for assignments with due dates in correct tz" do
|
|
Time.zone = 'Alaska' # -0800
|
|
assignment_model(:due_at => "Sep 3 2008 11:55am", :course => @course)
|
|
# force known value so we can check serialization
|
|
@assignment.updated_at = Time.at(1220472300) # 3 Sep 2008 12:05pm (AKDT)
|
|
res = @assignment.to_ics(in_own_calendar: false)
|
|
expect(res).not_to be_nil
|
|
expect(res.dtstart.tz_utc).to eq true
|
|
expect(res.dtstart.strftime('%Y-%m-%dT%H:%M:%S')).to eq Time.zone.parse("Sep 3 2008 11:55am").in_time_zone('UTC').strftime('%Y-%m-%dT%H:%M:00')
|
|
expect(res.dtend.tz_utc).to eq true
|
|
expect(res.dtend.strftime('%Y-%m-%dT%H:%M:%S')).to eq Time.zone.parse("Sep 3 2008 11:55am").in_time_zone('UTC').strftime('%Y-%m-%dT%H:%M:00')
|
|
expect(res.dtstamp.tz_utc).to eq true
|
|
expect(res.dtstamp.strftime('%Y-%m-%dT%H:%M:%S')).to eq Time.zone.parse("Sep 3 2008 12:05pm").in_time_zone('UTC').strftime('%Y-%m-%dT%H:%M:00')
|
|
end
|
|
|
|
it ".to_ics should return string dates for all_day events" do
|
|
Time.zone = 'UTC'
|
|
assignment_model(:due_at => "Sep 3 2008 11:59pm", :course => @course)
|
|
expect(@assignment.all_day).to eql(true)
|
|
res = @assignment.to_ics
|
|
expect(res.match(/DTSTART;VALUE=DATE:20080903/)).not_to be_nil
|
|
expect(res.match(/DTEND;VALUE=DATE:20080903/)).not_to be_nil
|
|
end
|
|
|
|
it ".to_ics should populate uid and summary fields" do
|
|
Time.zone = 'UTC'
|
|
assignment_model(:due_at => "Sep 3 2008 11:55am", :title => "assignment title", :course => @course)
|
|
ev = @a.to_ics(in_own_calendar: false)
|
|
expect(ev.uid).to eq "event-assignment-#{@a.id}"
|
|
expect(ev.summary).to eq "#{@a.title} [#{@a.context.course_code}]"
|
|
# TODO: ev.url.should == ?
|
|
end
|
|
|
|
it ".to_ics should apply due_at override information" do
|
|
Time.zone = 'UTC'
|
|
assignment_model(:due_at => "Sep 3 2008 11:55am", :title => "assignment title", :course => @course)
|
|
@override = @a.assignment_overrides.build
|
|
@override.set = @course.default_section
|
|
@override.override_due_at(Time.zone.parse("Sep 28 2008 11:55am"))
|
|
@override.save!
|
|
|
|
assignment = AssignmentOverrideApplicator.assignment_with_overrides(@a, [@override])
|
|
ev = assignment.to_ics(in_own_calendar: false)
|
|
expect(ev.uid).to eq "event-assignment-override-#{@override.id}"
|
|
expect(ev.summary).to eq "#{@a.title} (#{@override.title}) [#{assignment.context.course_code}]"
|
|
#TODO: ev.url.should == ?
|
|
end
|
|
|
|
it ".to_ics should not apply non-due_at override information" do
|
|
Time.zone = 'UTC'
|
|
assignment_model(:due_at => "Sep 3 2008 11:55am", :title => "assignment title", :course => @course)
|
|
@override = @a.assignment_overrides.build
|
|
@override.set = @course.default_section
|
|
@override.override_lock_at(Time.zone.parse("Sep 28 2008 11:55am"))
|
|
@override.save!
|
|
|
|
assignment = AssignmentOverrideApplicator.assignment_with_overrides(@a, [@override])
|
|
ev = assignment.to_ics(in_own_calendar: false)
|
|
expect(ev.uid).to eq "event-assignment-#{@a.id}"
|
|
expect(ev.summary).to eq "#{@a.title} [#{@a.context.course_code}]"
|
|
end
|
|
end
|
|
|
|
context "quizzes" do
|
|
before :once do
|
|
assignment_model(:submission_types => "online_quiz", :course => @course)
|
|
end
|
|
|
|
it "should create a quiz if none exists and specified" do
|
|
@a.reload
|
|
expect(@a.submission_types).to eql('online_quiz')
|
|
expect(@a.quiz).not_to be_nil
|
|
expect(@a.quiz.assignment_id).to eql(@a.id)
|
|
@a.due_at = Time.now
|
|
@a.save
|
|
@a.reload
|
|
expect(@a.quiz).not_to be_nil
|
|
expect(@a.quiz.assignment_id).to eql(@a.id)
|
|
end
|
|
|
|
it "should delete a quiz if no longer specified" do
|
|
@a.reload
|
|
expect(@a.submission_types).to eql('online_quiz')
|
|
expect(@a.quiz).not_to be_nil
|
|
expect(@a.quiz.assignment_id).to eql(@a.id)
|
|
@a.submission_types = 'on_paper'
|
|
@a.save!
|
|
@a.reload
|
|
expect(@a.quiz).to be_nil
|
|
end
|
|
|
|
it "should not delete the assignment when unlinked from a quiz" do
|
|
@a.reload
|
|
expect(@a.submission_types).to eql('online_quiz')
|
|
@quiz = @a.quiz
|
|
@quiz.unpublish!
|
|
expect(@quiz).not_to be_nil
|
|
expect(@quiz.state).to eql(:unpublished)
|
|
expect(@quiz.assignment_id).to eql(@a.id)
|
|
@a.submission_types = 'on_paper'
|
|
@a.save!
|
|
@quiz = Quizzes::Quiz.find(@quiz.id)
|
|
expect(@quiz.assignment_id).to eql(nil)
|
|
expect(@quiz.state).to eql(:deleted)
|
|
@a.reload
|
|
expect(@a.quiz).to be_nil
|
|
expect(@a.state).to eql(:unpublished)
|
|
end
|
|
|
|
it "should not delete the quiz if non-empty when unlinked" do
|
|
@a.reload
|
|
expect(@a.submission_types).to eql('online_quiz')
|
|
@quiz = @a.quiz
|
|
expect(@quiz).not_to be_nil
|
|
expect(@quiz.assignment_id).to eql(@a.id)
|
|
@quiz.quiz_questions.create!()
|
|
@quiz.generate_quiz_data
|
|
@quiz.save!
|
|
@a.quiz.reload
|
|
expect(@quiz.root_entries).not_to be_empty
|
|
@a.submission_types = 'on_paper'
|
|
@a.save!
|
|
@a.reload
|
|
expect(@a.quiz).to be_nil
|
|
expect(@a.state).to eql(:published)
|
|
@quiz = Quizzes::Quiz.find(@quiz.id)
|
|
expect(@quiz.assignment_id).to eql(nil)
|
|
expect(@quiz.state).to eql(:available)
|
|
end
|
|
|
|
it "should grab the original quiz if unlinked and relinked" do
|
|
@a.reload
|
|
expect(@a.submission_types).to eql('online_quiz')
|
|
@quiz = @a.quiz
|
|
expect(@quiz).not_to be_nil
|
|
expect(@quiz.assignment_id).to eql(@a.id)
|
|
@a.quiz.reload
|
|
@a.submission_types = 'on_paper'
|
|
@a.save!
|
|
@a.submission_types = 'online_quiz'
|
|
@a.save!
|
|
@a.reload
|
|
expect(@a.quiz).to eql(@quiz)
|
|
expect(@a.state).to eql(:published)
|
|
@quiz.reload
|
|
expect(@quiz.state).to eql(:available)
|
|
end
|
|
|
|
it "updates the draft state of its associated quiz" do
|
|
@a.reload
|
|
@a.publish
|
|
@a.save!
|
|
expect(@a.quiz.reload).to be_published
|
|
@a.unpublish
|
|
expect(@a.quiz.reload).not_to be_published
|
|
end
|
|
|
|
context "#quiz?" do
|
|
it "knows that it is a quiz" do
|
|
@a.reload
|
|
expect(@a.quiz?).to be true
|
|
end
|
|
|
|
it "knows that an assignment is not a quiz" do
|
|
@a.reload
|
|
@a.quiz = nil
|
|
@a.submission_types = 'postal_delivery_of_an_elephant'
|
|
expect(@a.quiz?).to be false
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#quiz_lti?" do
|
|
before :once do
|
|
assignment_model(:submission_types => "external_tool", :course => @course)
|
|
end
|
|
|
|
context "when quizzes 2 external tool not present" do
|
|
it "returns false" do
|
|
expect(@a.quiz_lti?).to be false
|
|
end
|
|
end
|
|
|
|
context "when quizzes 2 external tool is present" do
|
|
before do
|
|
tool = @c.context_external_tools.create!(
|
|
:name => 'Quizzes.Next',
|
|
:consumer_key => 'test_key',
|
|
:shared_secret => 'test_secret',
|
|
:tool_id => 'Quizzes 2',
|
|
:url => 'http://example.com/launch'
|
|
)
|
|
@a.external_tool_tag_attributes = { :content => tool }
|
|
end
|
|
|
|
it "returns true" do
|
|
expect(@a.quiz_lti?).to be true
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#quiz_lti!" do
|
|
before :once do
|
|
assignment_model(:submission_types => "online_quiz", :course => @course)
|
|
tool = @c.context_external_tools.create!(
|
|
:name => 'Quizzes.Next',
|
|
:consumer_key => 'test_key',
|
|
:shared_secret => 'test_secret',
|
|
:tool_id => 'Quizzes 2',
|
|
:url => 'http://example.com/launch'
|
|
)
|
|
@a.external_tool_tag_attributes = { :content => tool }
|
|
end
|
|
|
|
it "changes submission_types and break assignment's tie to quiz" do
|
|
expect(@a.reload.quiz).not_to be nil
|
|
expect(@a.submission_types).to eq 'online_quiz'
|
|
@a.quiz_lti! && @a.save!
|
|
expect(@a.reload.quiz).to be nil
|
|
expect(@a.submission_types).to eq 'external_tool'
|
|
end
|
|
end
|
|
|
|
describe "scope :type_quiz_lti" do
|
|
context "with a quiz_lti assignment" do
|
|
before :once do
|
|
assignment_model(:submission_types => "external_tool", :course => @course)
|
|
tool = @c.context_external_tools.create!(
|
|
:name => 'Quizzes.Next',
|
|
:consumer_key => 'test_key',
|
|
:shared_secret => 'test_secret',
|
|
:tool_id => 'Quizzes 2',
|
|
:url => 'http://example.com/launch'
|
|
)
|
|
@a.external_tool_tag_attributes = { :content => tool }
|
|
@a.save!
|
|
end
|
|
|
|
it "includes the quiz_lti quiz" do
|
|
expect(Assignment.type_quiz_lti).not_to be_empty
|
|
end
|
|
end
|
|
|
|
context "without any quiz_lti assignments" do
|
|
before :once do
|
|
assignment_model(:submission_types => "external_tool", :course => @course)
|
|
tool = @c.context_external_tools.create!(
|
|
:name => 'Some.Other.Tool',
|
|
:consumer_key => 'test_key',
|
|
:shared_secret => 'test_secret',
|
|
:tool_id => 'some-other-tool-id',
|
|
:url => 'http://example.com/launch'
|
|
)
|
|
@a.external_tool_tag_attributes = { :content => tool }
|
|
@a.save!
|
|
end
|
|
|
|
it "returns an empty scope" do
|
|
expect(Assignment.type_quiz_lti).to be_empty
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "linked submissions" do
|
|
shared_examples_for "submittable" do
|
|
before :once do
|
|
assignment_model(:course => @course, :submission_types => submission_type, :updating_user => @teacher)
|
|
end
|
|
|
|
it "should create a record if none exists and specified" do
|
|
expect(@a.submission_types).to eql(submission_type)
|
|
submittable = @a.send(submission_type)
|
|
expect(submittable).not_to be_nil
|
|
expect(submittable.assignment_id).to eql(@a.id)
|
|
expect(submittable.user_id).to eql(@teacher.id)
|
|
@a.due_at = Time.zone.now
|
|
@a.save
|
|
@a.reload
|
|
submittable = @a.send(submission_type)
|
|
expect(submittable).not_to be_nil
|
|
expect(submittable.assignment_id).to eql(@a.id)
|
|
expect(submittable.user_id).to eql(@teacher.id)
|
|
end
|
|
|
|
it "should delete a record if no longer specified" do
|
|
expect(@a.submission_types).to eql(submission_type)
|
|
submittable = @a.send(submission_type)
|
|
expect(submittable).not_to be_nil
|
|
expect(submittable.assignment_id).to eql(@a.id)
|
|
@a.submission_types = 'on_paper'
|
|
@a.save!
|
|
@a.reload
|
|
submittable = @a.send(submission_type)
|
|
expect(submittable).to be_nil
|
|
end
|
|
|
|
it "should not delete the assignment when unlinked" do
|
|
expect(@a.submission_types).to eql(submission_type)
|
|
submittable = @a.send(submission_type)
|
|
expect(submittable).not_to be_nil
|
|
expect(submittable.state).to eql(:active)
|
|
expect(submittable.assignment_id).to eql(@a.id)
|
|
@a.submission_types = 'on_paper'
|
|
@a.save!
|
|
submittable = submission_class.find(submittable.id)
|
|
expect(submittable.assignment_id).to eql(nil)
|
|
expect(submittable.state).to eql(:deleted)
|
|
@a.reload
|
|
submittable = @a.send(submission_type)
|
|
expect(submittable).to be_nil
|
|
expect(@a.state).to eql(:published)
|
|
end
|
|
end
|
|
|
|
context "topics" do
|
|
let(:submission_type) { "discussion_topic" }
|
|
let(:submission_class) { DiscussionTopic }
|
|
|
|
include_examples "submittable"
|
|
|
|
it "should not delete the topic if non-empty when unlinked" do
|
|
expect(@a.submission_types).to eql(submission_type)
|
|
@topic = @a.discussion_topic
|
|
expect(@topic).not_to be_nil
|
|
expect(@topic.assignment_id).to eql(@a.id)
|
|
@topic.discussion_entries.create!(:user => @user, :message => "testing")
|
|
@a.discussion_topic.reload
|
|
@a.submission_types = 'on_paper'
|
|
@a.save!
|
|
@a.reload
|
|
expect(@a.discussion_topic).to be_nil
|
|
expect(@a.state).to eql(:published)
|
|
@topic = submission_class.find(@topic.id)
|
|
expect(@topic.assignment_id).to eql(nil)
|
|
expect(@topic.state).to eql(:active)
|
|
end
|
|
|
|
it "should grab the original topic if unlinked and relinked" do
|
|
expect(@a.submission_types).to eql(submission_type)
|
|
@topic = @a.discussion_topic
|
|
expect(@topic).not_to be_nil
|
|
expect(@topic.assignment_id).to eql(@a.id)
|
|
@topic.discussion_entries.create!(:user => @user, :message => "testing")
|
|
@a.discussion_topic.reload
|
|
@a.submission_types = 'on_paper'
|
|
@a.save!
|
|
@a.submission_types = 'discussion_topic'
|
|
@a.save!
|
|
@a.reload
|
|
expect(@a.discussion_topic).to eql(@topic)
|
|
expect(@a.state).to eql(:published)
|
|
@topic.reload
|
|
expect(@topic.state).to eql(:active)
|
|
end
|
|
end
|
|
|
|
context "pages" do
|
|
let(:submission_type) { "wiki_page" }
|
|
let(:submission_class) { WikiPage }
|
|
|
|
context "feature enabled" do
|
|
before(:once) { @course.enable_feature!(:conditional_release) }
|
|
|
|
include_examples "submittable"
|
|
end
|
|
|
|
it "should not create a record if feature is disabled" do
|
|
expect do
|
|
assignment_model(:course => @course, :submission_types => 'wiki_page', :updating_user => @teacher)
|
|
end.not_to change { WikiPage.count }
|
|
expect(@a.submission_types).to eql(submission_type)
|
|
submittable = @a.send(submission_type)
|
|
expect(submittable).to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
context "participants" do
|
|
before :once do
|
|
setup_differentiated_assignments(ta: true)
|
|
end
|
|
|
|
it 'returns users with visibility' do
|
|
expect(@assignment.participants.length).to eq(4) #teacher, TA, 2 students
|
|
end
|
|
|
|
it 'includes students with visibility' do
|
|
expect(@assignment.participants.include?(@student1)).to be_truthy
|
|
end
|
|
|
|
it 'excludes students with inactive enrollments' do
|
|
@student1.student_enrollments.first.deactivate
|
|
expect(@assignment.participants.include?(@student1)).to be_falsey
|
|
end
|
|
|
|
it 'excludes students with completed enrollments' do
|
|
@student1.student_enrollments.first.complete!
|
|
expect(@assignment.participants.include?(@student1)).to be_falsey
|
|
end
|
|
|
|
it 'excludes students with completed enrollments by date' do
|
|
@course.start_at = 2.days.ago
|
|
@course.conclude_at = 1.day.ago
|
|
@course.restrict_enrollments_to_course_dates = true
|
|
@course.save!
|
|
expect(@assignment.participants.include?(@student1)).to be_falsey
|
|
end
|
|
|
|
it 'excludes students with completed enrollments by date when not differentiated' do
|
|
@course.update!(
|
|
conclude_at: 1.day.ago,
|
|
restrict_enrollments_to_course_dates: true,
|
|
start_at: 2.days.ago
|
|
)
|
|
# reload the course to clear any cached results of the participating_students_by_date scope
|
|
@course.reload
|
|
|
|
@assignment.update!(only_visible_to_overrides: false)
|
|
expect(@assignment.participants(by_date: true)).not_to include(@student1)
|
|
end
|
|
|
|
it 'excludes students without visibility' do
|
|
expect(@assignment.participants.include?(@student2)).to be_falsey
|
|
end
|
|
|
|
it 'includes admins with visibility' do
|
|
expect(@assignment.participants.include?(@teacher)).to be_truthy
|
|
expect(@assignment.participants.include?(@ta)).to be_truthy
|
|
end
|
|
|
|
context "including observers" do
|
|
before do
|
|
oe = @assignment.context.enroll_user(user_with_pseudonym(active_all: true), 'ObserverEnrollment',:enrollment_state => 'active')
|
|
@course_level_observer = oe.user
|
|
|
|
oe = @assignment.context.enroll_user(user_with_pseudonym(active_all: true), 'ObserverEnrollment',:enrollment_state => 'active')
|
|
oe.associated_user_id = @student1.id
|
|
oe.save!
|
|
@student1_observer = oe.user
|
|
|
|
oe = @assignment.context.enroll_user(user_with_pseudonym(active_all: true), 'ObserverEnrollment',:enrollment_state => 'active')
|
|
oe.associated_user_id = @student2.id
|
|
oe.save!
|
|
@student2_observer = oe.user
|
|
end
|
|
|
|
it "should include course_level observers" do
|
|
expect(@assignment.participants(include_observers: true).include?(@course_level_observer)).to be_truthy
|
|
end
|
|
|
|
it "should exclude student observers if their student does not have visibility" do
|
|
expect(@assignment.participants(include_observers: true).include?(@student1_observer)).to be_truthy
|
|
expect(@assignment.participants(include_observers: true).include?(@student2_observer)).to be_falsey
|
|
end
|
|
|
|
it "should exclude all observers unless opt is given" do
|
|
expect(@assignment.participants.include?(@student1_observer)).to be_falsey
|
|
expect(@assignment.participants.include?(@student2_observer)).to be_falsey
|
|
expect(@assignment.participants.include?(@course_level_observer)).to be_falsey
|
|
end
|
|
end
|
|
end
|
|
|
|
context "broadcast policy" do
|
|
context "due date changed" do
|
|
before :once do
|
|
Notification.create(:name => 'Assignment Due Date Changed')
|
|
end
|
|
|
|
it "should create a message when an assignment due date has changed" do
|
|
assignment_model(:title => 'Assignment with unstable due date', :course => @course)
|
|
@a.created_at = 1.month.ago
|
|
@a.due_at = Time.now + 60
|
|
@a.save!
|
|
expect(@a.messages_sent).to be_include('Assignment Due Date Changed')
|
|
expect(@a.messages_sent['Assignment Due Date Changed'].first.from_name).to eq @course.name
|
|
end
|
|
|
|
it "should NOT create a message when everything but the assignment due date has changed" do
|
|
t = Time.parse("Sep 1, 2009 5:00pm")
|
|
assignment_model(:title => 'Assignment with unstable due date', :due_at => t, :course => @course)
|
|
expect(@a.due_at).to eql(t)
|
|
@a.submission_types = "online_url"
|
|
@a.title = "New Title"
|
|
@a.due_at = t + 1
|
|
@a.description = "New description"
|
|
@a.points_possible = 50
|
|
@a.save!
|
|
expect(@a.messages_sent).not_to be_include('Assignment Due Date Changed')
|
|
end
|
|
end
|
|
|
|
context "assignment graded" do
|
|
before(:once) { setup_assignment_with_students }
|
|
|
|
specify { expect(@assignment).to be_published }
|
|
|
|
it "should notify students when their grade is changed" do
|
|
@sub2 = @assignment.grade_student(@stu2, grade: 8, grader: @teacher).first
|
|
expect(@sub2.messages_sent).not_to be_empty
|
|
expect(@sub2.messages_sent['Submission Graded']).to be_present
|
|
expect(@sub2.messages_sent['Submission Graded'].first.from_name).to eq @course.name
|
|
expect(@sub2.messages_sent['Submission Grade Changed']).to be_nil
|
|
@sub2.update(:graded_at => Time.zone.now - 60*60)
|
|
@sub2 = @assignment.grade_student(@stu2, grade: 9, grader: @teacher).first
|
|
expect(@sub2.messages_sent).not_to be_empty
|
|
expect(@sub2.messages_sent['Submission Graded']).to be_nil
|
|
expect(@sub2.messages_sent['Submission Grade Changed']).to be_present
|
|
expect(@sub2.messages_sent['Submission Grade Changed'].first.from_name).to eq @course.name
|
|
end
|
|
|
|
it "should not notify students when their grade is changed when grades are not yet posted" do
|
|
@assignment.ensure_post_policy(post_manually: true)
|
|
@sub2 = @assignment.grade_student(@stu2, grade: 8, grader: @teacher).first
|
|
@sub2.update(:graded_at => Time.zone.now - 60*60)
|
|
@sub2 = @assignment.grade_student(@stu2, grade: 9, grader: @teacher).first
|
|
expect(@sub2.messages_sent).to be_empty
|
|
end
|
|
end
|
|
|
|
context "assignment changed" do
|
|
before :once do
|
|
Notification.create(:name => 'Assignment Changed')
|
|
assignment_model(course: @course)
|
|
@a.unmute!
|
|
end
|
|
|
|
it "should create a message when an assignment changes after it's been published" do
|
|
@a.created_at = Time.parse("Jan 2 2000")
|
|
@a.description = "something different"
|
|
@a.notify_of_update = true
|
|
@a.save
|
|
expect(@a.messages_sent).to be_include('Assignment Changed')
|
|
expect(@a.messages_sent['Assignment Changed'].first.from_name).to eq @course.name
|
|
end
|
|
|
|
it "should NOT create a message when an assignment changes SHORTLY AFTER it's been created" do
|
|
@a.description = "something different"
|
|
@a.save
|
|
expect(@a.messages_sent).not_to be_include('Assignment Changed')
|
|
end
|
|
|
|
it "should not create a message when a muted assignment changes" do
|
|
@a.mute!
|
|
@a = Assignment.find(@a.id) # blank slate for messages_sent
|
|
@a.description = "something different"
|
|
@a.save
|
|
expect(@a.messages_sent).to be_empty
|
|
end
|
|
end
|
|
|
|
context "assignment created" do
|
|
before :once do
|
|
Notification.create(:name => 'Assignment Created')
|
|
end
|
|
|
|
it "should create a message when an assignment is added to a course in process" do
|
|
assignment_model(:course => @course)
|
|
expect(@a.messages_sent).to be_include('Assignment Created')
|
|
expect(@a.messages_sent['Assignment Created'].first.from_name).to eq @course.name
|
|
end
|
|
|
|
it "should not create a message in an unpublished course" do
|
|
Notification.create(:name => 'Assignment Created')
|
|
course_with_teacher(:active_user => true)
|
|
assignment_model(:course => @course)
|
|
expect(@a.messages_sent).not_to be_include('Assignment Created')
|
|
end
|
|
end
|
|
|
|
context "varied due date notifications" do
|
|
before :once do
|
|
@teacher.communication_channels.create(:path => "teacher@instructure.com").confirm!
|
|
|
|
@studentA = user_with_pseudonym(:active_all => true, :name => 'StudentA', :username => 'studentA@instructure.com')
|
|
@ta = user_with_pseudonym(:active_all => true, :name => 'TA1', :username => 'ta1@instructure.com')
|
|
@course.enroll_student(@studentA).update_attribute(:workflow_state, 'active')
|
|
@course.enroll_user(@ta, 'TaEnrollment', :enrollment_state => 'active', :limit_privileges_to_course_section => true)
|
|
|
|
@section2 = @course.course_sections.create!(:name => 'section 2')
|
|
@studentB = user_with_pseudonym(:active_all => true, :name => 'StudentB', :username => 'studentB@instructure.com')
|
|
@ta2 = user_with_pseudonym(:active_all => true, :name => 'TA2', :username => 'ta2@instructure.com')
|
|
@section2.enroll_user(@studentB, 'StudentEnrollment', 'active')
|
|
@course.enroll_user(@ta2, 'TaEnrollment', :section => @section2, :enrollment_state => 'active', :limit_privileges_to_course_section => true)
|
|
|
|
Time.zone = 'Alaska'
|
|
default_due = DateTime.parse("01 Jan 2011 14:00 AKST")
|
|
section_2_due = DateTime.parse("02 Jan 2011 14:00 AKST")
|
|
@assignment = @course.assignments.build(:title => "some assignment", :due_at => default_due, :submission_types => ['online_text_entry'])
|
|
@assignment.save_without_broadcasting!
|
|
override = @assignment.assignment_overrides.build
|
|
override.set = @section2
|
|
override.override_due_at(section_2_due)
|
|
override.save!
|
|
end
|
|
|
|
context "assignment created" do
|
|
before :once do
|
|
Notification.create(:name => 'Assignment Created')
|
|
end
|
|
|
|
it "preload user roles for much fasterness" do
|
|
expect(@assignment.context).to receive(:preloaded_user_has_been?).at_least(:once)
|
|
|
|
@assignment.do_notifications!
|
|
end
|
|
|
|
it "should notify of the correct due date for the recipient, or 'multiple'" do
|
|
@assignment.do_notifications!
|
|
|
|
messages_sent = @assignment.messages_sent['Assignment Created']
|
|
expect(messages_sent.detect{|m|m.user_id == @teacher.id}.body).to be_include "Multiple Dates"
|
|
expect(messages_sent.detect{|m|m.user_id == @studentA.id}.body).to be_include "Jan 1, 2011"
|
|
expect(messages_sent.detect{|m|m.user_id == @ta.id}.body).to be_include "Multiple Dates"
|
|
expect(messages_sent.detect{|m|m.user_id == @studentB.id}.body).to be_include "Jan 2, 2011"
|
|
expect(messages_sent.detect{|m|m.user_id == @ta2.id}.body).to be_include "Multiple Dates"
|
|
end
|
|
|
|
it "should notify the correct people with differentiated_assignments enabled" do
|
|
section = @course.course_sections.create!(name: 'Lonely Section')
|
|
student = student_in_section(section)
|
|
@assignment.do_notifications!
|
|
|
|
messages_sent = @assignment.messages_sent['Assignment Created']
|
|
expect(messages_sent.detect{|m|m.user_id == @teacher.id}.body).to be_include "Multiple Dates"
|
|
expect(messages_sent.detect{|m|m.user_id == @studentA.id}.body).to be_include "Jan 1, 2011"
|
|
expect(messages_sent.detect{|m|m.user_id == @ta.id}.body).to be_include "Multiple Dates"
|
|
expect(messages_sent.detect{|m|m.user_id == @studentB.id}.body).to be_include "Jan 2, 2011"
|
|
expect(messages_sent.detect{|m|m.user_id == @ta2.id}.body).to be_include "Multiple Dates"
|
|
expect(messages_sent.detect{|m|m.user_id == student.id}).to be_nil
|
|
end
|
|
|
|
it "should collapse identical instructor due dates" do
|
|
# change the override to match the default due date
|
|
override = @assignment.assignment_overrides.first
|
|
override.override_due_at(@assignment.due_at)
|
|
override.save!
|
|
@assignment.reload
|
|
|
|
@assignment.do_notifications!
|
|
|
|
# when the override matches the default, show the default and not "Multiple"
|
|
messages_sent = @assignment.messages_sent['Assignment Created']
|
|
messages_sent.each{|m| expect(m.body).to be_include "Jan 1, 2011"}
|
|
end
|
|
end
|
|
|
|
context "assignment due date changed" do
|
|
before :once do
|
|
Notification.create(:name => 'Assignment Due Date Changed')
|
|
Notification.create(:name => 'Assignment Due Date Override Changed')
|
|
end
|
|
|
|
it "should notify appropriate parties when the default due date changes" do
|
|
@assignment.update_attribute(:created_at, 1.day.ago)
|
|
|
|
@assignment.due_at = DateTime.parse("09 Jan 2011 14:00 AKST")
|
|
@assignment.save!
|
|
|
|
messages_sent = @assignment.messages_sent['Assignment Due Date Changed']
|
|
expect(messages_sent.detect{|m|m.user_id == @teacher.id}.body).to be_include "Jan 9, 2011"
|
|
expect(messages_sent.detect{|m|m.user_id == @studentA.id}.body).to be_include "Jan 9, 2011"
|
|
expect(messages_sent.detect{|m|m.user_id == @ta.id}.body).to be_include "Jan 9, 2011"
|
|
expect(messages_sent.detect{|m|m.user_id == @studentB.id}).to be_nil
|
|
expect(messages_sent.detect{|m|m.user_id == @ta2.id}.body).to be_include "Jan 9, 2011"
|
|
end
|
|
|
|
it "should notify appropriate parties when an override due date changes" do
|
|
@assignment.update_attribute(:created_at, 1.day.ago)
|
|
|
|
override = @assignment.assignment_overrides.first.reload
|
|
override.override_due_at(DateTime.parse("11 Jan 2011 11:11 AKST"))
|
|
override.save!
|
|
|
|
messages_sent = override.messages_sent['Assignment Due Date Changed']
|
|
expect(messages_sent.detect{|m|m.user_id == @studentA.id}).to be_nil
|
|
expect(messages_sent.detect{|m|m.user_id == @studentB.id}.body).to be_include "Jan 11, 2011"
|
|
|
|
messages_sent = override.messages_sent['Assignment Due Date Override Changed']
|
|
expect(messages_sent.detect{|m|m.user_id == @ta.id}).to be_nil
|
|
expect(messages_sent.detect{|m|m.user_id == @teacher.id}.body).to be_include "Jan 11, 2011"
|
|
expect(messages_sent.detect{|m|m.user_id == @ta2.id}.body).to be_include "Jan 11, 2011"
|
|
end
|
|
end
|
|
|
|
context "assignment submitted late" do
|
|
before :once do
|
|
Notification.create(:name => 'Assignment Submitted')
|
|
Notification.create(:name => 'Assignment Submitted Late')
|
|
end
|
|
|
|
it "should send a late submission notification iff the submit date is late for the submitter" do
|
|
fake_submission_time = Time.parse "Jan 01 17:00:00 -0900 2011"
|
|
allow(Time).to receive(:now).and_return(fake_submission_time)
|
|
subA = @assignment.submit_homework @studentA, :submission_type => "online_text_entry", :body => "ooga"
|
|
subB = @assignment.submit_homework @studentB, :submission_type => "online_text_entry", :body => "booga"
|
|
|
|
expect(subA.messages_sent["Assignment Submitted Late"]).not_to be_nil
|
|
expect(subB.messages_sent["Assignment Submitted Late"]).to be_nil
|
|
end
|
|
end
|
|
|
|
context "group assignment submitted late" do
|
|
before :once do
|
|
Notification.create(:name => 'Group Assignment Submitted Late')
|
|
end
|
|
|
|
it "should send a late submission notification iff the submit date is late for the group" do
|
|
@a = assignment_model(:course => @course, :group_category => "Study Groups", :due_at => Time.parse("Jan 01 17:00:00 -0900 2011"), :submission_types => ["online_text_entry"])
|
|
@group1 = @a.context.groups.create!(:name => "Study Group 1", :group_category => @a.group_category)
|
|
@group1.add_user(@studentA)
|
|
@group2 = @a.context.groups.create!(:name => "Study Group 2", :group_category => @a.group_category)
|
|
@group2.add_user(@studentB)
|
|
override = @a.assignment_overrides.new
|
|
override.set = @group2
|
|
override.override_due_at(Time.parse("Jan 03 17:00:00 -0900 2011"))
|
|
override.save!
|
|
fake_submission_time = Time.parse("Jan 02 17:00:00 -0900 2011")
|
|
allow(Time).to receive(:now).and_return(fake_submission_time)
|
|
subA = @assignment.submit_homework @studentA, :submission_type => "online_text_entry", :body => "eenie"
|
|
subB = @assignment.submit_homework @studentB, :submission_type => "online_text_entry", :body => "meenie"
|
|
|
|
expect(subA.messages_sent["Group Assignment Submitted Late"]).not_to be_nil
|
|
expect(subB.messages_sent["Group Assignment Submitted Late"]).to be_nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context "group assignment" do
|
|
before :once do
|
|
setup_assignment_with_group
|
|
@a.unmute!
|
|
end
|
|
|
|
it "should submit the homework for all students in the same group" do
|
|
sub = @a.submit_homework(@u1, :submission_type => "online_text_entry", :body => "Some text for you")
|
|
expect(sub.user_id).to eql(@u1.id)
|
|
@a.reload
|
|
subs = @a.submissions.not_placeholder
|
|
expect(subs.length).to eql(2)
|
|
expect(subs.map(&:group_id).uniq).to eql([@group.id])
|
|
expect(subs.map(&:submission_type).uniq).to eql(['online_text_entry'])
|
|
expect(subs.map(&:body).uniq).to eql(['Some text for you'])
|
|
end
|
|
|
|
it "should submit the homework for all students in the group if grading them individually" do
|
|
@a.update_attribute(:grade_group_students_individually, true)
|
|
@a.submit_homework(@u1, :submission_type => "online_text_entry", :body => "Test submission")
|
|
@a.reload
|
|
submissions = @a.submissions.not_placeholder
|
|
expect(submissions.length).to eql 2
|
|
expect(submissions.map(&:group_id).uniq).to eql [@group.id]
|
|
expect(submissions.map(&:submission_type).uniq).to eql ["online_text_entry"]
|
|
expect(submissions.map(&:body).uniq).to eql ["Test submission"]
|
|
end
|
|
|
|
it "should update submission for all students in the same group" do
|
|
res = @a.grade_student(@u1, grade: "10", grader: @teacher)
|
|
expect(res).not_to be_nil
|
|
expect(res).not_to be_empty
|
|
expect(res.length).to eql(2)
|
|
expect(res.map{|s| s.user}).to be_include(@u1)
|
|
expect(res.map{|s| s.user}).to be_include(@u2)
|
|
end
|
|
|
|
it "should create an initial submission comment for only the submitter by default" do
|
|
sub = @a.submit_homework(@u1, :submission_type => "online_text_entry", :body => "Some text for you", :comment => "hey teacher, i hate my group. i did this entire project by myself :(")
|
|
expect(sub.user_id).to eql(@u1.id)
|
|
expect(sub.submission_comments.size).to eql 1
|
|
@a.reload
|
|
other_sub = (@a.submissions - [sub])[0]
|
|
expect(other_sub.submission_comments.size).to eql 0
|
|
end
|
|
|
|
it "should add a submission comment for only the specified user by default" do
|
|
@a.submit_homework(@u1, :submission_type => "online_text_entry", :body => "Some text for you", :comment => "ohai teacher, we had so much fun working together", :group_comment => "1")
|
|
res = @a.update_submission(@u1, :comment => "woot")
|
|
expect(res).not_to be_nil
|
|
expect(res).not_to be_empty
|
|
expect(res.length).to eql(1)
|
|
expect(res.find{|s| s.user == @u1}.submission_comments).not_to be_empty
|
|
expect(res.find{|s| s.user == @u2}).to be_nil #.submission_comments.should be_empty
|
|
end
|
|
|
|
it "should update submission for only the individual student if set thay way" do
|
|
@a.update_attribute(:grade_group_students_individually, true)
|
|
res = @a.grade_student(@u1, grade: "10", grader: @teacher)
|
|
expect(res).not_to be_nil
|
|
expect(res).not_to be_empty
|
|
expect(res.length).to eql(1)
|
|
expect(res[0].user).to eql(@u1)
|
|
end
|
|
|
|
it "should create an initial submission comment for all group members if specified" do
|
|
sub = @a.submit_homework(@u1, :submission_type => "online_text_entry", :body => "Some text for you", :comment => "ohai teacher, we had so much fun working together", :group_comment => "1")
|
|
expect(sub.user_id).to eql(@u1.id)
|
|
expect(sub.submission_comments.size).to eql 1
|
|
@a.reload
|
|
other_sub = (@a.submissions.not_placeholder - [sub])[0]
|
|
expect(other_sub.submission_comments.size).to eql 1
|
|
end
|
|
|
|
it "should add a submission comment for all group members if specified" do
|
|
@a.submit_homework(@u1, :submission_type => "online_text_entry", :body => "Some text for you")
|
|
res = @a.update_submission(@u1, :comment => "woot", :group_comment => "1")
|
|
expect(res).not_to be_nil
|
|
expect(res).not_to be_empty
|
|
expect(res.length).to eql(2)
|
|
expect(res.find{|s| s.user == @u1}.submission_comments).not_to be_empty
|
|
expect(res.find{|s| s.user == @u2}.submission_comments).not_to be_empty
|
|
# all the comments should have the same group_comment_id, for deletion
|
|
comments = SubmissionComment.for_assignment_id(@a.id).to_a
|
|
expect(comments.size).to eq 2
|
|
group_comment_id = comments[0].group_comment_id
|
|
expect(group_comment_id).to be_present
|
|
expect(comments.all? { |c| c.group_comment_id == group_comment_id }).to be_truthy
|
|
end
|
|
|
|
it "hides grading comments for all group members if commenter is teacher and grades are hidden after commenting" do
|
|
@a.update_submission(@u1, :comment => "woot", :group_comment => "1", author: @teacher)
|
|
@a.mute!
|
|
|
|
comments = @a.submissions.preload(:submission_comments).map(&:submission_comments).flatten
|
|
expect(comments.map(&:hidden?)).to all(be true)
|
|
end
|
|
|
|
it "does not hide grading comments for all group members if commenter is student and assignment is muted after commenting" do
|
|
@a.update_submission(@u1, :comment => "woot", :group_comment => "1", author: @u1)
|
|
@a.mute!
|
|
|
|
comments = @a.submissions.preload(:submission_comments).map(&:submission_comments).flatten
|
|
expect(comments.map(&:hidden?)).to all(be false)
|
|
end
|
|
|
|
it "shows grading comments for all group members if commenter is teacher and assignment is unmuted" do
|
|
@a.mute!
|
|
@a.update_submission(@u1, :comment => "woot", :group_comment => "1", author: @teacher)
|
|
@a.unmute!
|
|
|
|
comments = @a.submissions.preload(:submission_comments).map(&:submission_comments).flatten
|
|
expect(comments.map(&:hidden?)).to all(be false)
|
|
end
|
|
|
|
it "return the single submission if the user is not in a group" do
|
|
res = @a.grade_student(@u3, :comment => "woot", :group_comment => "1")
|
|
expect(res).not_to be_nil
|
|
expect(res).not_to be_empty
|
|
expect(res.length).to eql(1)
|
|
res = @a.update_submission(@u3, :comment => "woot", :group_comment => "1")
|
|
comments = res.find{|s| s.user == @u3}.submission_comments
|
|
expect(comments.size).to eq 1
|
|
expect(comments[0].group_comment_id).to be_nil
|
|
end
|
|
|
|
it "associates attachments with all submissions" do
|
|
@a.update_attribute :submission_types, "online_upload"
|
|
f = @u1.attachments.create! uploaded_data: StringIO.new('blah'),
|
|
context: @u1,
|
|
filename: 'blah.txt'
|
|
@a.submit_homework(@u1, attachments: [f])
|
|
@a.submissions.reload.not_placeholder.each { |s|
|
|
expect(s.attachments).to eq [f]
|
|
}
|
|
end
|
|
end
|
|
|
|
context "adheres_to_policy" do
|
|
it "should serialize permissions" do
|
|
@assignment = @course.assignments.create!(:title => "some assignment")
|
|
data = @assignment.as_json(:permissions => {:user => @user, :session => nil}) rescue nil
|
|
expect(data).not_to be_nil
|
|
expect(data['assignment']).not_to be_nil
|
|
expect(data['assignment']['permissions']).not_to be_nil
|
|
expect(data['assignment']['permissions']).not_to be_empty
|
|
end
|
|
end
|
|
|
|
describe "sections_with_visibility" do
|
|
before(:once) do
|
|
course_with_teacher(:active_all => true)
|
|
@section = @course.course_sections.create!
|
|
@student = student_in_section(@section)
|
|
@assignment, @assignment2, @assignment3 = (1..3).map{ @course.assignments.create! }
|
|
|
|
@assignment.only_visible_to_overrides = true
|
|
create_section_override_for_assignment(@assignment, course_section: @section)
|
|
|
|
@assignment2.only_visible_to_overrides = true
|
|
|
|
@assignment3.only_visible_to_overrides = false
|
|
create_section_override_for_assignment(@assignment3, course_section: @section)
|
|
[@assignment, @assignment2, @assignment3].each(&:save!)
|
|
end
|
|
|
|
it "returns only sections with overrides with differentiated assignments on" do
|
|
expect(@assignment.sections_with_visibility(@teacher)).to eq [@section]
|
|
expect(@assignment2.sections_with_visibility(@teacher)).to eq []
|
|
expect(@assignment3.sections_with_visibility(@teacher)).to eq @course.course_sections
|
|
end
|
|
end
|
|
|
|
context "modules" do
|
|
it "should be locked when part of a locked module" do
|
|
ag = @course.assignment_groups.create!
|
|
a1 = ag.assignments.create!(:context => course_factory)
|
|
expect(a1.locked_for?(@user)).to be_falsey
|
|
|
|
m = @course.context_modules.create!
|
|
ct = ContentTag.new
|
|
ct.content_id = a1.id
|
|
ct.content_type = 'Assignment'
|
|
ct.context_id = course_factory.id
|
|
ct.context_type = 'Course'
|
|
ct.title = "Assignment"
|
|
ct.tag_type = "context_module"
|
|
ct.context_module_id = m.id
|
|
ct.context_code = "course_#{@course.id}"
|
|
ct.save!
|
|
|
|
m.unlock_at = Time.now.in_time_zone + 1.day
|
|
m.save
|
|
a1.reload
|
|
expect(a1.locked_for?(@user)).to be_truthy
|
|
end
|
|
|
|
it "should be locked when associated discussion topic is part of a locked module" do
|
|
a1 = assignment_model(:course => @course, :submission_types => "discussion_topic")
|
|
a1.reload
|
|
expect(a1.locked_for?(@user)).to be_falsey
|
|
|
|
m = @course.context_modules.create!
|
|
m.add_item(:id => a1.discussion_topic.id, :type => 'discussion_topic')
|
|
|
|
m.unlock_at = Time.now.in_time_zone + 1.day
|
|
m.save
|
|
a1.reload
|
|
expect(a1.locked_for?(@user)).to be_truthy
|
|
end
|
|
|
|
it "should be locked when associated wiki page is part of a locked module" do
|
|
@course.enable_feature!(:conditional_release)
|
|
a1 = assignment_model(:course => @course, :submission_types => "wiki_page")
|
|
a1.reload
|
|
expect(a1.locked_for?(@user)).to be_falsey
|
|
|
|
m = @course.context_modules.create!
|
|
m.add_item(:id => a1.wiki_page.id, :type => 'wiki_page')
|
|
|
|
m.unlock_at = Time.now.in_time_zone + 1.day
|
|
m.save
|
|
a1.reload
|
|
expect(a1.locked_for?(@user)).to be_truthy
|
|
end
|
|
|
|
it "should not be locked by wiki page when feature is disabled" do
|
|
a1 = wiki_page_assignment_model(:course => @course)
|
|
a1.reload
|
|
expect(a1.locked_for?(@user)).to be_falsey
|
|
|
|
m = @course.context_modules.create!
|
|
m.add_item(:id => a1.wiki_page.id, :type => 'wiki_page')
|
|
|
|
m.unlock_at = Time.now.in_time_zone + 1.day
|
|
m.save
|
|
a1.reload
|
|
expect(a1.locked_for?(@user)).to be_falsey
|
|
end
|
|
|
|
it "should be locked when associated quiz is part of a locked module" do
|
|
a1 = assignment_model(:course => @course, :submission_types => "online_quiz")
|
|
a1.reload
|
|
expect(a1.locked_for?(@user)).to be_falsey
|
|
|
|
m = @course.context_modules.create!
|
|
m.add_item(:id => a1.quiz.id, :type => 'quiz')
|
|
|
|
m.unlock_at = Time.now.in_time_zone + 1.day
|
|
m.save
|
|
a1.reload
|
|
expect(a1.locked_for?(@user)).to be_truthy
|
|
end
|
|
end
|
|
|
|
context "group_students" do
|
|
it "should return [nil, [student]] unless the assignment has a group_category" do
|
|
@assignment = assignment_model(course: @course)
|
|
@student = user_model
|
|
expect(@assignment.group_students(@student)).to eq [nil, [@student]]
|
|
end
|
|
|
|
it "should return [nil, [student]] if the context doesn't have any active groups in the same category" do
|
|
@assignment = assignment_model(:group_category => "Fake Category", :course => @course)
|
|
@student = user_model
|
|
expect(@assignment.group_students(@student)).to eq [nil, [@student]]
|
|
end
|
|
|
|
it "should return [nil, [student]] if the student isn't in any of the candidate groups" do
|
|
@assignment = assignment_model(:group_category => "Category", :course => @course)
|
|
@group = @course.groups.create(:name => "Group", :group_category => @assignment.group_category)
|
|
@student = user_model
|
|
expect(@assignment.group_students(@student)).to eq [nil, [@student]]
|
|
end
|
|
|
|
it "should return [group, [students from group]] if the student is in one of the candidate groups" do
|
|
@assignment = assignment_model(:group_category => "Category", :course => @course)
|
|
@course.enroll_student(@student1 = user_model)
|
|
@course.enroll_student(@student2 = user_model)
|
|
@course.enroll_student(@student3 = user_model)
|
|
@group1 = @course.groups.create(:name => "Group 1", :group_category => @assignment.group_category)
|
|
@group1.add_user(@student1)
|
|
@group1.add_user(@student2)
|
|
@group2 = @course.groups.create(:name => "Group 2", :group_category => @assignment.group_category)
|
|
@group2.add_user(@student3)
|
|
|
|
# have to reload because the enrolled students above don't show up in
|
|
# Course#students until the course has been reloaded
|
|
result = @assignment.reload.group_students(@student1)
|
|
expect(result.first).to eq @group1
|
|
expect(result.last.map{ |u| u.id }.sort).to eq [@student1, @student2].map{ |u| u.id }.sort
|
|
end
|
|
|
|
it "returns distinct users" do
|
|
s1, s2 = n_students_in_course(2)
|
|
|
|
section = @course.course_sections.create! name: "some section"
|
|
e = @course.enroll_user s1, 'StudentEnrollment',
|
|
section: section,
|
|
allow_multiple_enrollments: true
|
|
e.update_attribute :workflow_state, 'active'
|
|
|
|
gc = @course.group_categories.create! name: "Homework Groups"
|
|
group = gc.groups.create! name: "Group 1", context: @course
|
|
group.add_user(s1)
|
|
group.add_user(s2)
|
|
|
|
a = @course.assignments.create! name: "Group Assignment",
|
|
group_category_id: gc.id
|
|
g, students = a.group_students(s1)
|
|
expect(g).to eq group
|
|
expect(students.sort_by(&:id)).to eq [s1, s2]
|
|
end
|
|
end
|
|
|
|
it "should maintain the deprecated group_category attribute" do
|
|
assignment = assignment_model(course: @course)
|
|
expect(assignment.read_attribute(:group_category)).to be_nil
|
|
assignment.group_category = assignment.context.group_categories.create(:name => "my category")
|
|
assignment.save
|
|
assignment.reload
|
|
expect(assignment.read_attribute(:group_category)).to eql("my category")
|
|
assignment.group_category = nil
|
|
assignment.save
|
|
assignment.reload
|
|
expect(assignment.read_attribute(:group_category)).to be_nil
|
|
end
|
|
|
|
it "should provide has_group_category?" do
|
|
assignment = assignment_model(course: @course)
|
|
expect(assignment.has_group_category?).to be_falsey
|
|
assignment.group_category = assignment.context.group_categories.create(:name => "my category")
|
|
expect(assignment.has_group_category?).to be_truthy
|
|
assignment.group_category = nil
|
|
expect(assignment.has_group_category?).to be_falsey
|
|
end
|
|
|
|
context "turnitin settings" do
|
|
before(:once) { assignment_model(course: @course) }
|
|
|
|
it "should sanitize bad data" do
|
|
assignment = @assignment
|
|
assignment.turnitin_settings = {
|
|
:originality_report_visibility => 'invalid',
|
|
:s_paper_check => '2',
|
|
:internet_check => 1,
|
|
:journal_check => 0,
|
|
:exclude_biblio => true,
|
|
:exclude_quoted => false,
|
|
:exclude_type => '3',
|
|
:exclude_value => 'poiuopiuuiop',
|
|
:bogus => 'haha'
|
|
}
|
|
expect(assignment.turnitin_settings).to eql({
|
|
:originality_report_visibility => 'immediate',
|
|
:s_paper_check => '1',
|
|
:internet_check => '1',
|
|
:journal_check => '0',
|
|
:exclude_biblio => '1',
|
|
:exclude_quoted => '0',
|
|
:exclude_type => '0',
|
|
:exclude_value => '',
|
|
:s_view_report => '1',
|
|
:submit_papers_to => '0'
|
|
})
|
|
end
|
|
|
|
it "should persist :created across changes" do
|
|
assignment = @assignment
|
|
assignment.turnitin_settings = Turnitin::Client.default_assignment_turnitin_settings
|
|
assignment.save
|
|
assignment.turnitin_settings[:created] = true
|
|
assignment.save
|
|
assignment.reload
|
|
expect(assignment.turnitin_settings[:created]).to be_truthy
|
|
|
|
assignment.turnitin_settings = Turnitin::Client.default_assignment_turnitin_settings.merge(:s_paper_check => '0')
|
|
assignment.save
|
|
assignment.reload
|
|
expect(assignment.turnitin_settings[:created]).to be_truthy
|
|
end
|
|
|
|
it "should clear out :current" do
|
|
assignment = @assignment
|
|
assignment.turnitin_settings = Turnitin::Client.default_assignment_turnitin_settings
|
|
assignment.save
|
|
assignment.turnitin_settings[:current] = true
|
|
assignment.save
|
|
assignment.reload
|
|
expect(assignment.turnitin_settings[:current]).to be_truthy
|
|
|
|
assignment.turnitin_settings = Turnitin::Client.default_assignment_turnitin_settings.merge(:s_paper_check => '0')
|
|
assignment.save
|
|
assignment.reload
|
|
expect(assignment.turnitin_settings[:current]).to be_nil
|
|
end
|
|
|
|
it "should use default originality setting from account" do
|
|
assignment = @assignment
|
|
account = assignment.course.account
|
|
account.turnitin_originality = "after_grading"
|
|
account.save!
|
|
expect(assignment.turnitin_settings[:originality_report_visibility]).to eq('after_grading')
|
|
end
|
|
end
|
|
|
|
context "generate comments from submissions" do
|
|
def create_and_submit
|
|
setup_assignment_without_submission
|
|
|
|
@attachment = @user.attachments.new :filename => "homework.doc"
|
|
@attachment.content_type = "foo/bar"
|
|
@attachment.size = 10
|
|
@attachment.save!
|
|
|
|
@submission = @assignment.submit_homework @user, :submission_type => :online_upload, :attachments => [@attachment]
|
|
end
|
|
|
|
it "should infer_comment_context_from_filename" do
|
|
create_and_submit
|
|
ignore_file = "/tmp/._why_macos_why.txt"
|
|
@assignment.instance_variable_set :@ignored_files, []
|
|
expect(@assignment.send(:infer_comment_context_from_filename, ignore_file)).to be_nil
|
|
expect(@assignment.instance_variable_get(:@ignored_files)).to eq [ignore_file]
|
|
|
|
filename = [@user.last_name_first, @user.id, @attachment.id, @attachment.display_name].join("_")
|
|
|
|
expect(@assignment.send(:infer_comment_context_from_filename, filename)).to eq({
|
|
:user => @user,
|
|
:submission => @submission,
|
|
:filename => filename,
|
|
:display_name => @attachment.display_name
|
|
})
|
|
expect(@assignment.instance_variable_get(:@ignored_files)).to eq [ignore_file]
|
|
end
|
|
|
|
it "should ignore when assignment.id does not belog to the user" do
|
|
create_and_submit
|
|
false_attachment = @attachment
|
|
student_in_course(active_all: true, user_name: "other user")
|
|
create_and_submit
|
|
ignore_file = [@user.last_name_first, @user.id, false_attachment.id, @attachment.display_name].join("_")
|
|
@assignment.instance_variable_set :@ignored_files, []
|
|
expect(@assignment.send(:infer_comment_context_from_filename, ignore_file)).to be_nil
|
|
expect(@assignment.instance_variable_get(:@ignored_files)).to eq [ignore_file]
|
|
end
|
|
|
|
describe "newly-created comments" do
|
|
before(:each) do
|
|
@assignment = @course.assignments.create!(name: "Mute Comment Test", submission_types: %w(online_upload))
|
|
end
|
|
|
|
let(:zip) { zip_submissions_legacy }
|
|
let(:added_comment) { @assignment.submission_for_student(@student).submission_comments.last }
|
|
|
|
context "for a manually-posted assignment" do
|
|
before(:each) do
|
|
@assignment.post_policy.update!(post_manually: true)
|
|
end
|
|
|
|
it "hides new comments if the submission is not posted" do
|
|
submit_homework(@student)
|
|
|
|
@assignment.generate_comments_from_files_legacy(zip.open.path, @user)
|
|
expect(added_comment).to be_hidden
|
|
end
|
|
|
|
it "shows new comments if the submission is posted" do
|
|
submit_homework(@student)
|
|
@assignment.post_submissions
|
|
|
|
@assignment.generate_comments_from_files_legacy(zip.open.path, @user)
|
|
expect(added_comment).not_to be_hidden
|
|
end
|
|
end
|
|
|
|
context "for a automatically-posted assignment" do
|
|
it "shows new comments if the submission is posted" do
|
|
submit_homework(@student)
|
|
@assignment.post_submissions
|
|
|
|
@assignment.generate_comments_from_files_legacy(zip.open.path, @user)
|
|
expect(added_comment).not_to be_hidden
|
|
end
|
|
|
|
it "hides new comments if the submission is graded but not posted" do
|
|
submit_homework(@student)
|
|
@assignment.grade_student(@student, grade: 1, grader: @teacher)
|
|
@assignment.hide_submissions
|
|
|
|
@assignment.generate_comments_from_files_legacy(zip.open.path, @user)
|
|
expect(added_comment).to be_hidden
|
|
end
|
|
|
|
it "shows new comments if the submission is neither graded nor posted" do
|
|
submit_homework(@student)
|
|
|
|
@assignment.generate_comments_from_files_legacy(zip.open.path, @user)
|
|
expect(added_comment).not_to be_hidden
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context "attribute freezing" do
|
|
before :once do
|
|
@asmnt = @course.assignments.create!(:title => 'lock locky')
|
|
@att_map = {"lock_at" => "yes",
|
|
"assignment_group" => "no",
|
|
"title" => "no",
|
|
"assignment_group_id" => "no",
|
|
"submission_types" => "yes",
|
|
"points_possible" => "yes",
|
|
"description" => "yes",
|
|
"grading_type" => "yes"}
|
|
end
|
|
|
|
def stub_plugin
|
|
allow(PluginSetting).to receive(:settings_for_plugin).and_return(@att_map)
|
|
end
|
|
|
|
it "should not be frozen if not copied" do
|
|
stub_plugin
|
|
@asmnt.freeze_on_copy = true
|
|
expect(@asmnt.frozen?).to eq false
|
|
@att_map.each_key{|att| expect(@asmnt.att_frozen?(att)).to eq false}
|
|
end
|
|
|
|
it "should not be frozen if copied but not frozen set" do
|
|
stub_plugin
|
|
@asmnt.copied = true
|
|
expect(@asmnt.frozen?).to eq false
|
|
@att_map.each_key{|att| expect(@asmnt.att_frozen?(att)).to eq false}
|
|
end
|
|
|
|
it "should not be frozen if plugin not enabled" do
|
|
@asmnt.copied = true
|
|
@asmnt.freeze_on_copy = true
|
|
expect(@asmnt.frozen?).to eq false
|
|
@att_map.each_key{|att| expect(@asmnt.att_frozen?(att)).to eq false}
|
|
end
|
|
|
|
context "assignments are frozen" do
|
|
before :once do
|
|
@admin = account_admin_user()
|
|
teacher_in_course(:course => @course)
|
|
end
|
|
|
|
before :each do
|
|
stub_plugin
|
|
@asmnt.copied = true
|
|
@asmnt.freeze_on_copy = true
|
|
end
|
|
|
|
it "should be frozen" do
|
|
expect(@asmnt.frozen?).to eq true
|
|
end
|
|
|
|
it "should flag specific attributes as frozen for no user" do
|
|
@att_map.each_pair do |att, setting|
|
|
expect(@asmnt.att_frozen?(att)).to eq(setting == "yes")
|
|
end
|
|
end
|
|
|
|
it "should flag specific attributes as frozen for teacher" do
|
|
@att_map.each_pair do |att, setting|
|
|
expect(@asmnt.att_frozen?(att, @teacher)).to eq(setting == "yes")
|
|
end
|
|
end
|
|
|
|
it "should not flag attributes as frozen for admin" do
|
|
@att_map.each_pair do |att, setting|
|
|
expect(@asmnt.att_frozen?(att, @admin)).to eq false
|
|
end
|
|
end
|
|
|
|
it "should be frozen for nil user" do
|
|
expect(@asmnt.frozen_for_user?(nil)).to eq true
|
|
end
|
|
|
|
it "should not be frozen for admin" do
|
|
expect(@asmnt.frozen_for_user?(@admin)).to eq false
|
|
end
|
|
|
|
it "should not validate if saving without user" do
|
|
@asmnt.description = "new description"
|
|
@asmnt.save
|
|
expect(@asmnt.valid?).to eq false
|
|
expect(@asmnt.errors["description"]).to eq ["You don't have permission to edit the locked attribute description"]
|
|
end
|
|
|
|
it "should allow teacher to edit unlocked attributes" do
|
|
@asmnt.title = "new title"
|
|
@asmnt.updating_user = @teacher
|
|
@asmnt.save!
|
|
|
|
@asmnt.reload
|
|
expect(@asmnt.title).to eq "new title"
|
|
end
|
|
|
|
it "should not allow teacher to edit locked attributes" do
|
|
@asmnt.description = "new description"
|
|
@asmnt.updating_user = @teacher
|
|
@asmnt.save
|
|
|
|
expect(@asmnt.valid?).to eq false
|
|
expect(@asmnt.errors["description"]).to eq ["You don't have permission to edit the locked attribute description"]
|
|
|
|
@asmnt.reload
|
|
expect(@asmnt.description).not_to eq "new title"
|
|
end
|
|
|
|
it "should allow admin to edit unlocked attributes" do
|
|
@asmnt.description = "new description"
|
|
@asmnt.updating_user = @admin
|
|
@asmnt.save!
|
|
|
|
@asmnt.reload
|
|
expect(@asmnt.description).to eq "new description"
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
context "not_locked scope" do
|
|
before :once do
|
|
assignment_quiz([], :course => @course, :user => @user)
|
|
# Setup default values for tests (leave unsaved for easy changes)
|
|
@quiz.unlock_at = nil
|
|
@quiz.lock_at = nil
|
|
@quiz.due_at = 2.days.from_now
|
|
end
|
|
|
|
before :each do
|
|
user_session(@user)
|
|
end
|
|
|
|
it "should include assignments with no locks" do
|
|
@quiz.save!
|
|
list = Assignment.not_locked.to_a
|
|
expect(list.size).to eql 1
|
|
expect(list.first.title).to eql 'Test Assignment'
|
|
end
|
|
it "should include assignments with unlock_at in the past" do
|
|
@quiz.unlock_at = 1.day.ago
|
|
@quiz.save!
|
|
list = Assignment.not_locked.to_a
|
|
expect(list.size).to eql 1
|
|
expect(list.first.title).to eql 'Test Assignment'
|
|
end
|
|
it "should include assignments where lock_at is future" do
|
|
@quiz.lock_at = 1.day.from_now
|
|
@quiz.save!
|
|
list = Assignment.not_locked.to_a
|
|
expect(list.size).to eql 1
|
|
expect(list.first.title).to eql 'Test Assignment'
|
|
end
|
|
it "should include assignments where unlock_at is in the past and lock_at is future" do
|
|
@quiz.unlock_at = 1.day.ago
|
|
@quiz.due_at = 1.hour.ago
|
|
@quiz.lock_at = 1.day.from_now
|
|
@quiz.save!
|
|
list = Assignment.not_locked.to_a
|
|
expect(list.size).to be 1
|
|
expect(list.first.title).to eql 'Test Assignment'
|
|
end
|
|
it "should not include assignments where unlock_at is in future" do
|
|
@quiz.unlock_at = 1.hour.from_now
|
|
@quiz.save!
|
|
expect(Assignment.not_locked.count).to eq 0
|
|
end
|
|
it "should not include assignments where lock_at is in past" do
|
|
@quiz.lock_at = 1.hours.ago
|
|
@quiz.save!
|
|
expect(Assignment.not_locked.count).to eq 0
|
|
end
|
|
end
|
|
|
|
context "with_latest_due_date" do
|
|
before :once do
|
|
course_factory
|
|
@s2 = @course.course_sections.create! name: 'other section'
|
|
@dates = (0..7).map { |x| DateTime.new(2020, 1, 10 + x, 12, 0, 0) }
|
|
@a1 = @course.assignments.create!(title: 'no due date')
|
|
@a2 = @course.assignments.create!(title: 'no overrides', due_at: @dates[0])
|
|
@a3 = @course.assignments.create!(title: 'latest is override', due_at: @dates[1])
|
|
assignment_override_model(assignment: @a3, set: @course.default_section, due_at: @dates[2])
|
|
@a4 = @course.assignments.create!(title: 'latest is base', due_at: @dates[4])
|
|
assignment_override_model(assignment: @a4, set: @course.default_section, due_at: @dates[3])
|
|
@a5 = @course.assignments.create!(title: 'two overrides', due_at: @dates[5])
|
|
assignment_override_model(assignment: @a5, set: @course.default_section, due_at: @dates[4])
|
|
assignment_override_model(assignment: @a5, set: @s2, due_at: @dates[6])
|
|
@a6 = @course.assignments.create!(title: 'only overrides')
|
|
assignment_override_model(assignment: @a6, set: @s2, due_at: @dates[6])
|
|
assignment_override_model(assignment: @a6, set: @course.default_section, due_at: @dates[7])
|
|
end
|
|
|
|
it "returns the latest override in each circumstance" do
|
|
assignments = @course.assignments.with_latest_due_date.reorder('latest_due_date').to_a
|
|
expect(assignments.map { |a| [a.title, a.latest_due_date] }).to eq([
|
|
['no overrides', @dates[0]],
|
|
['latest is override', @dates[2]],
|
|
['latest is base', @dates[4]],
|
|
['two overrides', @dates[6]],
|
|
['only overrides', @dates[7]],
|
|
['no due date', nil]
|
|
])
|
|
end
|
|
end
|
|
|
|
context "due_between_with_overrides" do
|
|
before :once do
|
|
@assignment = @course.assignments.create!(:title => 'assignment', :due_at => Time.now)
|
|
@overridden_assignment = @course.assignments.create!(:title => 'overridden_assignment', :due_at => Time.now)
|
|
|
|
override = @assignment.assignment_overrides.build
|
|
override.due_at = Time.now
|
|
override.title = 'override'
|
|
override.save!
|
|
end
|
|
|
|
before :each do
|
|
@results = @course.assignments.due_between_with_overrides(Time.now - 1.day, Time.now + 1.day)
|
|
end
|
|
|
|
it 'should return assignments between the given dates' do
|
|
expect(@results).to include(@assignment)
|
|
end
|
|
|
|
it 'should return overridden assignments that are due between the given dates' do
|
|
expect(@results).to include(@overridden_assignment)
|
|
end
|
|
end
|
|
|
|
context "destroy" do
|
|
before :once do
|
|
group_discussion_assignment
|
|
end
|
|
|
|
it "destroys the associated page if enabled" do
|
|
course_factory
|
|
@course.enable_feature!(:conditional_release)
|
|
wiki_page_assignment_model course: @course
|
|
@assignment.destroy
|
|
expect(@page.reload).to be_deleted
|
|
expect(@assignment.reload).to be_deleted
|
|
end
|
|
|
|
it "does not destroy the associated page" do
|
|
wiki_page_assignment_model
|
|
@assignment.destroy
|
|
expect(@page.reload).not_to be_deleted
|
|
expect(@assignment.reload).to be_deleted
|
|
end
|
|
|
|
it "destroys the associated discussion topic" do
|
|
@assignment.reload.destroy
|
|
expect(@topic.reload).to be_deleted
|
|
expect(@assignment.reload).to be_deleted
|
|
end
|
|
|
|
it "does not revive the discussion if touched after destroyed" do
|
|
@assignment.reload.destroy
|
|
expect(@topic.reload).to be_deleted
|
|
@assignment.touch
|
|
expect(@topic.reload).to be_deleted
|
|
end
|
|
|
|
it 'raises an error on validation error' do
|
|
assignment = Assignment.new
|
|
expect {assignment.destroy}.to raise_error(ActiveRecord::RecordInvalid)
|
|
end
|
|
|
|
it 'refreshes the course participation counts' do
|
|
expect_any_instance_of(Progress).to receive(:process_job)
|
|
.with(@assignment.context, :refresh_content_participation_counts,
|
|
singleton: "refresh_content_participation_counts:#{@assignment.context.global_id}")
|
|
@assignment.destroy
|
|
end
|
|
end
|
|
|
|
describe "#too_many_qs_versions" do
|
|
it "returns if there are too many versions to load at once" do
|
|
quiz_with_graded_submission [], :course => @course, :user => @student
|
|
submissions = @quiz.assignment.submissions
|
|
|
|
Setting.set('too_many_quiz_submission_versions', 3)
|
|
1.times { @quiz_submission.versions.create! }
|
|
expect(@quiz.assignment.too_many_qs_versions?(submissions)).to be_falsey
|
|
|
|
2.times { @quiz_submission.versions.create! }
|
|
expect(@quiz.reload.assignment.too_many_qs_versions?(submissions)).to be_truthy
|
|
end
|
|
end
|
|
|
|
describe "#quiz_submission_versions" do
|
|
it "finds quiz submission versions for submissions" do
|
|
quiz_with_graded_submission([], { :course => @course, :user => @student })
|
|
@quiz.save!
|
|
|
|
assignment = @quiz.assignment
|
|
submissions = assignment.submissions
|
|
too_many = assignment.too_many_qs_versions?(submissions)
|
|
|
|
versions = assignment.quiz_submission_versions(submissions, too_many)
|
|
|
|
expect(versions[@quiz_submission.id].size).to eq 1
|
|
end
|
|
end
|
|
|
|
describe "update_student_submissions" do
|
|
context "grade change events" do
|
|
before(:once) do
|
|
@assignment = @course.assignments.create!
|
|
@assignment.grade_student(@student, grade: 5, grader: @teacher)
|
|
@assistant = User.create!
|
|
@course.enroll_ta(@assistant, enrollment_state: "active")
|
|
end
|
|
|
|
it "triggers a grade change event with the grader_id as the updating_user" do
|
|
@assignment.updating_user = @assistant
|
|
|
|
expect(Auditors::GradeChange).to receive(:record).once do |args|
|
|
expect(args.fetch(:submission).grader_id).to eq @assistant.id
|
|
end
|
|
@assignment.update_student_submissions
|
|
end
|
|
|
|
it "triggers a grade change event using the grader_id on the submission if no updating_user is present" do
|
|
expect(Auditors::GradeChange).to receive(:record).once do |args|
|
|
expect(args.fetch(:submission).grader_id).to eq @teacher.id
|
|
end
|
|
|
|
@assignment.update_student_submissions
|
|
end
|
|
end
|
|
|
|
context "pass/fail assignments" do
|
|
before :once do
|
|
@student1, @student2 = create_users_in_course(@course, 2, return_type: :record)
|
|
@assignment = @course.assignments.create! grading_type: "pass_fail",
|
|
points_possible: 5
|
|
@sub1 = @assignment.grade_student(@student1, grade: "complete", grader: @teacher).first
|
|
@sub2 = @assignment.grade_student(@student2, grade: "incomplete", grader: @teacher).first
|
|
end
|
|
|
|
it "should save a version when changing grades" do
|
|
@assignment.update_attribute :points_possible, 10
|
|
expect(@sub1.reload.version_number).to eq 2
|
|
end
|
|
|
|
it "works for pass/fail assignments" do
|
|
@assignment.update_attribute :points_possible, 10
|
|
expect(@sub1.reload.grade).to eq "complete"
|
|
expect(@sub2.reload.grade).to eq "incomplete"
|
|
end
|
|
|
|
it "works for pass/fail assignments with 0 points possible" do
|
|
@assignment.update_attribute :points_possible, 0
|
|
expect(@sub1.reload.grade).to eq "complete"
|
|
expect(@sub2.reload.grade).to eq "incomplete"
|
|
end
|
|
end
|
|
|
|
context "pass/fail assignments with initial 0 points possible" do
|
|
before :once do
|
|
setup_assignment_without_submission
|
|
@assignment.grading_type = "pass_fail"
|
|
@assignment.points_possible = 0.0
|
|
@assignment.save
|
|
end
|
|
|
|
let(:submission) { @assignment.submissions.first }
|
|
|
|
it "preserves pass/fail grade when changing from 0 to positive points possible" do
|
|
@assignment.grade_student(@user, grade: 'pass', grader: @teacher)
|
|
@assignment.points_possible = 1.0
|
|
@assignment.update_student_submissions
|
|
|
|
submission.reload
|
|
expect(submission.grade).to eql('complete')
|
|
end
|
|
|
|
it "changes the score of 'complete' pass/fail submissions to match the assignment's possible points" do
|
|
@assignment.grade_student(@user, grade: 'pass', grader: @teacher)
|
|
@assignment.points_possible = 3.0
|
|
@assignment.update_student_submissions
|
|
|
|
submission.reload
|
|
expect(submission.score).to eql(3.0)
|
|
end
|
|
|
|
it "does not change the score of 'incomplete' pass/fail submissions if assignment points possible has changed" do
|
|
@assignment.grade_student(@user, grade: 'fail', grader: @teacher)
|
|
@assignment.points_possible = 2.0
|
|
@assignment.update_student_submissions
|
|
|
|
submission.reload
|
|
expect(submission.score).to eql(0.0)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#graded_count' do
|
|
before :once do
|
|
setup_assignment_without_submission
|
|
@assignment.grade_student(@user, grade: 1, grader: @teacher)
|
|
end
|
|
|
|
it 'counts the submissions that have been graded' do
|
|
expect(@assignment.graded_count).to eq 1
|
|
end
|
|
|
|
it 'returns the cached value if present' do
|
|
@assignment = Assignment.select("assignments.*, 50 AS graded_count").where(id: @assignment).first
|
|
expect(@assignment.graded_count).to eq 50
|
|
end
|
|
end
|
|
|
|
describe '#submitted_count' do
|
|
before :once do
|
|
setup_assignment_without_submission
|
|
@assignment.grade_student(@user, grade: 1, grader: @teacher)
|
|
@assignment.submissions.first.update_attribute(:submission_type, 'online_url')
|
|
end
|
|
|
|
it 'counts the submissions that have submission types' do
|
|
expect(@assignment.submitted_count).to eq 1
|
|
end
|
|
|
|
it 'returns the cached value if present' do
|
|
@assignment = Assignment.select("assignments.*, 50 AS submitted_count").where(id: @assignment).first
|
|
expect(@assignment.submitted_count).to eq 50
|
|
end
|
|
end
|
|
|
|
describe "linking overrides with quizzes" do
|
|
let_once(:assignment) { assignment_model(:course => @course, :due_at => 5.days.from_now).reload }
|
|
let_once(:override) { assignment_override_model(:assignment => assignment) }
|
|
|
|
before :once do
|
|
override.override_due_at(7.days.from_now)
|
|
override.save!
|
|
|
|
@override_student = override.assignment_override_students.build
|
|
@override_student.user = @student
|
|
@override_student.save!
|
|
end
|
|
|
|
context "before the assignment has a quiz" do
|
|
context "override" do
|
|
it "has a nil quiz" do
|
|
expect(override.quiz).to be_nil
|
|
end
|
|
|
|
it "has an assignment" do
|
|
expect(override.assignment).to eq assignment
|
|
end
|
|
end
|
|
|
|
context "override student" do
|
|
it "has a nil quiz" do
|
|
expect(@override_student.quiz).to be_nil
|
|
end
|
|
|
|
it "has an assignment" do
|
|
expect(@override_student.assignment).to eq assignment
|
|
end
|
|
end
|
|
end
|
|
|
|
context "once the assignment changes to a quiz submission" do
|
|
before :once do
|
|
assignment.submission_types = "online_quiz"
|
|
assignment.save
|
|
assignment.reload
|
|
override.reload
|
|
@override_student.reload
|
|
end
|
|
|
|
it "has a quiz" do
|
|
expect(assignment.quiz).to be_present
|
|
end
|
|
|
|
context "override" do
|
|
it "has an assignment" do
|
|
expect(override.assignment).to eq assignment
|
|
end
|
|
|
|
it "has the assignment's quiz" do
|
|
expect(override.quiz).to eq assignment.quiz
|
|
end
|
|
end
|
|
|
|
context "override student" do
|
|
it "has an assignment" do
|
|
expect(@override_student.assignment).to eq assignment
|
|
end
|
|
|
|
it "has the assignment's quiz" do
|
|
expect(@override_student.quiz).to eq assignment.quiz
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "updating cached due dates" do
|
|
before :once do
|
|
@assignment = assignment_model(course: @course)
|
|
@assignment.due_at = 2.weeks.from_now
|
|
@assignment.save
|
|
end
|
|
|
|
it "triggers when assignment is created" do
|
|
new_assignment = @course.assignments.build
|
|
expect(DueDateCacher).to receive(:recompute).with(new_assignment, hash_including(update_grades: true))
|
|
new_assignment.save
|
|
end
|
|
|
|
it "triggers when due_at changes" do
|
|
expect(DueDateCacher).to receive(:recompute).with(@assignment, hash_including(update_grades: true))
|
|
@assignment.due_at = 1.week.from_now
|
|
@assignment.save
|
|
end
|
|
|
|
it "triggers when due_at changes to nil" do
|
|
expect(DueDateCacher).to receive(:recompute).with(@assignment, hash_including(update_grades: true))
|
|
@assignment.due_at = nil
|
|
@assignment.save
|
|
end
|
|
|
|
it "triggers when assignment deleted" do
|
|
expect(DueDateCacher).to receive(:recompute).with(@assignment, hash_including(update_grades: true))
|
|
@assignment.destroy
|
|
end
|
|
|
|
it "does not trigger when nothing changed" do
|
|
expect(DueDateCacher).to receive(:recompute).never
|
|
@assignment.save
|
|
end
|
|
end
|
|
|
|
describe "#title_slug" do
|
|
before :once do
|
|
@assignment = assignment_model(course: @course)
|
|
end
|
|
|
|
let(:errors) do
|
|
@assignment.valid?
|
|
@assignment.errors
|
|
end
|
|
|
|
it "should hard truncate at 30 characters" do
|
|
@assignment.title = "a" * 31
|
|
expect(@assignment.title.length).to eq 31
|
|
expect(@assignment.title_slug.length).to eq 30
|
|
expect(@assignment.title).to match /^#{@assignment.title_slug}/
|
|
end
|
|
|
|
it "should not change the title" do
|
|
title = "a" * 31
|
|
@assignment.title = title
|
|
expect(@assignment.title_slug).not_to eq @assignment.title
|
|
expect(@assignment.title).to eq title
|
|
end
|
|
|
|
it "should leave short titles alone" do
|
|
@assignment.title = 'short title'
|
|
expect(@assignment.title_slug).to eq @assignment.title
|
|
end
|
|
|
|
it "should not allow titles over 255 char" do
|
|
@assignment.title = 'a' * 256
|
|
expect(errors[:title]).not_to be_empty
|
|
end
|
|
end
|
|
|
|
describe "due_date" do
|
|
let(:assignment) do
|
|
@course.assignments.new(assignment_valid_attributes)
|
|
end
|
|
|
|
it "is valid when due_date_ok? is true" do
|
|
allow(AssignmentUtil).to receive(:due_date_ok?).and_return(true)
|
|
expect(assignment.valid?).to eq(true)
|
|
end
|
|
|
|
it "is not valid when due_date_ok? is false" do
|
|
allow(AssignmentUtil).to receive(:due_date_ok?).and_return(false)
|
|
expect(assignment.valid?).to eq(false)
|
|
end
|
|
end
|
|
|
|
describe "validate_assignment_overrides_due_date" do
|
|
let(:section_1) { @course.course_sections.create!(name: "section 1") }
|
|
let(:section_2) { @course.course_sections.create!(name: "section 2") }
|
|
|
|
let(:assignment) do
|
|
@course.assignments.create!(assignment_valid_attributes)
|
|
end
|
|
|
|
describe "when an override has no due date" do
|
|
before do
|
|
# Create an override with a due date
|
|
create_section_override_for_assignment(assignment, course_section: section_1)
|
|
|
|
# Create an override without a due date
|
|
override = create_section_override_for_assignment(assignment, course_section: section_2)
|
|
override.due_at = nil
|
|
override.save
|
|
end
|
|
|
|
it "is not valid when AssignmentUtil.due_date_required? is true" do
|
|
allow(AssignmentUtil).to receive(:due_date_required?).and_return(true)
|
|
expect(assignment.valid?).to eq(false)
|
|
end
|
|
|
|
it "is valid when AssignmentUtil.due_date_required? is false" do
|
|
allow(AssignmentUtil).to receive(:due_date_required?).and_return(false)
|
|
expect(assignment.valid?).to eq(true)
|
|
end
|
|
end
|
|
|
|
describe "when all overrides have a due date" do
|
|
before do
|
|
# Create 2 overrides with due dates
|
|
create_section_override_for_assignment(assignment, course_section: section_1)
|
|
create_section_override_for_assignment(assignment, course_section: section_2)
|
|
end
|
|
|
|
it "is valid when AssignmentUtil.due_date_required? is true" do
|
|
allow(AssignmentUtil).to receive(:due_date_required?).and_return(true)
|
|
expect(assignment.valid?).to eq(true)
|
|
end
|
|
|
|
it "is valid when AssignmentUtil.due_date_required? is false" do
|
|
allow(AssignmentUtil).to receive(:due_date_required?).and_return(false)
|
|
expect(assignment.valid?).to eq(true)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "due_date_required?" do
|
|
let(:assignment) do
|
|
@course.assignments.create!(assignment_valid_attributes)
|
|
end
|
|
|
|
it "is true when due_date_required? is true" do
|
|
allow(AssignmentUtil).to receive(:due_date_required?).and_return(true)
|
|
expect(assignment.due_date_required?).to eq(true)
|
|
end
|
|
|
|
it "is false when due_date_required? is false" do
|
|
allow(AssignmentUtil).to receive(:due_date_required?).and_return(false)
|
|
expect(assignment.due_date_required?).to eq(false)
|
|
end
|
|
end
|
|
|
|
describe "external_tool_tag" do
|
|
it "should update the existing tag when updating the assignment" do
|
|
a = @course.assignments.create!(title: "test",
|
|
submission_types: 'external_tool',
|
|
external_tool_tag_attributes: {url: "http://example.com/launch"})
|
|
tag = a.external_tool_tag
|
|
expect(tag).not_to be_new_record
|
|
|
|
a = Assignment.find(a.id)
|
|
a.attributes = {external_tool_tag_attributes: {url: "http://example.com/launch2"}}
|
|
a.save!
|
|
expect(a.external_tool_tag.url).to eq "http://example.com/launch2"
|
|
expect(a.external_tool_tag).to eq tag
|
|
end
|
|
end
|
|
|
|
describe "allowed_extensions=" do
|
|
it "should accept a string as input" do
|
|
a = Assignment.new
|
|
a.allowed_extensions = "doc,xls,txt"
|
|
expect(a.allowed_extensions).to eq ["doc", "xls", "txt"]
|
|
end
|
|
|
|
it "should accept an array as input" do
|
|
a = Assignment.new
|
|
a.allowed_extensions = ["doc", "xls", "txt"]
|
|
expect(a.allowed_extensions).to eq ["doc", "xls", "txt"]
|
|
end
|
|
|
|
it "should sanitize the string" do
|
|
a = Assignment.new
|
|
a.allowed_extensions = ".DOC, .XLS, .TXT"
|
|
expect(a.allowed_extensions).to eq ["doc", "xls", "txt"]
|
|
end
|
|
|
|
it "should sanitize the array" do
|
|
a = Assignment.new
|
|
a.allowed_extensions = [".DOC", " .XLS", " .TXT"]
|
|
expect(a.allowed_extensions).to eq ["doc", "xls", "txt"]
|
|
end
|
|
end
|
|
|
|
describe '#generate_comments_from_files_legacy' do
|
|
before :once do
|
|
@students = create_users_in_course(@course, 3, return_type: :record)
|
|
|
|
@assignment = @course.assignments.create! :name => "zip upload test",
|
|
:submission_types => %w(online_upload)
|
|
end
|
|
|
|
it "should work for individuals" do
|
|
s1 = @students.first
|
|
submit_homework(s1)
|
|
|
|
zip = zip_submissions_legacy
|
|
|
|
comments, ignored = @assignment.generate_comments_from_files_legacy(
|
|
zip.open.path,
|
|
@teacher)
|
|
|
|
expect(comments.map { |g| g.map { |c| c.submission.user } }).to eq [[s1]]
|
|
expect(ignored).to be_empty
|
|
end
|
|
|
|
it "should work for groups" do
|
|
s1, s2 = @students
|
|
|
|
gc = @course.group_categories.create! name: "Homework Groups"
|
|
@assignment.update group_category_id: gc.id,
|
|
grade_group_students_individually: false
|
|
g1, g2 = 2.times.map { |i| gc.groups.create! name: "Group #{i}", context: @course }
|
|
g1.add_user(s1)
|
|
g1.add_user(s2)
|
|
|
|
submit_homework(s1)
|
|
zip = zip_submissions_legacy
|
|
|
|
comments, _ = @assignment.generate_comments_from_files_legacy(
|
|
zip.open.path,
|
|
@teacher)
|
|
|
|
expect(comments.map { |g|
|
|
g.map { |c| c.submission.user }.sort_by(&:id)
|
|
}).to eq [[s1, s2]]
|
|
end
|
|
|
|
it "excludes student names from filenames when anonymous grading is enabled" do
|
|
@assignment.update!(anonymous_grading: true)
|
|
|
|
s1 = @students.first
|
|
att = submit_homework(s1)
|
|
sub = @assignment.submissions.where(:user_id => s1).first
|
|
|
|
zip = zip_submissions_legacy
|
|
filename = Zip::File.new(zip.open).entries.map(&:name).first
|
|
expect(filename).to eq "anon_#{sub.anonymous_id}_#{att.id}_homework.pdf"
|
|
|
|
comments, ignored = @assignment.generate_comments_from_files_legacy(
|
|
zip.open.path,
|
|
@teacher)
|
|
|
|
expect(comments.map { |g| g.map { |c| c.submission.user } }).to eq [[s1]]
|
|
expect(ignored).to be_empty
|
|
end
|
|
end
|
|
|
|
describe "generating comments from files" do
|
|
let(:attachment_data) { {uploaded_data: stub_file_data("submissions.zip", "", "application/zip")} }
|
|
|
|
before :once do
|
|
Account.site_admin.enable_feature!(:submissions_reupload_status_page)
|
|
@students = create_users_in_course(@course, 3, return_type: :record)
|
|
|
|
@assignment = @course.assignments.create! name: "zip upload test",
|
|
submission_types: %w(online_upload)
|
|
end
|
|
|
|
def zip_submissions
|
|
zip = Attachment.new filename: 'submissions.zip'
|
|
zip.user = @teacher
|
|
zip.workflow_state = 'to_be_zipped'
|
|
zip.context = @assignment
|
|
zip.save!
|
|
|
|
# add all submissions from the assignment to the zip file
|
|
ContentZipper.process_attachment(zip, @teacher)
|
|
raise "zip failed" if zip.workflow_state != "zipped"
|
|
|
|
# return a tempfile for use in generating comments
|
|
zip.open
|
|
end
|
|
|
|
def generate_comments(user)
|
|
tempfile = zip_submissions
|
|
|
|
# create an uploaded file with the zipped submissions, as would be uploaded by the user
|
|
uploaded_data = ActionDispatch::Http::UploadedFile.new(tempfile: tempfile, filename: "submissions.zip")
|
|
|
|
@assignment.generate_comments_from_files_later(
|
|
{uploaded_data: uploaded_data},
|
|
user
|
|
)
|
|
|
|
# invoke the job that was created by the previous step
|
|
job = Delayed::Job.where(tag: "Assignment#generate_comments_from_files").order(:id).last
|
|
job.invoke_job
|
|
end
|
|
|
|
it "should work for individuals" do
|
|
s1 = @students.first
|
|
submit_homework(s1)
|
|
|
|
generate_comments(@teacher)
|
|
results = @assignment.submission_reupload_progress.results
|
|
|
|
expect(results[:comments].map { |c| c[:submission][:user_id] }).to eq [s1.id]
|
|
expect(results[:ignored_files]).to be_empty
|
|
end
|
|
|
|
it "should work for groups" do
|
|
s1, s2 = @students
|
|
|
|
gc = @course.group_categories.create! name: "Homework Groups"
|
|
@assignment.update group_category_id: gc.id,
|
|
grade_group_students_individually: false
|
|
g1, g2 = 2.times.map { |i| gc.groups.create! name: "Group #{i}", context: @course }
|
|
g1.add_user(s1)
|
|
g1.add_user(s2)
|
|
|
|
submit_homework(s1)
|
|
|
|
generate_comments(@teacher)
|
|
results = @assignment.submission_reupload_progress.results
|
|
submission_user_ids = results[:comments].map { |c| c[:submission][:user_id] }
|
|
|
|
expect(submission_user_ids.sort).to eq [s1.id, s2.id]
|
|
end
|
|
|
|
it "excludes student names from filenames when anonymous grading is enabled" do
|
|
@assignment.update!(anonymous_grading: true)
|
|
|
|
s1 = @students.first
|
|
submit_homework(s1)
|
|
|
|
generate_comments(@teacher)
|
|
results = @assignment.submission_reupload_progress.results
|
|
|
|
expect(results[:comments].map { |c| c[:submission][:user_id] }).to eq [s1.id]
|
|
expect(results[:ignored_files]).to be_empty
|
|
end
|
|
|
|
describe "newly-created comments" do
|
|
before(:each) do
|
|
@assignment = @course.assignments.create!(name: "Mute Comment Test", submission_types: %w(online_upload))
|
|
end
|
|
|
|
let(:added_comment) { @assignment.submission_for_student(@student).submission_comments.last }
|
|
|
|
context "for a manually-posted assignment" do
|
|
before(:each) do
|
|
@assignment.post_policy.update!(post_manually: true)
|
|
end
|
|
|
|
it "hides new comments if the submission is not posted" do
|
|
submit_homework(@student)
|
|
generate_comments(@user)
|
|
expect(added_comment).to be_hidden
|
|
end
|
|
|
|
it "shows new comments if the submission is posted" do
|
|
submit_homework(@student)
|
|
@assignment.post_submissions
|
|
|
|
generate_comments(@user)
|
|
expect(added_comment).not_to be_hidden
|
|
end
|
|
end
|
|
|
|
context "for a automatically-posted assignment" do
|
|
it "shows new comments if the submission is posted" do
|
|
submit_homework(@student)
|
|
@assignment.post_submissions
|
|
|
|
generate_comments(@user)
|
|
expect(added_comment).not_to be_hidden
|
|
end
|
|
|
|
it "hides new comments if the submission is graded but not posted" do
|
|
submit_homework(@student)
|
|
@assignment.grade_student(@student, grade: 1, grader: @teacher)
|
|
@assignment.hide_submissions
|
|
|
|
generate_comments(@user)
|
|
expect(added_comment).to be_hidden
|
|
end
|
|
|
|
it "shows new comments if the submission is neither graded nor posted" do
|
|
submit_homework(@student)
|
|
|
|
generate_comments(@user)
|
|
expect(added_comment).not_to be_hidden
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#restore" do
|
|
it "restores to unpublished if draft state w/ no submissions" do
|
|
assignment_model course: @course
|
|
@a.destroy
|
|
@a.restore
|
|
expect(@a.reload).to be_unpublished
|
|
end
|
|
|
|
it "restores to published if draft state w/ submissions" do
|
|
setup_assignment_with_homework
|
|
@assignment.destroy
|
|
@assignment.restore
|
|
expect(@assignment.reload).to be_published
|
|
end
|
|
|
|
it 'refreshes the course participation counts' do
|
|
assignment = assignment_model(course: @course)
|
|
assignment.destroy
|
|
expect_any_instance_of(Progress).to receive(:process_job)
|
|
.with(assignment.context, :refresh_content_participation_counts,
|
|
singleton: "refresh_content_participation_counts:#{assignment.context.global_id}").
|
|
once
|
|
assignment.restore
|
|
end
|
|
end
|
|
|
|
describe '#readable_submission_type' do
|
|
it "should work for on paper assignments" do
|
|
assignment_model(:submission_types => 'on_paper', :course => @course)
|
|
expect(@assignment.readable_submission_types).to eq 'on paper'
|
|
end
|
|
end
|
|
|
|
describe '#update_grading_period_grades with no grading periods' do
|
|
before :once do
|
|
assignment_model(course: @course)
|
|
end
|
|
|
|
it 'should not update grades when due_at changes' do
|
|
expect(@assignment.context).to receive(:recompute_student_scores).never
|
|
@assignment.due_at = 6.months.ago
|
|
@assignment.save!
|
|
end
|
|
end
|
|
|
|
describe '#update_grading_period_grades' do
|
|
before :once do
|
|
assignment_model(course: @course)
|
|
@grading_period_group = @course.root_account.grading_period_groups.create!(title: "Example Group")
|
|
@grading_period_group.enrollment_terms << @course.enrollment_term
|
|
@grading_period_group.grading_periods.create!(
|
|
title: 'GP1',
|
|
start_date: 9.months.ago,
|
|
end_date: 5.months.ago
|
|
)
|
|
@grading_period_group.grading_periods.create!(
|
|
title: 'GP2',
|
|
start_date: 4.months.ago,
|
|
end_date: 2.months.from_now
|
|
)
|
|
@course.enrollment_term.save!
|
|
@assignment.reload
|
|
end
|
|
|
|
it 'should update grades when due_at changes to a grading period' do
|
|
expect(@assignment.context).to receive(:recompute_student_scores).twice
|
|
@assignment.due_at = 6.months.ago
|
|
@assignment.save!
|
|
end
|
|
|
|
it 'should update grades twice when due_at changes to another grading period' do
|
|
@assignment.due_at = 1.month.ago
|
|
@assignment.save!
|
|
expect(@assignment.context).to receive(:recompute_student_scores).twice
|
|
@assignment.due_at = 6.months.ago
|
|
@assignment.save!
|
|
end
|
|
|
|
it 'should not update grades if grading period did not change' do
|
|
@assignment.due_at = 1.month.ago
|
|
@assignment.save!
|
|
expect(@assignment.context).to receive(:recompute_student_scores).never
|
|
@assignment.due_at = 2.months.ago
|
|
@assignment.save!
|
|
end
|
|
end
|
|
|
|
describe '#update_submissions_and_grades_if_details_changed' do
|
|
before :once do
|
|
@assignment = @course.assignments.create! grading_type: "points", points_possible: 5
|
|
student1, student2 = create_users_in_course(@course, 2, return_type: :record)
|
|
@assignment.grade_student(student1, grade: 3, grader: @teacher).first
|
|
@assignment.grade_student(student2, grade: 2, grader: @teacher).first
|
|
end
|
|
|
|
it "should update grades if points_possible changes" do
|
|
expect(@assignment.context).to receive(:recompute_student_scores).once
|
|
@assignment.points_possible = 3
|
|
@assignment.save!
|
|
end
|
|
|
|
it "should update grades if workflow_state changes" do
|
|
expect(@assignment.context).to receive(:recompute_student_scores).once
|
|
@assignment.unpublish
|
|
end
|
|
|
|
it "updates when omit_from_final_grade changes" do
|
|
expect(@assignment.context).to receive(:recompute_student_scores).once
|
|
@assignment.update_attribute :omit_from_final_grade, true
|
|
end
|
|
|
|
it "updates when grading_type changes" do
|
|
expect(@assignment.context).to receive(:recompute_student_scores).once
|
|
@assignment.update_attribute :grading_type, "percent"
|
|
end
|
|
|
|
it "should not update grades otherwise" do
|
|
expect(@assignment.context).to receive(:recompute_student_scores).never
|
|
@assignment.title = 'hi'
|
|
@assignment.due_at = 1.hour.ago
|
|
@assignment.description = 'blah'
|
|
@assignment.save!
|
|
end
|
|
end
|
|
|
|
describe '#add_submission_comment' do
|
|
let(:assignment) { assignment_model(course: @course) }
|
|
|
|
it 'raises an error if original_student is nil' do
|
|
expect {
|
|
assignment.add_submission_comment(nil)
|
|
}.to raise_error 'Student Required'
|
|
end
|
|
|
|
context 'when the student is not in a group' do
|
|
let!(:associate_student_and_submission) {
|
|
assignment.submissions.find_by user: @student
|
|
}
|
|
let(:update_submission_response) {
|
|
assignment.add_submission_comment(@student, comment: 'WAT?')
|
|
}
|
|
|
|
it 'returns an Array' do
|
|
expect(update_submission_response.class).to eq Array
|
|
end
|
|
|
|
it 'returns a collection of submission comments' do
|
|
expect(update_submission_response.first.class).to eq SubmissionComment
|
|
end
|
|
end
|
|
|
|
context 'when the student is in a group' do
|
|
let!(:create_a_group_with_a_submitted_assignment) {
|
|
setup_assignment_with_group
|
|
@assignment.submit_homework(
|
|
@u1,
|
|
submission_type: 'online_text_entry',
|
|
body: 'Some text for you'
|
|
)
|
|
}
|
|
|
|
context 'when a comment is submitted' do
|
|
let(:update_assignment_with_comment) {
|
|
@assignment.add_submission_comment(
|
|
@u2,
|
|
comment: 'WAT?',
|
|
group_comment: true,
|
|
user_id: @course.teachers.first.id
|
|
)
|
|
}
|
|
|
|
it 'returns an Array' do
|
|
expect(update_assignment_with_comment).to be_an_instance_of Array
|
|
end
|
|
|
|
it 'creates a comment for each student in the group' do
|
|
expect {
|
|
update_assignment_with_comment
|
|
}.to change{ SubmissionComment.count }.by(@u1.groups.first.users.count)
|
|
end
|
|
|
|
it 'creates comments with the same group_comment_id' do
|
|
comments = update_assignment_with_comment
|
|
expect(comments.first.group_comment_id).to eq comments.last.group_comment_id
|
|
end
|
|
end
|
|
|
|
context 'when a comment is not submitted' do
|
|
it 'returns an Array' do
|
|
expect(@assignment.add_submission_comment(@u2).class).to eq Array
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#update_submission" do
|
|
let(:assignment) { assignment_model(course: @course) }
|
|
|
|
it "raises an error if original_student is nil" do
|
|
expect {
|
|
assignment.update_submission(nil)
|
|
}.to raise_error "Student Required"
|
|
end
|
|
|
|
context "when the student is not in a group" do
|
|
let!(:associate_student_and_submission) {
|
|
assignment.submissions.find_by user: @student
|
|
}
|
|
let(:update_submission_response) { assignment.update_submission(@student) }
|
|
|
|
it "returns an Array" do
|
|
expect(update_submission_response.class).to eq Array
|
|
end
|
|
|
|
it "returns a collection of submissions" do
|
|
assignment.update_submission(@student).first
|
|
expect(update_submission_response.first.class).to eq Submission
|
|
end
|
|
end
|
|
|
|
context "when the student is in a group" do
|
|
let!(:create_a_group_with_a_submitted_assignment) {
|
|
setup_assignment_with_group
|
|
@assignment.submit_homework(
|
|
@u1,
|
|
submission_type: "online_text_entry",
|
|
body: "Some text for you"
|
|
)
|
|
}
|
|
|
|
context "when a comment is submitted" do
|
|
let(:update_assignment_with_comment) {
|
|
@assignment.update_submission(
|
|
@u2,
|
|
comment: "WAT?",
|
|
group_comment: true,
|
|
user_id: @course.teachers.first.id
|
|
)
|
|
}
|
|
|
|
it "returns an Array" do
|
|
expect(update_assignment_with_comment).to be_an_instance_of Array
|
|
end
|
|
|
|
it "creates a comment for each student in the group" do
|
|
expect {
|
|
update_assignment_with_comment
|
|
}.to change{ SubmissionComment.count }.by(@u1.groups.first.users.count)
|
|
end
|
|
|
|
it "creates comments with the same group_comment_id" do
|
|
update_assignment_with_comment
|
|
comments = SubmissionComment.last(@u1.groups.first.users.count)
|
|
expect(comments.first.group_comment_id).to eq comments.last.group_comment_id
|
|
end
|
|
end
|
|
|
|
context "when a comment is not submitted" do
|
|
it "returns an Array" do
|
|
expect(@assignment.update_submission(@u2).class).to eq Array
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#in_closed_grading_period?' do
|
|
subject(:assignment) { @course.assignments.create! }
|
|
|
|
context 'when there are no grading periods' do
|
|
it { is_expected.not_to be_in_closed_grading_period }
|
|
end
|
|
|
|
context 'when there is a past and current grading period' do
|
|
before(:once) do
|
|
@old, @current = create_grading_periods_for(@course, grading_periods: [:old, :current])
|
|
end
|
|
|
|
context 'when there are no submissions in a closed grading period' do
|
|
it { is_expected.not_to be_in_closed_grading_period }
|
|
end
|
|
|
|
context 'when there are at least one submission in a closed grading period' do
|
|
before { assignment.update!(due_at: 3.months.ago) }
|
|
|
|
it { is_expected.to be_in_closed_grading_period }
|
|
|
|
context 'when a grading period is deleted for a submission' do
|
|
before { @old.grading_period_group.destroy }
|
|
it { is_expected.not_to be_in_closed_grading_period }
|
|
end
|
|
end
|
|
|
|
context 'when a single submission is in a closed grading period via overrides' do
|
|
let(:user) { student_in_course(active_all: true, user_name: 'another student').user }
|
|
|
|
before { create_adhoc_override_for_assignment(assignment, user, due_at: 3.months.ago) }
|
|
|
|
it { is_expected.to be_in_closed_grading_period }
|
|
end
|
|
|
|
context 'when there is a soft deleted closed grading period pointed at by concluded submissions' do
|
|
before do
|
|
# We need to set up a situation where a submission owned by
|
|
# a concluded enrollment points at a soft deleted grading
|
|
# period that would be considered closed.
|
|
student_enrollment = student_in_course(course: assignment.context, active_all: true, user_name: 'another student')
|
|
current_dup = @current.dup
|
|
assignment.update(due_at: 45.days.ago(Time.zone.now))
|
|
@current.update!(end_date: 1.month.ago(Time.zone.now))
|
|
student_enrollment.conclude
|
|
@current.destroy!
|
|
current_dup.save!
|
|
end
|
|
|
|
context "without preloaded submissions" do
|
|
it { is_expected.not_to be_in_closed_grading_period }
|
|
end
|
|
|
|
context "with preloaded submissions" do
|
|
before { assignment.submissions.load }
|
|
it { is_expected.not_to be_in_closed_grading_period }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "basic validation" do
|
|
describe "possible points" do
|
|
it "does not allow a negative value" do
|
|
assignment = Assignment.new(points_possible: -1)
|
|
assignment.valid?
|
|
expect(assignment.errors.keys.include?(:points_possible)).to be_truthy
|
|
end
|
|
|
|
it "allows a nil value" do
|
|
assignment = Assignment.new(points_possible: nil)
|
|
assignment.valid?
|
|
expect(assignment.errors.keys.include?(:points_possible)).to be_falsey
|
|
end
|
|
|
|
it "allows a 0 value" do
|
|
assignment = Assignment.new(points_possible: 0)
|
|
assignment.valid?
|
|
expect(assignment.errors.keys.include?(:points_possible)).to be_falsey
|
|
end
|
|
|
|
it "allows a positive value" do
|
|
assignment = Assignment.new(points_possible: 13)
|
|
assignment.valid?
|
|
expect(assignment.errors.keys.include?(:points_possible)).to be_falsey
|
|
end
|
|
|
|
it "does not attempt validation unless points_possible has changed" do
|
|
assignment = Assignment.new(points_possible: -13)
|
|
allow(assignment).to receive(:points_possible_changed?).and_return(false)
|
|
assignment.valid?
|
|
expect(assignment.errors.keys.include?(:points_possible)).to be_falsey
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#a2_enabled?' do
|
|
before do
|
|
allow(@course).to receive(:feature_enabled?) { false }
|
|
allow(@course).to receive(:feature_enabled?).with(:assignments_2_student) { true }
|
|
end
|
|
|
|
let(:assignment) do
|
|
@course.assignments.create!(assignment_valid_attributes)
|
|
end
|
|
|
|
it 'returns false if the assignment_2_student flag is not enabled' do
|
|
allow(@course).to receive(:feature_enabled?).with(:assignments_2_student) { false }
|
|
assignment.submission_types = 'online_text_entry'
|
|
|
|
expect(assignment.a2_enabled?).to be(false)
|
|
end
|
|
|
|
[
|
|
'discussion_topic',
|
|
'external_tool',
|
|
'on_paper',
|
|
'online_quiz',
|
|
'none',
|
|
'not_graded',
|
|
'wiki_page',
|
|
''
|
|
].each do |type|
|
|
it "returns false if submission type is set to #{type}" do
|
|
assignment.build_wiki_page
|
|
assignment.build_discussion_topic
|
|
assignment.build_quiz
|
|
assignment.submission_types = type
|
|
|
|
expect(assignment.a2_enabled?).to be(false)
|
|
end
|
|
end
|
|
|
|
[
|
|
'online_text_entry',
|
|
'online_upload',
|
|
'online_url'
|
|
].each do |type|
|
|
it "returns true if the flag is on and the submission type is #{type}" do
|
|
assignment.submission_types = type
|
|
expect(assignment.a2_enabled?).to be(true)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'title validation' do
|
|
let(:assignment) do
|
|
@course.assignments.create!(assignment_valid_attributes)
|
|
end
|
|
let(:errors) {
|
|
assignment.valid?
|
|
assignment.errors
|
|
}
|
|
|
|
it 'must allow a title equal to the maximum length' do
|
|
assignment.title = 'a' * Assignment.maximum_string_length
|
|
expect(errors[:title]).to be_empty
|
|
end
|
|
|
|
it 'must not allow a title longer than the maximum length' do
|
|
assignment.title = 'a' * (Assignment.maximum_string_length + 1)
|
|
expect(errors[:title]).not_to be_empty
|
|
end
|
|
|
|
it 'must allow a blank title when it is unchanged and was previously blank' do
|
|
assignment.title = ''
|
|
assignment.save(validate: false)
|
|
|
|
assignment.valid?
|
|
errors = assignment.errors
|
|
expect(errors[:title]).to be_empty
|
|
end
|
|
|
|
it 'must not allow the title to be blank if changed' do
|
|
assignment.title = ' '
|
|
assignment.valid?
|
|
errors = assignment.errors
|
|
expect(errors[:title]).not_to be_empty
|
|
end
|
|
end
|
|
|
|
describe "#ensure_post_to_sis_valid" do
|
|
let(:assignment) { assignment_model(course: @course, post_to_sis: true) }
|
|
|
|
it "sets post_to_sis to false if the assignment is not_graded" do
|
|
assignment.submission_types = 'not_graded'
|
|
assignment.save!
|
|
|
|
expect(assignment.post_to_sis).to eq false
|
|
end
|
|
|
|
it "sets post_to_sis to false if the assignment is a wiki_page" do
|
|
assignment.submission_types = 'wiki_page'
|
|
assignment.save!
|
|
|
|
expect(assignment.post_to_sis).to eq false
|
|
end
|
|
|
|
it "does not set post_to_sis to false for other assignments" do
|
|
expect(assignment.post_to_sis).to eq true
|
|
end
|
|
end
|
|
|
|
describe "validate_overrides_for_sis" do
|
|
def api_create_assignment_in_course(course,assignment_params)
|
|
api_call(:post,
|
|
"/api/v1/courses/#{course.id}/assignments.json",
|
|
{
|
|
:controller => 'assignments_api',
|
|
:action => 'create',
|
|
:format => 'json',
|
|
:course_id => course.id.to_s
|
|
}, {:assignment => assignment_params })
|
|
end
|
|
|
|
let(:assignment) do
|
|
@course.assignments.new(assignment_valid_attributes)
|
|
end
|
|
|
|
before do
|
|
assignment.post_to_sis = true
|
|
allow(assignment.context.account).to receive(:sis_syncing).and_return({value: true})
|
|
allow(assignment.context.account).to receive(:feature_enabled?).with('new_sis_integrations').and_return(true)
|
|
allow(assignment.context.account).to receive(:sis_require_assignment_due_date).and_return({value: true})
|
|
end
|
|
|
|
it "raises an invalid record error if overrides are invalid" do
|
|
overrides = [{
|
|
'course_section_id' => @course.default_section.id,
|
|
'due_at' => nil
|
|
}]
|
|
expect{assignment.validate_overrides_for_sis(overrides)}.to raise_error(ActiveRecord::RecordInvalid)
|
|
end
|
|
end
|
|
|
|
describe "when sis sync with required due dates is enabled" do
|
|
before :each do
|
|
@assignment = assignment_model(course: @course)
|
|
@overrides = {
|
|
:overrides_to_create=>[],
|
|
:overrides_to_update=>[],
|
|
:overrides_to_delete=>[],
|
|
:override_errors=>[]
|
|
}
|
|
allow(AssignmentUtil).to receive(:due_date_required?).and_return(true)
|
|
allow(AssignmentUtil).to receive(:due_date_required_for_account?).and_return(true)
|
|
allow(AssignmentUtil).to receive(:sis_integration_settings_enabled?).and_return(true)
|
|
end
|
|
|
|
it "can duplicate" do
|
|
create_section_override_for_assignment(@assignment)
|
|
assignment_duplicate = @assignment.duplicate
|
|
expect(assignment_duplicate.save).to eq(true)
|
|
end
|
|
|
|
context "checking if overrides are valid" do
|
|
it "is valid if a new override has a due date" do
|
|
override = assignment_override_model(assignment: @assignment, due_at: 2.days.from_now)
|
|
@overrides[:overrides_to_create].push(override)
|
|
expect{@assignment.validate_overrides_for_sis(@overrides)}.not_to raise_error
|
|
end
|
|
|
|
it "is valid if an override has a due date and everyone else does not have a due date" do
|
|
@assignment.due_at = nil
|
|
create_section_override_for_assignment(@assignment)
|
|
expect{@assignment.validate_overrides_for_sis(@overrides)}.not_to raise_error
|
|
end
|
|
|
|
it "is invalid if a new override does not have a due date" do
|
|
override = assignment_override_model(assignment: @assignment, due_at: nil, due_at_overridden: false)
|
|
@overrides[:overrides_to_create].push(override)
|
|
expect{@assignment.validate_overrides_for_sis(@overrides)}.to raise_error(ActiveRecord::RecordInvalid)
|
|
end
|
|
|
|
it "is invalid if an active existing override does not have a due date" do
|
|
create_section_override_for_assignment(@assignment, due_at: nil, due_at_overridden: false)
|
|
expect{@assignment.validate_overrides_for_sis(@overrides)}.to raise_error(ActiveRecord::RecordInvalid)
|
|
end
|
|
|
|
it "is valid if a deleted existing override does not have a due date" do
|
|
create_section_override_for_assignment(@assignment, due_at: nil, due_at_overridden: false,
|
|
workflow_state: 'deleted')
|
|
expect{@assignment.validate_overrides_for_sis(@overrides)}.not_to raise_error
|
|
end
|
|
|
|
it "is invalid if updating an override to not set a due date" do
|
|
db_override = create_section_override_for_assignment(@assignment)
|
|
update_override = db_override.clone
|
|
update_override[:id] = db_override[:id]
|
|
update_override[:due_at] = nil
|
|
@overrides[:overrides_to_update].push(update_override)
|
|
expect{@assignment.validate_overrides_for_sis(@overrides)}.to raise_error(ActiveRecord::RecordInvalid)
|
|
end
|
|
|
|
it "is valid if an existing override has no due date, but the update sets a due date" do
|
|
db_override = assignment_override_model(assignment: @assignment, due_at: nil)
|
|
update_override = db_override.clone
|
|
update_override[:id] = db_override[:id]
|
|
update_override[:due_at] = 2.days.from_now
|
|
@overrides[:overrides_to_update].push(update_override)
|
|
expect{@assignment.validate_overrides_for_sis(@overrides)}.not_to raise_error
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "max_name_length" do
|
|
let(:assignment) do
|
|
@course.assignments.new(assignment_valid_attributes)
|
|
end
|
|
|
|
it "returns custom name length if sis_assignment_name_length_input is present" do
|
|
assignment.post_to_sis = true
|
|
allow(assignment.context.account).to receive(:sis_syncing).and_return({value: true})
|
|
allow(assignment.context.account).to receive(:sis_assignment_name_length).and_return({value: true})
|
|
allow(assignment.context.account).to receive(:feature_enabled?).with('new_sis_integrations').and_return(true)
|
|
allow(assignment.context.account).to receive(:sis_assignment_name_length_input).and_return({value: 15})
|
|
expect(assignment.max_name_length).to eq(15)
|
|
end
|
|
|
|
it "returns default of 255 if sis_assignment_name_length_input is not present " do
|
|
expect(assignment.max_name_length).to eq(255)
|
|
end
|
|
end
|
|
|
|
describe "group category validation" do
|
|
before :once do
|
|
@group_category = @course.group_categories.create! name: "groups"
|
|
@groups = 2.times.map { |i|
|
|
@group_category.groups.create! name: "group #{i}", context: @course
|
|
}
|
|
end
|
|
|
|
let_once(:a1) { assignment }
|
|
|
|
def assignment(group_category = nil)
|
|
a = @course.assignments.build name: "test"
|
|
a.group_category = group_category
|
|
a.tap &:save!
|
|
end
|
|
|
|
it "lets you change group category attributes before homework is submitted" do
|
|
a1.group_category = @group_category
|
|
expect(a1).to be_valid
|
|
|
|
a2 = assignment(@group_category)
|
|
a2.group_category = nil
|
|
expect(a2).to be_valid
|
|
end
|
|
|
|
it "doesn't let you change group category attributes after homework is submitted" do
|
|
a1.submit_homework @student, body: "hello, world"
|
|
a1.group_category = @group_category
|
|
expect(a1).not_to be_valid
|
|
|
|
a2 = assignment(@group_category)
|
|
a2.submit_homework @student, body: "hello, world"
|
|
a2.group_category = nil
|
|
expect(a2).not_to be_valid
|
|
end
|
|
|
|
it "recognizes if it has submissions and belongs to a deleted group category" do
|
|
a1.group_category = @group_category
|
|
a1.submit_homework @student, body: "hello, world"
|
|
expect(a1.group_category_deleted_with_submissions?).to eq false
|
|
a1.group_category.destroy
|
|
expect(a1.group_category_deleted_with_submissions?).to eq true
|
|
|
|
a2 = assignment(@group_category)
|
|
a2.group_category.destroy
|
|
expect(a2.group_category_deleted_with_submissions?).to eq false
|
|
end
|
|
|
|
context 'when anonymous grading is enabled from before' do
|
|
before :each do
|
|
a1.group_category = nil
|
|
a1.anonymous_grading = true
|
|
a1.save!
|
|
|
|
a1.group_category = @group_category
|
|
end
|
|
|
|
it 'invalidates the record' do
|
|
expect(a1).not_to be_valid
|
|
end
|
|
|
|
it 'adds a validation error on the group category field' do
|
|
a1.valid?
|
|
|
|
expected_validation_error = "Anonymously graded assignments can't be group assignments"
|
|
expect(a1.errors[:group_category_id]).to eq([expected_validation_error])
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'anonymous grading validation' do
|
|
before :once do
|
|
@group_category = @course.group_categories.create! name: "groups"
|
|
@groups = Array.new(2) do |i|
|
|
@group_category.groups.create! name: "group #{i}", context: @course
|
|
end
|
|
|
|
@assignment = @course.assignments.build(name: "Assignment")
|
|
@assignment.save!
|
|
end
|
|
|
|
context 'when group_category is enabled from before' do
|
|
before :each do
|
|
@assignment.group_category = @group_category
|
|
@assignment.save!
|
|
|
|
@assignment.anonymous_grading = true
|
|
end
|
|
|
|
it 'invalidates the record' do
|
|
expect(@assignment).not_to be_valid
|
|
end
|
|
|
|
it 'adds a validation error on the anonymous grading field' do
|
|
@assignment.valid?
|
|
|
|
expected_validation_error = "Group assignments can't be anonymously graded"
|
|
expect(@assignment.errors[:anonymous_grading]).to eq([expected_validation_error])
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'group category and anonymous grading co-validation' do
|
|
before :once do
|
|
@group_category = @course.group_categories.create! name: "groups"
|
|
@groups = Array.new(2) do |i|
|
|
@group_category.groups.create! name: "group #{i}", context: @course
|
|
end
|
|
|
|
@assignment = @course.assignments.build(name: "Assignment")
|
|
@assignment.save!
|
|
|
|
@assignment.group_category = @group_category
|
|
@assignment.anonymous_grading = true
|
|
end
|
|
|
|
it 'invalidates the record' do
|
|
expect(@assignment).not_to be_valid
|
|
end
|
|
|
|
it 'adds a validation error on the base record' do
|
|
@assignment.valid?
|
|
|
|
expected_validation_error = "Can't enable anonymous grading and group assignments together"
|
|
expect(@assignment.errors[:base]).to eq([expected_validation_error])
|
|
end
|
|
|
|
it 'does not add a validation error on the anonymous grading field' do
|
|
@assignment.valid?
|
|
|
|
expect(@assignment.errors[:anonymous_grading]).to be_empty
|
|
end
|
|
end
|
|
|
|
describe "moderated_grading validation" do
|
|
it "does not allow turning on if graded submissions exist" do
|
|
assignment_model(course: @course)
|
|
@assignment.grade_student @student, score: 0, grader: @teacher
|
|
@assignment.moderated_grading = true
|
|
@assignment.grader_count = 1
|
|
expect(@assignment.save).to eq false
|
|
expect(@assignment.errors[:moderated_grading]).to be_present
|
|
end
|
|
|
|
it "does not allow turning on if is also peer reviewed" do
|
|
assignment_model(course: @course)
|
|
@assignment.peer_reviews = true
|
|
@assignment.moderated_grading = true
|
|
@assignment.grader_count = 1
|
|
expect(@assignment.save).to eq false
|
|
expect(@assignment.errors[:moderated_grading]).to be_present
|
|
end
|
|
|
|
it "does not allow turning on if also a group assignment" do
|
|
assignment_model(course: @course)
|
|
@assignment.group_category = @course.group_categories.create!(name: "groups")
|
|
@assignment.moderated_grading = true
|
|
@assignment.grader_count = 1
|
|
expect(@assignment.save).to eq false
|
|
expect(@assignment.errors[:moderated_grading]).to be_present
|
|
end
|
|
|
|
it "does not allow turning off if graded submissions exist" do
|
|
assignment_model(course: @course, moderated_grading: true, grader_count: 2, final_grader: @teacher)
|
|
expect(@assignment).to be_moderated_grading
|
|
@assignment.grade_student @student, score: 0, grader: @teacher
|
|
@assignment.moderated_grading = false
|
|
expect(@assignment.save).to eq false
|
|
expect(@assignment.errors[:moderated_grading]).to be_present
|
|
end
|
|
|
|
it "does not allow turning off if provisional grades exist" do
|
|
assignment_model(course: @course, moderated_grading: true, grader_count: 2)
|
|
expect(@assignment).to be_moderated_grading
|
|
submission = @assignment.submit_homework @student, body: "blah"
|
|
submission.find_or_create_provisional_grade!(@teacher, score: 0)
|
|
@assignment.moderated_grading = false
|
|
expect(@assignment.save).to eq false
|
|
expect(@assignment.errors[:moderated_grading]).to be_present
|
|
end
|
|
|
|
it "does not allow turning on for an ungraded assignment" do
|
|
assignment_model(course: @course, submission_types: 'not_graded')
|
|
@assignment.moderated_grading = true
|
|
@assignment.grader_count = 1
|
|
expect(@assignment.save).to eq false
|
|
expect(@assignment.errors[:moderated_grading]).to be_present
|
|
end
|
|
|
|
it "does not allow creating a new ungraded assignment with moderated grading" do
|
|
a = @course.assignments.build
|
|
a.moderated_grading = true
|
|
a.grader_count = 1
|
|
a.submission_types = 'not_graded'
|
|
expect(a).not_to be_valid
|
|
end
|
|
|
|
end
|
|
|
|
describe "context_module_tag_info" do
|
|
before(:once) do
|
|
@assignment = @course.assignments.create!(:due_at => 1.week.ago,
|
|
:points_possible => 100,
|
|
:submission_types => 'online_text_entry')
|
|
end
|
|
|
|
it "returns past_due if an assignment is due in the past and no submission exists" do
|
|
info = @assignment.context_module_tag_info(@student, @course, has_submission: false)
|
|
expect(info[:past_due]).to be_truthy
|
|
end
|
|
|
|
it "does not return past_due for assignments that don't expect submissions" do
|
|
@assignment.submission_types = ''
|
|
@assignment.save!
|
|
info = @assignment.context_module_tag_info(@student, @course, has_submission: false)
|
|
expect(info[:past_due]).to be_falsey
|
|
end
|
|
|
|
it "does not return past_due for assignments that were turned in on time" do
|
|
Timecop.freeze(2.weeks.ago) { @assignment.submit_homework(@student, :submission_type => 'online_text_entry', :body => 'blah') }
|
|
info = @assignment.context_module_tag_info(@student, @course, has_submission: true)
|
|
expect(info[:past_due]).to be_falsey
|
|
end
|
|
|
|
it "does not return past_due for assignments that were turned in late" do
|
|
@assignment.submit_homework(@student, :submission_type => 'online_text_entry', :body => 'blah')
|
|
info = @assignment.context_module_tag_info(@student, @course, has_submission: true)
|
|
expect(info[:past_due]).to be_falsey
|
|
end
|
|
end
|
|
|
|
describe '#touch_submissions_if_muted' do
|
|
before(:once) do
|
|
@assignment = @course.assignments.create! points_possible: 10
|
|
@submission = @assignment.submit_homework(@student, body: "hello")
|
|
@assignment.ensure_post_policy(post_manually: true)
|
|
end
|
|
|
|
it "touches submissions if you mute the assignment" do
|
|
@assignment.update!(muted: false)
|
|
@submission_last_updated_at = @submission.reload.updated_at
|
|
@assignment.mute!
|
|
expect(@submission.reload.updated_at).to be > @submission_last_updated_at
|
|
end
|
|
|
|
context "calls assignment_muted_changed" do
|
|
it "for graded submissions" do
|
|
@assignment.grade_student(@student, grade: 10, grader: @teacher)
|
|
@called = false
|
|
allow_any_instance_of(Submission).to receive(:assignment_muted_changed) do
|
|
@called = true
|
|
expect(self.submission_model).to eq @submission
|
|
end
|
|
|
|
@assignment.unmute!
|
|
expect(@called).to eq true
|
|
end
|
|
|
|
it "does not dispatch update for ungraded submissions" do
|
|
expect_any_instance_of(Submission).to receive(:assignment_muted_changed).never
|
|
@assignment.unmute!
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '.remove_user_as_final_grader' do
|
|
it 'calls .remove_user_as_final_grader_immediately in a delayed job' do
|
|
expect(Assignment).to receive(:send_later_if_production_enqueue_args).
|
|
with(:remove_user_as_final_grader_immediately, any_args)
|
|
Assignment.remove_user_as_final_grader(@teacher.id, @course.id)
|
|
end
|
|
|
|
it 'runs the job in a strand, stranded by the root account ID' do
|
|
delayed_job_args = {
|
|
strand: "Assignment.remove_user_as_final_grader:#{@course.root_account.global_id}",
|
|
max_attempts: 1,
|
|
priority: Delayed::LOW_PRIORITY
|
|
}
|
|
|
|
expect(Assignment).to receive(:send_later_if_production_enqueue_args).
|
|
with(:remove_user_as_final_grader_immediately, delayed_job_args, any_args)
|
|
Assignment.remove_user_as_final_grader(@teacher.id, @course.id)
|
|
end
|
|
end
|
|
|
|
describe '.remove_user_as_final_grader_immediately' do
|
|
it 'removes the user as final grader in all assignments in the given course' do
|
|
2.times { @course.assignments.create!(moderated_grading: true, grader_count: 2, final_grader: @teacher) }
|
|
og_teacher = @teacher
|
|
another_teacher = teacher_in_course(course: @course, active_all: true).user
|
|
@course.enroll_teacher(another_teacher, active_all: true)
|
|
@course.assignments.create!(moderated_grading: true, grader_count: 2, final_grader: another_teacher)
|
|
expect { Assignment.remove_user_as_final_grader_immediately(og_teacher.id, @course.id) }.to change {
|
|
@course.assignments.order(:created_at).pluck(:final_grader_id)
|
|
}.from([og_teacher.id, og_teacher.id, another_teacher.id]).to([nil, nil, another_teacher.id])
|
|
end
|
|
|
|
it 'includes soft-deleted assignments when removing the user as final grader' do
|
|
assignment = @course.assignments.create!(moderated_grading: true, grader_count: 2, final_grader: @teacher)
|
|
assignment.destroy
|
|
expect { Assignment.remove_user_as_final_grader_immediately(@teacher.id, @course.id) }.to change {
|
|
assignment.reload.final_grader_id
|
|
}.from(@teacher.id).to(nil)
|
|
end
|
|
end
|
|
|
|
describe '.suspend_due_date_caching' do
|
|
it 'suspends the update_cached_due_dates after_save callback on Assignment' do
|
|
Assignment.suspend_due_date_caching do
|
|
expect(Assignment.send(:suspended_callback?, :update_cached_due_dates, :save, :after)).to be true
|
|
end
|
|
end
|
|
|
|
it 'suspends the update_cached_due_dates after_commit callback on AssignmentOverride' do
|
|
Assignment.suspend_due_date_caching do
|
|
expect(AssignmentOverride.send(:suspended_callback?, :update_cached_due_dates, :commit, :after)).to be true
|
|
end
|
|
end
|
|
|
|
it 'suspends the update_cached_due_dates after_create callback on AssignmentOverrideStudent' do
|
|
Assignment.suspend_due_date_caching do
|
|
expect(AssignmentOverrideStudent.send(:suspended_callback?, :update_cached_due_dates, :create, :after)).to be true
|
|
end
|
|
end
|
|
|
|
it 'suspends the update_cached_due_dates after_destroy callback on AssignmentOverrideStudent' do
|
|
Assignment.suspend_due_date_caching do
|
|
expect(AssignmentOverrideStudent.send(:suspended_callback?, :update_cached_due_dates, :destroy, :after)).to be true
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '.with_student_submission_count' do
|
|
require_relative '../sharding_spec_helper'
|
|
specs_require_sharding
|
|
|
|
it "doesn't reference multiple shards when accessed from a different shard" do
|
|
@assignment = @course.assignments.create! points_possible: 10
|
|
allow(Assignment.connection).to receive(:use_qualified_names?).and_return(true)
|
|
@shard1.activate do
|
|
allow(Assignment.connection).to receive(:use_qualified_names?).and_return(true)
|
|
sql = @course.assignments.with_student_submission_count.to_sql
|
|
expect(sql).to be_include(Shard.default.name)
|
|
expect(sql).not_to be_include(@shard1.name)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#lti_resource_link_id' do
|
|
subject { assignment.lti_resource_link_id }
|
|
|
|
context 'without external tool tag' do
|
|
let(:assignment) do
|
|
@course.assignments.create!(assignment_valid_attributes)
|
|
end
|
|
|
|
it { is_expected.to be_nil }
|
|
end
|
|
|
|
context 'with external tool tag' do
|
|
let(:assignment) do
|
|
@course.assignments.create!(submission_types: 'external_tool',
|
|
external_tool_tag_attributes: { url: 'http://example.com/launch' },
|
|
**assignment_valid_attributes)
|
|
end
|
|
|
|
it 'calls ContextExternalTool.opaque_identifier_for with the external tool tag and assignment shard' do
|
|
lti_resource_link_id = SecureRandom.hex
|
|
expect(ContextExternalTool).to receive(:opaque_identifier_for).with(
|
|
assignment.external_tool_tag,
|
|
assignment.shard
|
|
).and_return(lti_resource_link_id)
|
|
expect(assignment.lti_resource_link_id).to eq(lti_resource_link_id)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#available_moderators' do
|
|
before(:once) do
|
|
@course = Course.create!
|
|
@first_teacher = User.create!
|
|
@second_teacher = User.create!
|
|
[@first_teacher, @second_teacher].each { |user| @course.enroll_teacher(user, enrollment_state: 'active') }
|
|
@first_ta = User.create!
|
|
@second_ta = User.create
|
|
[@first_ta, @second_ta].each { |user| @course.enroll_ta(user, enrollment_state: 'active') }
|
|
@assignment = @course.assignments.create!(
|
|
final_grader: @first_teacher,
|
|
grader_count: 2,
|
|
moderated_grading: true
|
|
)
|
|
end
|
|
|
|
it 'returns a list of active, available moderators in the course' do
|
|
expected_moderator_ids = [@first_teacher, @second_teacher, @first_ta, @second_ta].map(&:id)
|
|
expect(@assignment.available_moderators.map(&:id)).to match_array expected_moderator_ids
|
|
end
|
|
|
|
it 'excludes admins' do
|
|
admin = account_admin_user
|
|
expect(@course.moderators).not_to include admin
|
|
end
|
|
|
|
it 'excludes deactivated moderators in the course (see exception below)' do
|
|
@course.enrollments.find_by(user: @second_teacher).deactivate
|
|
expected_moderator_ids = [@first_teacher, @first_ta, @second_ta].map(&:id)
|
|
expect(@assignment.available_moderators.map(&:id)).to match_array expected_moderator_ids
|
|
end
|
|
|
|
it 'excludes concluded moderators in the course (see exception below)' do
|
|
@course.enrollments.find_by(user: @second_teacher).conclude
|
|
expected_moderator_ids = [@first_teacher, @first_ta, @second_ta].map(&:id)
|
|
expect(@assignment.available_moderators.map(&:id)).to match_array expected_moderator_ids
|
|
end
|
|
|
|
it 'excludes TAs if they do not have "Select Final Grade" permissions' do
|
|
@course.root_account.role_overrides.create!(permission: 'select_final_grade', role: ta_role, enabled: false)
|
|
expected_moderator_ids = [@first_teacher, @second_teacher].map(&:id)
|
|
expect(@assignment.available_moderators.map(&:id)).to match_array expected_moderator_ids
|
|
end
|
|
|
|
it 'excludes teachers if they do not have "Select Final Grade" permissions' do
|
|
@assignment.update!(final_grader: @first_ta)
|
|
@course.root_account.role_overrides.create!(permission: 'select_final_grade', role: teacher_role, enabled: false)
|
|
expected_moderator_ids = [@first_ta, @second_ta].map(&:id)
|
|
expect(@assignment.available_moderators.map(&:id)).to match_array expected_moderator_ids
|
|
end
|
|
|
|
it 'includes an inactive user in the list if that user was picked as the final grader before being deactivated' do
|
|
@course.enrollments.find_by(user: @first_teacher).deactivate
|
|
expected_moderator_ids = [@first_teacher, @second_teacher, @first_ta, @second_ta].map(&:id)
|
|
expect(@assignment.available_moderators.map(&:id)).to match_array expected_moderator_ids
|
|
end
|
|
|
|
it 'includes a concluded user in the list if that user was picked as the final grader before being concluded' do
|
|
@course.enrollments.find_by(user: @first_teacher).conclude
|
|
expected_moderator_ids = [@first_teacher, @second_teacher, @first_ta, @second_ta].map(&:id)
|
|
expect(@assignment.available_moderators.map(&:id)).to match_array expected_moderator_ids
|
|
end
|
|
end
|
|
|
|
describe '#moderated_grading_max_grader_count' do
|
|
let_once(:course) { Course.create! }
|
|
let_once(:teacher) { course.enroll_teacher(User.create!, enrollment_state: 'active').user }
|
|
let_once(:assignment) {
|
|
course.assignments.create!(
|
|
final_grader: teacher,
|
|
grader_count: 1,
|
|
moderated_grading: true
|
|
)
|
|
}
|
|
|
|
before(:once) do
|
|
teacher2 = User.create!
|
|
@teacher2_enrollment = course.enroll_teacher(teacher2, enrollment_state: 'active')
|
|
course.enroll_teacher(User.create!, enrollment_state: 'active')
|
|
assignment.update!(grader_count: 2)
|
|
end
|
|
|
|
it 'returns the number of active instructors minus one' do
|
|
expect(assignment.moderated_grading_max_grader_count).to eq 2
|
|
end
|
|
|
|
it 'returns the number of active instructors minus one when a grader is deactivated' do
|
|
@teacher2_enrollment.deactivate
|
|
expect(assignment.moderated_grading_max_grader_count).to eq 1
|
|
end
|
|
|
|
it 'returns number of active instructors minus 1 when number of graders > available graders count' do
|
|
assignment.update!(grader_count: 7)
|
|
expect(assignment.moderated_grading_max_grader_count).to eq 2
|
|
end
|
|
|
|
it 'returns available graders count when number of graders is set to the max possible' do
|
|
assignment.update!(grader_count: 9)
|
|
course.enroll_teacher(User.create!, enrollment_state: 'active')
|
|
course.enroll_teacher(User.create!, enrollment_state: 'active')
|
|
expect(assignment.moderated_grading_max_grader_count).to eq 4
|
|
end
|
|
end
|
|
|
|
describe '#moderated_grader_limit_reached?' do
|
|
before(:once) do
|
|
@course = Course.create!
|
|
@teacher = User.create!
|
|
second_teacher = User.create!
|
|
@ta = User.create!
|
|
@course.enroll_teacher(@teacher, enrollment_state: :active)
|
|
@course.enroll_teacher(second_teacher, enrollment_state: :active)
|
|
@course.enroll_ta(@ta, enrollment_state: :active)
|
|
@course.enroll_student(@student, enrollment_state: :active)
|
|
@assignment = @course.assignments.create!(
|
|
final_grader: @teacher,
|
|
grader_count: 2,
|
|
moderated_grading: true
|
|
)
|
|
@assignment.grade_student(@student, grader: second_teacher, provisional: true, score: 5)
|
|
end
|
|
|
|
it 'returns false if all provisional grader slots are not filled' do
|
|
expect(@assignment.moderated_grader_limit_reached?).to eq false
|
|
end
|
|
|
|
it 'returns true if all provisional grader slots are filled' do
|
|
@assignment.grade_student(@student, grader: @ta, provisional: true, score: 10)
|
|
expect(@assignment.moderated_grader_limit_reached?).to eq true
|
|
end
|
|
|
|
it 'ignores grades issued by the final grader when determining if slots are filled' do
|
|
@assignment.grade_student(@student, grader: @teacher, provisional: true, score: 10)
|
|
expect(@assignment.moderated_grader_limit_reached?).to eq false
|
|
end
|
|
|
|
it 'returns false if moderated grading is off' do
|
|
@assignment.grade_student(@student, grader: @ta, provisional: true, score: 10)
|
|
@assignment.moderated_grading = false
|
|
expect(@assignment.moderated_grader_limit_reached?).to eq false
|
|
end
|
|
end
|
|
|
|
describe '#can_be_moderated_grader?' do
|
|
before(:once) do
|
|
@course = Course.create!
|
|
@teacher = User.create!
|
|
@second_teacher = User.create!
|
|
@final_teacher = User.create!
|
|
student = User.create!
|
|
@course.enroll_teacher(@teacher, enrollment_state: :active)
|
|
@course.enroll_teacher(@second_teacher, enrollment_state: :active)
|
|
@course.enroll_teacher(@final_teacher, enrollment_state: :active)
|
|
@course.enroll_student(student, enrollment_state: :active)
|
|
@assignment = @course.assignments.create!(
|
|
final_grader: @final_teacher,
|
|
grader_count: 2,
|
|
moderated_grading: true,
|
|
points_possible: 10
|
|
)
|
|
@assignment.grade_student(student, grader: @second_teacher, provisional: true, score: 10)
|
|
end
|
|
|
|
shared_examples 'grader permissions are checked' do
|
|
it 'returns true when the user has default teacher permissions' do
|
|
expect(@assignment.can_be_moderated_grader?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns true when the user has permission to only manage grades' do
|
|
@course.root_account.role_overrides.create!(permission: 'manage_grades', enabled: true, role: teacher_role)
|
|
@course.root_account.role_overrides.create!(permission: 'view_all_grades', enabled: false, role: teacher_role)
|
|
expect(@assignment.can_be_moderated_grader?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns true when the user has permission to only view all grades' do
|
|
@course.root_account.role_overrides.create!(permission: 'manage_grades', enabled: false, role: teacher_role)
|
|
@course.root_account.role_overrides.create!(permission: 'view_all_grades', enabled: true, role: teacher_role)
|
|
expect(@assignment.can_be_moderated_grader?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns false when the user does not have sufficient privileges' do
|
|
@course.root_account.role_overrides.create!(permission: 'manage_grades', enabled: false, role: teacher_role)
|
|
@course.root_account.role_overrides.create!(permission: 'view_all_grades', enabled: false, role: teacher_role)
|
|
expect(@assignment.can_be_moderated_grader?(@teacher)).to be false
|
|
end
|
|
end
|
|
|
|
context 'when the assignment is not moderated' do
|
|
before :once do
|
|
@assignment.update!(moderated_grading: false)
|
|
end
|
|
|
|
it_behaves_like 'grader permissions are checked'
|
|
end
|
|
|
|
context 'when the assignment is moderated' do
|
|
it_behaves_like 'grader permissions are checked'
|
|
|
|
context 'and moderator limit is reached' do
|
|
before :once do
|
|
@assignment.update!(grader_count: 1)
|
|
end
|
|
|
|
it 'returns false' do
|
|
expect(@assignment.can_be_moderated_grader?(@teacher)).to be false
|
|
end
|
|
|
|
it 'returns true if user is one of the moderators' do
|
|
expect(@assignment.can_be_moderated_grader?(@second_teacher)).to be true
|
|
end
|
|
|
|
it 'returns true if user is the final grader' do
|
|
expect(@assignment.can_be_moderated_grader?(@final_teacher)).to be true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#user_is_moderation_grader?' do
|
|
before(:once) do
|
|
@course = Course.create!
|
|
@teacher = User.create!
|
|
@course.enroll_teacher(@teacher, enrollment_state: :active)
|
|
@assignment = @course.assignments.create!(moderated_grading: true, grader_count: 2)
|
|
end
|
|
|
|
it 'returns true if the user is a moderation grader occupying a grader slot' do
|
|
@assignment.create_moderation_grader(@teacher, occupy_slot: true)
|
|
expect(@assignment.user_is_moderation_grader?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns true if the user is a moderation grader not occupying a grader slot' do
|
|
@assignment.create_moderation_grader(@teacher, occupy_slot: false)
|
|
expect(@assignment.user_is_moderation_grader?(@teacher)).to be true
|
|
end
|
|
|
|
it 'returns false if the user is not a moderation grader' do
|
|
expect(@assignment.user_is_moderation_grader?(@teacher)).to be false
|
|
end
|
|
end
|
|
|
|
describe '#can_view_speed_grader?' do
|
|
before :once do
|
|
@course = Course.create!
|
|
@teacher = User.create!
|
|
@course.enroll_teacher(@teacher, enrollment_state: 'active')
|
|
@assignment = @course.assignments.create!(
|
|
final_grader: @teacher,
|
|
grader_count: 2,
|
|
moderated_grading: true
|
|
)
|
|
end
|
|
|
|
it 'returns false when the course does not allow speed grader' do
|
|
expect(@assignment.context).to receive(:allows_speed_grader?).and_return false
|
|
expect(@assignment.can_view_speed_grader?(@teacher)).to be false
|
|
end
|
|
|
|
it 'returns false when the user cannot view or manage grades' do
|
|
@course.root_account.role_overrides.create!(permission: 'manage_grades', enabled: false, role: teacher_role)
|
|
@course.root_account.role_overrides.create!(permission: 'view_all_grades', enabled: false, role: teacher_role)
|
|
expect(@assignment.context).to receive(:allows_speed_grader?).and_return true
|
|
expect(@assignment.can_view_speed_grader?(@teacher)).to be false
|
|
end
|
|
|
|
it 'returns true when the course allows speed grader and user can manage grades' do
|
|
expect(@assignment.context).to receive(:allows_speed_grader?).and_return true
|
|
expect(@assignment.can_view_speed_grader?(@teacher)).to be true
|
|
end
|
|
end
|
|
|
|
describe '#can_view_audit_trail?' do
|
|
before :once do
|
|
@admin = account_admin_user
|
|
@assignment = @course.assignments.create!(
|
|
final_grader: @teacher,
|
|
grader_count: 2,
|
|
grades_published_at: 2.days.ago,
|
|
moderated_grading: true
|
|
)
|
|
@assignment.update!(muted: false)
|
|
end
|
|
|
|
it 'returns true for an auditor when the assignment is moderated, not muted, and grades have been posted' do
|
|
expect(@assignment.can_view_audit_trail?(@admin)).to be true
|
|
end
|
|
|
|
it 'returns true for an auditor when the assignment is graded anonymously, not muted, and grades have been posted' do
|
|
@assignment.update!(anonymous_grading: true, moderated_grading: false)
|
|
@assignment.update!(muted: false)
|
|
expect(@assignment.can_view_audit_trail?(@admin)).to be true
|
|
end
|
|
|
|
it "returns false when the user's role does not allow viewing the assignment audit trail" do
|
|
@course.root_account.role_overrides.create!(enabled: false, permission: :view_audit_trail, role: admin_role)
|
|
expect(@assignment.can_view_audit_trail?(@admin)).to be false
|
|
end
|
|
|
|
it 'returns false when the assignment is neither moderated nor anonymous' do
|
|
@assignment.update!(moderated_grading: false)
|
|
@assignment.update!(muted: false)
|
|
expect(@assignment.can_view_audit_trail?(@admin)).to be false
|
|
end
|
|
|
|
it 'returns false when the assignment is muted' do
|
|
@assignment.update!(muted: true)
|
|
expect(@assignment.can_view_audit_trail?(@admin)).to be false
|
|
end
|
|
|
|
it 'returns false when the assignment grades have not been posted' do
|
|
@assignment.update!(grades_published_at: nil)
|
|
expect(@assignment.can_view_audit_trail?(@admin)).to be false
|
|
end
|
|
end
|
|
|
|
describe '#auditable?' do
|
|
let(:course) { Course.create! }
|
|
let(:assignment) { course.assignments.create!(title: 'hi') }
|
|
let(:teacher) { course.enroll_teacher(User.create!, enrollment_state: 'active').user }
|
|
|
|
it 'is true if the assignment is anonymous' do
|
|
assignment.update!(anonymous_grading: true, moderated_grading: false)
|
|
expect(assignment).to be_auditable
|
|
end
|
|
|
|
it 'is true if the assignment is moderated' do
|
|
assignment.update!(
|
|
anonymous_grading: false,
|
|
moderated_grading: true,
|
|
grader_count: 1,
|
|
final_grader: teacher
|
|
)
|
|
expect(assignment).to be_auditable
|
|
end
|
|
|
|
it "is true if an anonymous assignment just became non-anonymous" do
|
|
assignment.update!(anonymous_grading: true)
|
|
assignment.update!(anonymous_grading: false)
|
|
expect(assignment).to be_auditable
|
|
end
|
|
|
|
it "is true if an anonymous assignment just became non-moderated" do
|
|
assignment.update!(moderated_grading: true, grader_count: 1, final_grader: teacher)
|
|
assignment.update!(moderated_grading: false)
|
|
expect(assignment).to be_auditable
|
|
end
|
|
|
|
it 'is false if the assignment is neither anonymous nor moderated' do
|
|
expect(assignment).not_to be_auditable
|
|
end
|
|
end
|
|
|
|
describe "#effective_post_policy" do
|
|
let(:course) { Course.create! }
|
|
let(:assignment) { course.assignments.create!(title: 'hi') }
|
|
|
|
it "returns the post policy for the course if the assignment has no policy attached" do
|
|
assignment.post_policy.destroy
|
|
expect(assignment.reload.effective_post_policy).to eq(course.default_post_policy)
|
|
end
|
|
|
|
it "returns the post policy for the assignment if present" do
|
|
assignment.post_policy.update!(post_manually: false)
|
|
|
|
expect(assignment.effective_post_policy).to eq(assignment.post_policy)
|
|
end
|
|
end
|
|
|
|
describe "#post_manually?" do
|
|
let(:course) { Course.create! }
|
|
let(:assignment) { course.assignments.create!(title: 'hello') }
|
|
|
|
context "when the assignment has a post policy" do
|
|
it "returns true if the assignment's post policy has manual posting enabled" do
|
|
assignment.post_policy.update!(post_manually: true)
|
|
expect(assignment).to be_post_manually
|
|
end
|
|
|
|
it "returns false if the assignment's post policy has manual posting disabled" do
|
|
assignment.post_policy.update!(post_manually: false)
|
|
expect(assignment).not_to be_post_manually
|
|
end
|
|
end
|
|
|
|
context "when the assignment has no post policy but the course does" do
|
|
it "returns true if the course's post policy has manual posting enabled" do
|
|
course.default_post_policy.update!(post_manually: true)
|
|
assignment.post_policy.destroy
|
|
expect(assignment.reload).to be_post_manually
|
|
end
|
|
|
|
it "returns false if the course's post policy has manual posting disabled" do
|
|
course.default_post_policy.update!(post_manually: false)
|
|
assignment.post_policy.destroy
|
|
expect(assignment).not_to be_post_manually
|
|
end
|
|
end
|
|
|
|
it "returns false if neither the assignment nor the course has a post policy attached" do
|
|
course.default_post_policy.destroy
|
|
assignment.post_policy.destroy
|
|
expect(assignment).not_to be_post_manually
|
|
end
|
|
end
|
|
|
|
describe "posting and unposting submissions" do
|
|
let(:assignment) { @course.assignments.create!(title: 'hi') }
|
|
|
|
let(:student1) { @course.enroll_student(User.create!, active_all: true).user }
|
|
let(:student2) { @course.enroll_student(User.create!, active_all: true).user }
|
|
let(:student1_submission) { assignment.submission_for_student(student1) }
|
|
let(:student2_submission) { assignment.submission_for_student(student2) }
|
|
|
|
let(:teacher) { @course.enroll_teacher(User.create!, active_all: true).user }
|
|
|
|
before(:each) do
|
|
student1
|
|
student2
|
|
end
|
|
|
|
describe "#post_submissions" do
|
|
it "updates the posted_at field of the specified submissions" do
|
|
update_time = Time.zone.now
|
|
Timecop.freeze(update_time) do
|
|
assignment.post_submissions
|
|
end
|
|
|
|
expect(student1_submission.reload.posted_at).to eq(update_time)
|
|
end
|
|
|
|
it "does not update the posted_at field of submissions that were not specified" do
|
|
assignment.post_submissions(submission_ids: [student1_submission.id])
|
|
|
|
expect(student2_submission).not_to be_posted
|
|
end
|
|
|
|
it "reveals hidden comments on specified submissions" do
|
|
comment = student1_submission.add_comment(author: teacher, hidden: true, comment: 'ok')
|
|
assignment.post_submissions
|
|
|
|
expect(comment.reload).not_to be_hidden
|
|
end
|
|
|
|
it "does not update the posted_at field if skip_updating_timestamp is passed" do
|
|
expect {
|
|
assignment.post_submissions(skip_updating_timestamp: true)
|
|
}.not_to change {
|
|
assignment.submission_for_student(student1).posted_at
|
|
}
|
|
end
|
|
|
|
it "calls broadcast_notifications for submissions" do
|
|
expect(Submission.broadcast_policy_list).to receive(:broadcast).with(student1_submission)
|
|
assignment.post_submissions(submission_ids: [student1_submission.id])
|
|
end
|
|
|
|
describe "grade change audit records" do
|
|
context "when assignment posts manually" do
|
|
before(:once) do
|
|
assignment.ensure_post_policy(post_manually: true)
|
|
end
|
|
|
|
it "inserts a single grade change record" do
|
|
expect(Auditors::GradeChange::Stream).to receive(:insert).once
|
|
assignment.grade_student(student1, grade: 10, grader: teacher)
|
|
end
|
|
|
|
it "does not insert a grade change record when posting" do
|
|
expect(Auditors::GradeChange::Stream).not_to receive(:insert)
|
|
assignment.post_submissions
|
|
end
|
|
|
|
it "does not insert a grade change record when hiding" do
|
|
assignment.post_submissions
|
|
expect(Auditors::GradeChange::Stream).not_to receive(:insert)
|
|
assignment.hide_submissions
|
|
end
|
|
end
|
|
|
|
context "when assignment posts automatically" do
|
|
before(:once) do
|
|
assignment.ensure_post_policy(post_manually: false)
|
|
end
|
|
|
|
it "inserts a single grade change record" do
|
|
expect(Auditors::GradeChange::Stream).to receive(:insert).once
|
|
assignment.grade_student(student1, grade: 10, grader: teacher)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "grade changed live events" do
|
|
context "when assignment posts manually" do
|
|
before(:each) do
|
|
assignment.ensure_post_policy(post_manually: true)
|
|
end
|
|
|
|
it "emits an event when grading" do
|
|
expect(Canvas::LiveEvents).to receive(:grade_changed).once
|
|
assignment.grade_student(student1, grade: 10, grader: teacher)
|
|
end
|
|
|
|
it "emits an event when posting graded submissions" do
|
|
assignment.grade_student(student1, grade: 10, grader: teacher)
|
|
expect(Canvas::LiveEvents).to receive(:grade_changed).once
|
|
assignment.post_submissions
|
|
end
|
|
|
|
it "emits an event when hiding graded submissions" do
|
|
assignment.grade_student(student1, grade: 10, grader: teacher)
|
|
assignment.post_submissions
|
|
expect(Canvas::LiveEvents).to receive(:grade_changed).once
|
|
assignment.hide_submissions
|
|
end
|
|
end
|
|
|
|
context "when assignment posts automatically" do
|
|
before(:each) do
|
|
assignment.ensure_post_policy(post_manually: false)
|
|
end
|
|
|
|
it "emits two events when grading: one for grading and one for posting" do
|
|
expect(Canvas::LiveEvents).to receive(:grade_changed).twice
|
|
assignment.grade_student(student1, grade: 10, grader: teacher)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "Submissions Posted notification" do
|
|
let_once(:notification) { Notification.find_or_create_by!(category: "Grading", name: "Submissions Posted") }
|
|
let(:context) { { current_user: teacher } }
|
|
let(:teacher_enrollment) { @course.teacher_enrollments.find_by!(user: teacher) }
|
|
let(:section1) { @course.course_sections.create! }
|
|
let(:submissions_posted_messages) do
|
|
Message.where(
|
|
notification: notification
|
|
)
|
|
end
|
|
|
|
before(:each) do
|
|
section1.enroll_user(student1, "StudentEnrollment", "active")
|
|
student1.update!(email: "studentemail@example.com", workflow_state: :registered)
|
|
student1.email_channel.update!(workflow_state: :active)
|
|
teacher.update!(email: "teacheremail@example.com", workflow_state: :registered)
|
|
teacher.email_channel.update!(workflow_state: :active)
|
|
teacher_enrollment.update!(workflow_state: :active)
|
|
end
|
|
|
|
it "does not broadcast a notification when not including posting_params" do
|
|
expect { assignment.post_submissions }.not_to change { submissions_posted_messages.count }
|
|
end
|
|
|
|
it "does not broadcast a notification for students" do
|
|
expect {
|
|
assignment.post_submissions(posting_params: { graded_only: false })
|
|
}.not_to change {
|
|
submissions_posted_messages.where(communication_channel: student1.communication_channels).count
|
|
}
|
|
end
|
|
|
|
it "broadcasts a notification for teachers" do
|
|
expect {
|
|
assignment.post_submissions(posting_params: { graded_only: false })
|
|
}.to change {
|
|
submissions_posted_messages.where(communication_channel: teacher.communication_channels).count
|
|
}.by(1)
|
|
end
|
|
|
|
it "broadcasts a notification when posting to everyone" do
|
|
assignment.post_submissions(posting_params: { graded_only: false })
|
|
body_text = "Grade changes and comments have been released for everyone."
|
|
expect(submissions_posted_messages.order(:id).last.body).to include body_text
|
|
end
|
|
|
|
it "broadcasts a notification when posting to everyone graded" do
|
|
assignment.grade_student(student1, grader: teacher, score: 1)
|
|
assignment.post_submissions(posting_params: { graded_only: true })
|
|
body_text = "Grade changes and comments have been released for everyone graded."
|
|
expect(submissions_posted_messages.order(:id).last.body).to include body_text
|
|
end
|
|
|
|
it "broadcasts a notification when posting to everyone in sections" do
|
|
assignment.post_submissions(posting_params: { graded_only: false, section_names: ["section 1"] })
|
|
body_text = "Grade changes and comments have been released for everyone in sections: section 1."
|
|
expect(submissions_posted_messages.order(:id).last.body).to include body_text
|
|
end
|
|
|
|
it "broadcasts a notification when posting to everyone graded in sections" do
|
|
assignment.grade_student(student1, grader: teacher, score: 1)
|
|
assignment.post_submissions(posting_params: { graded_only: true, section_names: ["section 1"] })
|
|
body_text = "Grade changes and comments have been released for everyone graded in sections: section 1."
|
|
expect(submissions_posted_messages.order(:id).last.body).to include body_text
|
|
end
|
|
end
|
|
|
|
context "when given a Progress" do
|
|
before(:each) do
|
|
@progress = @course.progresses.create!(tag: "post_submissions")
|
|
end
|
|
|
|
it "sets the assignment id in the results" do
|
|
assignment.post_submissions(progress: @progress, submission_ids: [student1_submission.id])
|
|
expect(@progress.results[:assignment_id]).to eq assignment.id
|
|
end
|
|
|
|
it "sets the posted_at in the results" do
|
|
assignment.post_submissions(progress: @progress, submission_ids: [student1_submission.id])
|
|
expect(@progress.results[:posted_at]).to eq student1_submission.reload.posted_at
|
|
end
|
|
|
|
it "sets the user ids in the results" do
|
|
assignment.post_submissions(progress: @progress, submission_ids: [student1_submission.id])
|
|
expect(@progress.results[:user_ids]).to match_array [student1.id]
|
|
end
|
|
end
|
|
|
|
context "when post policies are enabled" do
|
|
it "unmutes the assignment if all submissions are now posted" do
|
|
assignment.mute!
|
|
|
|
assignment.post_submissions
|
|
expect(assignment).not_to be_muted
|
|
end
|
|
|
|
it "leaves the assignment muted if some submissions remain unposted" do
|
|
assignment.mute!
|
|
|
|
assignment.post_submissions(submission_ids: [student1_submission.id])
|
|
expect(assignment).to be_muted
|
|
end
|
|
|
|
it "recomputes grades for the affected students" do
|
|
assignment.mute!
|
|
|
|
expect(@course).to receive(:recompute_student_scores).with([student1.id])
|
|
assignment.post_submissions(submission_ids: [student1_submission.id])
|
|
end
|
|
end
|
|
|
|
describe "context module progressions" do
|
|
let(:context_module) { @course.context_modules.create! }
|
|
let(:student1) { @course.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user }
|
|
let(:student2) { @course.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user }
|
|
let(:tag) { context_module.add_item({id: assignment.id, type: "assignment"}) }
|
|
|
|
before(:each) do
|
|
context_module.update!(completion_requirements: {tag.id => {type: "min_score", min_score: 90}})
|
|
# Have a manual post policy to stop the evaluation of the requirement
|
|
# until post_submissions is called.
|
|
assignment.ensure_post_policy(post_manually: true)
|
|
end
|
|
|
|
it "updates the met requirements" do
|
|
assignment.grade_student(student1, grader: teacher, score: 100)
|
|
assignment.post_submissions
|
|
progression = context_module.context_module_progressions.find_by(user: student1)
|
|
requirement = {id: tag.id, type: "min_score", min_score: 90.0}
|
|
expect(progression.requirements_met).to include requirement
|
|
end
|
|
|
|
it "does not update the met requirements for students that did not meet requirement" do
|
|
assignment.grade_student(student1, grader: teacher, score: 20)
|
|
assignment.post_submissions
|
|
progression = context_module.context_module_progressions.find_by(user: student1)
|
|
requirement = {id: tag.id, type: "min_score", min_score: 90.0, score: 20.0}
|
|
expect(progression.incomplete_requirements).to include requirement
|
|
end
|
|
|
|
it "does not update the met requirements for students not included" do
|
|
assignment.grade_student(student1, grader: teacher, score: 100)
|
|
assignment.grade_student(student2, grader: teacher, score: 100)
|
|
student1_sub = assignment.submissions.find_by(user: student1)
|
|
assignment.post_submissions(submission_ids: [student1_sub])
|
|
progression = context_module.context_module_progressions.find_by(user: student2)
|
|
requirement = {id: tag.id, type: "min_score", min_score: 90.0, score: nil}
|
|
expect(progression.incomplete_requirements).to include requirement
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#hide_submissions" do
|
|
before(:each) { assignment.post_submissions }
|
|
|
|
it "nullifies the posted_at field of the specified submissions" do
|
|
assignment.hide_submissions
|
|
expect(student1_submission.reload.posted_at).to be_nil
|
|
end
|
|
|
|
it "does not nullify the posted_at field of submissions that were not specified" do
|
|
expect {
|
|
assignment.hide_submissions(submission_ids: [student1_submission.id])
|
|
}.not_to change {
|
|
student2_submission.posted_at
|
|
}
|
|
end
|
|
|
|
it "hides instructor comments on specified submissions" do
|
|
comment = student1_submission.add_comment(author: teacher, hidden: false, comment: 'ok')
|
|
assignment.hide_submissions
|
|
|
|
expect(comment.reload).to be_hidden
|
|
end
|
|
|
|
it "does not update the posted_at field if skip_updating_timestamp is passed" do
|
|
student1_submission.update!(posted_at: 1.day.ago)
|
|
|
|
expect {
|
|
assignment.hide_submissions(skip_updating_timestamp: true)
|
|
}.not_to change {
|
|
assignment.submission_for_student(student1).posted_at
|
|
}
|
|
end
|
|
|
|
context "when given a Progress" do
|
|
before(:each) do
|
|
@progress = @course.progresses.create!(tag: "hide_submissions")
|
|
end
|
|
|
|
it "sets the assignment id in the results" do
|
|
assignment.hide_submissions(progress: @progress, submission_ids: [student1_submission.id])
|
|
expect(@progress.results[:assignment_id]).to eq assignment.id
|
|
end
|
|
|
|
it "sets the posted_at to nil in the results" do
|
|
assignment.hide_submissions(progress: @progress, submission_ids: [student1_submission.id])
|
|
expect(@progress.results[:posted_at]).to be_nil
|
|
end
|
|
|
|
it "sets the user ids in the results" do
|
|
assignment.hide_submissions(progress: @progress, submission_ids: [student1_submission.id])
|
|
expect(@progress.results[:user_ids]).to match_array [student1.id]
|
|
end
|
|
end
|
|
|
|
context "when post policies are enabled" do
|
|
it "mutes the assignment if any submissions are now unposted" do
|
|
assignment.hide_submissions
|
|
expect(assignment).to be_muted
|
|
end
|
|
|
|
it "leaves the assignment unmuted if all submissions remain posted" do
|
|
assignment.hide_submissions(submission_ids: [])
|
|
expect(assignment).not_to be_muted
|
|
end
|
|
|
|
it "recomputes grades for the affected students" do
|
|
expect(@course).to receive(:recompute_student_scores).with([student1.id])
|
|
assignment.hide_submissions(submission_ids: [student1_submission.id])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'Anonymous Moderated Marking setting validation' do
|
|
before(:once) do
|
|
assignment_model(course: @course)
|
|
end
|
|
|
|
describe 'Moderated Grading validation' do
|
|
context 'when moderated_grading is not enabled' do
|
|
subject(:assignment) { @course.assignments.build }
|
|
|
|
it { is_expected.to validate_absence_of(:grader_section) }
|
|
it { is_expected.to validate_absence_of(:final_grader) }
|
|
|
|
it 'before validation, sets final_grader_id to nil if it is present' do
|
|
teacher = User.create!
|
|
@course.enroll_teacher(teacher, active_all: true)
|
|
assignment.final_grader_id = teacher.id
|
|
assignment.validate
|
|
expect(assignment.final_grader_id).to be_nil
|
|
end
|
|
|
|
it 'before validation, sets grader_count to 0 if it is present' do
|
|
teacher = User.create!
|
|
@course.enroll_teacher(teacher, active_all: true)
|
|
assignment.grader_count = nil
|
|
assignment.validate
|
|
expect(assignment.grader_count).to be 0
|
|
end
|
|
end
|
|
|
|
context 'when moderated_grading is enabled' do
|
|
before(:each) do
|
|
@section1 = @course.course_sections.first
|
|
@section1_ta = ta_in_section(@section1)
|
|
|
|
@section2 = @course.course_sections.create!(name: 'other section')
|
|
@section2_ta = ta_in_section(@section2)
|
|
|
|
@assignment.moderated_grading = true
|
|
@assignment.grader_count = 1
|
|
@assignment.final_grader = @section1_ta
|
|
end
|
|
|
|
let(:errors) { @assignment.errors }
|
|
|
|
describe 'basic field validation' do
|
|
subject { @course.assignments.create(moderated_grading: true, grader_count: 1, final_grader: @section1_ta) }
|
|
|
|
it { is_expected.to be_muted }
|
|
it { is_expected.to validate_numericality_of(:grader_count).is_greater_than(0) }
|
|
end
|
|
|
|
describe 'grader_section validation' do
|
|
let(:error_message) { 'must be active and in same course as assignment' }
|
|
|
|
it 'allows an active grader section from the course to be set' do
|
|
@assignment.grader_section = @section1
|
|
expect(@assignment).to be_valid
|
|
end
|
|
|
|
it 'does not allow a non-active grader section from the course' do
|
|
@section2.destroy
|
|
@assignment.grader_section = @section2
|
|
@assignment.final_grader = @section2_ta
|
|
@assignment.valid?
|
|
|
|
expect(errors[:grader_section]).to eq [error_message]
|
|
end
|
|
|
|
it 'does not allow a grader section from a different course' do
|
|
other_course = Course.create!(name: 'other course')
|
|
@assignment.grader_section = other_course.course_sections.create!(name: 'other course section')
|
|
@assignment.valid?
|
|
|
|
expect(errors[:grader_section]).to eq [error_message]
|
|
end
|
|
end
|
|
|
|
describe 'final_grader validation' do
|
|
it 'allows a final grader from the selected grader section' do
|
|
@assignment.grader_section = @section1
|
|
@assignment.final_grader = @section1_ta
|
|
|
|
expect(@assignment).to be_valid
|
|
end
|
|
|
|
it 'allows a final grader from the course if no section is set' do
|
|
@assignment.final_grader = @section2_ta
|
|
|
|
expect(@assignment).to be_valid
|
|
end
|
|
|
|
it 'does not allow a final grader from a different section' do
|
|
@assignment.grader_section = @section1
|
|
@assignment.final_grader = @section2_ta
|
|
@assignment.valid?
|
|
|
|
expect(errors[:final_grader]).to eq ['must be enrolled in selected section']
|
|
end
|
|
|
|
it 'does not allow a non-instructor final grader' do
|
|
@assignment.final_grader = @initial_student
|
|
@assignment.valid?
|
|
|
|
expect(errors[:final_grader]).to eq ['must be an instructor in this course']
|
|
end
|
|
|
|
it 'does not allow changing final grader to an inactive user' do
|
|
@section1_ta.enrollments.first.deactivate
|
|
@assignment.final_grader = @section1_ta
|
|
expect(@assignment).to be_invalid
|
|
end
|
|
|
|
it 'allows a non-active final grader if the final grader was set when the user was active' do
|
|
@assignment.update!(final_grader: @section1_ta)
|
|
@section1_ta.enrollments.first.deactivate
|
|
expect(@assignment).to be_valid
|
|
end
|
|
|
|
it 'does not allow a final grader not in the course' do
|
|
other_course = Course.create!(name: 'other course')
|
|
other_course_ta = ta_in_course(course: other_course).user
|
|
|
|
@assignment.final_grader = other_course_ta
|
|
@assignment.valid?
|
|
|
|
expect(errors[:final_grader]).to eq ['must be an instructor in this course']
|
|
end
|
|
end
|
|
|
|
describe 'graders_anonymous_to_graders' do
|
|
it 'cannot be set to true when grader_comments_visible_to_graders is false' do
|
|
@assignment.update!(grader_comments_visible_to_graders: false, graders_anonymous_to_graders: true)
|
|
expect(@assignment).not_to be_graders_anonymous_to_graders
|
|
end
|
|
|
|
it 'can be set to true when grader_comments_visible_to_graders is true' do
|
|
@assignment.update!(grader_comments_visible_to_graders: true, graders_anonymous_to_graders: true)
|
|
expect(@assignment).to be_graders_anonymous_to_graders
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'allowed_attempts validation' do
|
|
before(:once) do
|
|
assignment_model(course: @course)
|
|
end
|
|
|
|
it { is_expected.to validate_numericality_of(:allowed_attempts).allow_nil }
|
|
|
|
it 'should allow -1' do
|
|
@assignment.allowed_attempts = -1
|
|
expect(@assignment).to be_valid
|
|
end
|
|
|
|
it 'should disallow 0' do
|
|
@assignment.allowed_attempts = 0
|
|
expect(@assignment).to_not be_valid
|
|
end
|
|
|
|
it 'should disallow values less than -1' do
|
|
@assignment.allowed_attempts = -2
|
|
expect(@assignment).to_not be_valid
|
|
end
|
|
|
|
it 'should allow values greater than 0' do
|
|
@assignment.allowed_attempts = 2
|
|
expect(@assignment).to be_valid
|
|
end
|
|
end
|
|
|
|
describe "after create callbacks" do
|
|
subject(:event) { AnonymousOrModerationEvent.where(assignment: assignment).last }
|
|
|
|
let(:course) { @course }
|
|
|
|
it "does not create an AnonymousOrModerationEvent when assignment is neither anonymous nor moderated" do
|
|
expect{ course.assignments.create!(updating_user: @teacher) }.not_to change{ AnonymousOrModerationEvent.count }
|
|
end
|
|
|
|
it "does not create an AnonymousOrModerationEvent when assignment does not have an updating user" do
|
|
expect{ course.assignments.create!(anonymous_grading: true) }.not_to change{ AnonymousOrModerationEvent.count }
|
|
end
|
|
|
|
context "for an anonymous assignment" do
|
|
let(:assignment) do
|
|
course.assignments.create!(anonymous_grading: true, updating_user: @teacher)
|
|
end
|
|
|
|
it "creates only one AnonymousOrModerationEvent on creation" do
|
|
expect {
|
|
course.assignments.create!(anonymous_grading: true, updating_user: @teacher)
|
|
}.to change { AnonymousOrModerationEvent.count }.by(1)
|
|
end
|
|
|
|
it "creates an AnonymousOrModerationEvent with event_type assignment_created on assignment creation" do
|
|
course.assignments.create!(anonymous_grading: true, updating_user: @teacher)
|
|
expect(event.event_type).to eq "assignment_created"
|
|
end
|
|
|
|
describe "event payload" do
|
|
subject { event.payload }
|
|
|
|
it { is_expected.to include("anonymous_grading" => true) }
|
|
it { is_expected.to include("anonymous_instructor_annotations" => false) }
|
|
it { is_expected.to include("grader_comments_visible_to_graders" => true) }
|
|
it { is_expected.to include("grader_count" => 0) }
|
|
it { is_expected.to include("grader_names_visible_to_final_grader" => true) }
|
|
it { is_expected.to include("graders_anonymous_to_graders" => false) }
|
|
it { is_expected.to include("moderated_grading" => false) }
|
|
it { is_expected.to include("muted" => true) }
|
|
it { is_expected.to include("omit_from_final_grade" => false) }
|
|
end
|
|
end
|
|
|
|
context "for a moderated assignment" do
|
|
let(:assignment) do
|
|
course.assignments.create!(params)
|
|
end
|
|
|
|
let(:params) { { moderated_grading: true, final_grader: @teacher, grader_count: 2, updating_user: @teacher } }
|
|
|
|
it "creates exactly one AnonymousOrModerationEvent on creation " do
|
|
expect {
|
|
course.assignments.create!(params)
|
|
}.to change { AnonymousOrModerationEvent.count }.by(1)
|
|
end
|
|
|
|
it "creates an AnonymousOrModerationEvent with event_type assignment_created on assignment creation" do
|
|
course.assignments.create!(params)
|
|
expect(event.event_type).to eq "assignment_created"
|
|
end
|
|
|
|
describe "event_payload" do
|
|
subject { event.payload }
|
|
|
|
it { is_expected.to include("anonymous_grading" => false) }
|
|
it { is_expected.to include("anonymous_instructor_annotations" => false) }
|
|
it { is_expected.to include("final_grader_id" => @teacher.id) }
|
|
it { is_expected.to include("grader_comments_visible_to_graders" => true) }
|
|
it { is_expected.to include("grader_count" => 2) }
|
|
it { is_expected.to include("grader_names_visible_to_final_grader" => true) }
|
|
it { is_expected.to include("graders_anonymous_to_graders" => false) }
|
|
it { is_expected.to include("moderated_grading" => true) }
|
|
it { is_expected.to include("muted" => true) }
|
|
it { is_expected.to include("omit_from_final_grade" => false) }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'after save callbacks' do
|
|
let(:course) { @course }
|
|
|
|
before(:once) { @ta = ta_in_course(course: @course, enrollment_state: :active).user }
|
|
|
|
context "non-anonymous and non-moderated assignments" do
|
|
let(:assignment) { course.assignments.create!(updating_user: @teacher) }
|
|
|
|
context "when becoming an anonymous assignment" do
|
|
subject do
|
|
assignment.update!(anonymous_grading: true)
|
|
AnonymousOrModerationEvent.where(assignment: assignment).last.payload
|
|
end
|
|
|
|
it { is_expected.to include("anonymous_grading" => [false, true]) }
|
|
it { is_expected.to include("anonymous_instructor_annotations" => [false, false]) }
|
|
it { is_expected.to include("grader_comments_visible_to_graders" => [true, true]) }
|
|
it { is_expected.to include("grader_count" => [0, 0]) }
|
|
it { is_expected.to include("grader_names_visible_to_final_grader" => [true, true]) }
|
|
it { is_expected.to include("graders_anonymous_to_graders" => [false, false]) }
|
|
it { is_expected.to include("moderated_grading" => [false, false]) }
|
|
it { is_expected.to include("muted" => [true, true]) }
|
|
it { is_expected.to include("omit_from_final_grade" => [false, false]) }
|
|
end
|
|
|
|
context "when becoming a moderated assignment" do
|
|
subject(:payload) do
|
|
assignment.update!(moderated_grading: true, grader_count: 1, final_grader: @ta)
|
|
AnonymousOrModerationEvent.where(assignment: assignment).last.payload
|
|
end
|
|
|
|
it { is_expected.to include("anonymous_grading" => [false, false]) }
|
|
it { is_expected.to include("anonymous_instructor_annotations" => [false, false]) }
|
|
it { is_expected.to include("final_grader_id" => [nil, @ta.id]) }
|
|
it { is_expected.to include("grader_comments_visible_to_graders" => [true, true]) }
|
|
it { is_expected.to include("grader_count" => [0, 1]) }
|
|
it { is_expected.to include("grader_names_visible_to_final_grader" => [true, true]) }
|
|
it { is_expected.to include("graders_anonymous_to_graders" => [false, false]) }
|
|
it { is_expected.to include("moderated_grading" => [false, true]) }
|
|
it { is_expected.to include("muted" => [true, true]) }
|
|
it { is_expected.to include("omit_from_final_grade" => [false, false]) }
|
|
end
|
|
end
|
|
|
|
context 'given an anonymous assignment' do
|
|
subject(:event) { AnonymousOrModerationEvent.where(assignment: assignment).last }
|
|
|
|
let(:assignment) { course.assignments.create!(anonymous_grading: true, updating_user: @teacher) }
|
|
|
|
describe 'create a grades_posted event' do
|
|
context "when grades were posted" do
|
|
let(:event_type) { :grades_posted }
|
|
let(:now) { Time.zone.now }
|
|
|
|
it "creates an AnonymousOrModerationEvent with an 'event_type' of 'grades_posted'" do
|
|
expect { assignment.update!(grades_published_at: now) }.to change {
|
|
AnonymousOrModerationEvent.where(assignment: assignment).count
|
|
}.by(1)
|
|
end
|
|
|
|
it "sets the event user to the 'updating_user' on the assignment" do
|
|
assignment.update!(grades_published_at: now, updating_user: @ta)
|
|
expect(event).to have_attributes(user_id: @ta.id)
|
|
end
|
|
|
|
it "includes the 'grades_published_at' attribute in the event data" do
|
|
assignment.update!(grades_published_at: now)
|
|
expect(event.payload).to include('grades_published_at' => [nil, now.iso8601])
|
|
end
|
|
|
|
it "has no other values in the payload" do
|
|
assignment.update!(grades_published_at: now)
|
|
expect(event.payload.keys).to eq ['grades_published_at']
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'create an assignment_updated event' do
|
|
let(:event_type) { :assignment_updated }
|
|
|
|
it 'creates only one AnonymousOrModerationEvent on update' do
|
|
assignment = course.assignments.create!(anonymous_grading: true, updating_user: @teacher)
|
|
expect {
|
|
assignment.update!(muted: false)
|
|
}.to change { AnonymousOrModerationEvent.count }.by(1)
|
|
end
|
|
|
|
it 'creates an AnonymousOrModerationEvent with assignment changes when muted is changed' do
|
|
assignment.update!(muted: false)
|
|
expect(event.payload).to include('muted' => [true, false])
|
|
end
|
|
|
|
it 'creates an AnonymousOrModerationEvent with assignment changes when due_at is changed' do
|
|
now = Time.zone.now
|
|
assignment.update!(due_at: now)
|
|
expect(event.payload).to include('due_at' => [nil, now.iso8601])
|
|
end
|
|
|
|
it 'creates an AnonymousOrModerationEvent with assignment changes when anonymous_grading is changed' do
|
|
assignment.update!(anonymous_grading: false)
|
|
expect(event.payload).to include('anonymous_grading' => [true, false])
|
|
end
|
|
|
|
it 'creates an AnonymousOrModerationEvent with assignment changes when omit_from_final_grade is changed' do
|
|
assignment.update!(omit_from_final_grade: true)
|
|
expect(event.payload).to include('omit_from_final_grade' => [false, true])
|
|
end
|
|
|
|
it 'creates an AnonymousOrModerationEvent with assignment changes when anonymous_instructor_annotations is changed' do
|
|
assignment.update!(anonymous_instructor_annotations: true)
|
|
expect(event.payload).to include('anonymous_instructor_annotations' => [false, true])
|
|
end
|
|
|
|
it "does not create an AnonymousOrModerationEvent when non-grading-related attributes are updated" do
|
|
expect { assignment.update!(title: "Different Name") }.not_to change {
|
|
AnonymousOrModerationEvent.where(assignment: assignment).count
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'given a moderated assignment' do
|
|
subject(:event) { AnonymousOrModerationEvent.where(assignment: assignment).last }
|
|
|
|
let(:event_type) { :assignment_updated }
|
|
let(:assignment) { course.assignments.create!(params) }
|
|
let(:params) {
|
|
{
|
|
moderated_grading: true,
|
|
final_grader: @teacher,
|
|
grader_count: 2,
|
|
updating_user: @teacher
|
|
}
|
|
}
|
|
|
|
it 'creates only one AnonymousOrModerationEvent on update' do
|
|
assignment = course.assignments.create!(params)
|
|
expect {
|
|
assignment.update!(muted: false)
|
|
}.to change { AnonymousOrModerationEvent.count }.by(1)
|
|
end
|
|
|
|
it "creates an AnonymousOrModerationEvent with event_type assignment_updated on assignment update" do
|
|
assignment.update!(points_possible: 23)
|
|
expect(event.event_type).to eq "assignment_updated"
|
|
end
|
|
|
|
it 'creates an AnonymousOrModerationEvent with assignment changes when points_possible is changed' do
|
|
assignment.update!(points_possible: 23)
|
|
expect(event.payload).to include('points_possible' => [nil, 23.0])
|
|
end
|
|
|
|
it 'creates an AnonymousOrModerationEvent with assignment changes when moderated_grading is changed' do
|
|
assignment.update!(moderated_grading: false)
|
|
expect(event.payload).to include('moderated_grading' => [true, false])
|
|
end
|
|
|
|
it 'creates an AnonymousOrModerationEvent with assignment changes when final_grader_id is changed' do
|
|
assignment.update!(final_grader: @ta)
|
|
expect(event.payload).to include('final_grader_id' => [@teacher.id, @ta.id])
|
|
end
|
|
|
|
it 'creates an AnonymousOrModerationEvent with assignment changes when grader_count is changed' do
|
|
assignment.update!(grader_count: 70)
|
|
expect(event.payload).to include('grader_count' => [2, 70])
|
|
end
|
|
|
|
it 'creates an AnonymousOrModerationEvent with assignment changes when grader_names_visible_to_final_grader is changed' do
|
|
assignment.update!(grader_names_visible_to_final_grader: false)
|
|
expect(event.payload).to include('grader_names_visible_to_final_grader' => [true, false])
|
|
end
|
|
|
|
it 'creates an AnonymousOrModerationEvent with assignment changes when grader_comments_visible_to_graders is changed' do
|
|
assignment.update!(grader_comments_visible_to_graders: false)
|
|
expect(event.payload).to include('grader_comments_visible_to_graders' => [true, false])
|
|
end
|
|
|
|
it 'creates an AnonymousOrModerationEvent with assignment changes when graders_anonymous_to_graders is changed' do
|
|
assignment.update!(graders_anonymous_to_graders: true)
|
|
expect(event.payload).to include('graders_anonymous_to_graders' => [false, true])
|
|
end
|
|
end
|
|
|
|
describe '#update_line_items' do
|
|
let(:use_1_3) { true }
|
|
let(:dev_key) { DeveloperKey.create! }
|
|
let(:tool) do
|
|
course.context_external_tools.create!(
|
|
consumer_key: 'key',
|
|
shared_secret: 'secret',
|
|
name: 'test tool',
|
|
url: 'http://www.tool.com/launch',
|
|
settings: { use_1_3: use_1_3 },
|
|
workflow_state: 'public',
|
|
developer_key: dev_key
|
|
)
|
|
end
|
|
let(:assignment) do
|
|
@course.assignments.create!(submission_types: 'external_tool',
|
|
external_tool_tag_attributes: { content: tool },
|
|
**assignment_valid_attributes)
|
|
end
|
|
|
|
shared_examples 'line item and resource link existence check' do
|
|
it 'has a line item and a resource link referencing the currently bound tool' do
|
|
expect(assignment.line_items.length).to eq 1
|
|
expect(assignment.line_items.first.label).to eq assignment.title
|
|
expect(assignment.line_items.first.score_maximum).to eq assignment.points_possible
|
|
expect(assignment.line_items.first.resource_link).not_to be_nil
|
|
expect(assignment.line_items.first.resource_link.resource_link_id).to eq assignment.lti_context_id
|
|
expect(assignment.line_items.first.resource_link.current_external_tool(assignment.context)).to eq tool
|
|
expect(assignment.external_tool_tag.content).to eq tool
|
|
expect(assignment.line_items.first.resource_link.line_items.first).to eq assignment.line_items.first
|
|
end
|
|
end
|
|
|
|
shared_examples 'assignment to line item attribute sync check' do
|
|
it 'synchronizes assignment title and points_possible changes to the primary line item' do
|
|
# create a secondary line item (i.e. one that should not be synchronized)
|
|
previous_title = assignment.title
|
|
previous_points_possible = assignment.points_possible
|
|
first_line_item = assignment.line_items.first
|
|
line_item_two = assignment.line_items.create!(
|
|
label: previous_title,
|
|
score_maximum: previous_points_possible,
|
|
resource_link: first_line_item.resource_link
|
|
)
|
|
line_item_two.update!(created_at: first_line_item.created_at + 1.minute)
|
|
assignment.title += " edit"
|
|
assignment.points_possible += 10
|
|
assignment.save!
|
|
assignment.reload
|
|
expect(assignment.line_items.length).to eq 2
|
|
expect(assignment.line_items.find(&:assignment_line_item?).label).to eq assignment.title
|
|
expect(assignment.line_items.find(&:assignment_line_item?).score_maximum).to eq assignment.points_possible
|
|
expect(assignment.line_items.find { |li| !li.assignment_line_item? }.label).to eq previous_title
|
|
expect(assignment.line_items.find { |li| !li.assignment_line_item? }.score_maximum).to eq previous_points_possible
|
|
end
|
|
end
|
|
|
|
context 'given an assignment bound to a LTI 1.3 tool' do
|
|
|
|
it_behaves_like 'line item and resource link existence check'
|
|
it_behaves_like 'assignment to line item attribute sync check'
|
|
|
|
context 'and no resource link or line item exist' do
|
|
let(:resource_link) { subject.line_items.first.resource_link }
|
|
let(:line_item) { subject.line_items.first }
|
|
|
|
before do
|
|
resource_link
|
|
line_item
|
|
subject.line_items.destroy_all
|
|
resource_link.destroy!
|
|
subject.update!(lti_context_id: SecureRandom.uuid)
|
|
subject.create_assignment_line_item!
|
|
end
|
|
|
|
describe '#create_assignment_line_item!' do
|
|
subject { assignment }
|
|
|
|
it 'sets a line item' do
|
|
expect(subject.line_items.active.count).to eq 1
|
|
end
|
|
|
|
it 'creates a new assignment line item' do
|
|
expect(subject.line_items.first).not_to eq line_item
|
|
end
|
|
|
|
it 'sets a resource link' do
|
|
expect(subject.line_items.first.resource_link).to be_present
|
|
end
|
|
|
|
it 'creates a new resource link' do
|
|
expect(subject.line_items.first.resource_link).not_to eq resource_link
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'and resource link and line item exist' do
|
|
let(:resource_link) { subject.line_items.first.resource_link }
|
|
let(:line_item) { subject.line_items.first }
|
|
|
|
describe '#create_assignment_line_item!' do
|
|
subject { assignment }
|
|
|
|
before { subject.create_assignment_line_item! }
|
|
|
|
it 'does not add a new line item' do
|
|
expect(subject.line_items.count).to eq 1
|
|
end
|
|
|
|
it 'does not replace the existing line item' do
|
|
expect(subject.line_items.first).to eq line_item
|
|
end
|
|
|
|
it 'does not replace the existing resource link' do
|
|
expect(subject.line_items.first.resource_link).to eq resource_link
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'and the tool binding is changed' do
|
|
let(:different_tool_use_1_3) { true }
|
|
let!(:different_tool) do
|
|
course.context_external_tools.create!(
|
|
consumer_key: 'key2',
|
|
shared_secret: 'secret2',
|
|
name: 'test tool 2',
|
|
url: 'http://www.tool2.com/launch',
|
|
settings: { use_1_3: different_tool_use_1_3 },
|
|
workflow_state: 'public'
|
|
)
|
|
end
|
|
|
|
before(:each) do
|
|
assignment.update!(external_tool_tag_attributes: { content: different_tool })
|
|
assignment.reload
|
|
end
|
|
|
|
shared_examples 'unchanged line item and resource link check' do
|
|
it 'does not change nor add to the line item nor resource link' do
|
|
expect(assignment.line_items.length).to eq 1
|
|
expect(assignment.line_items.first.resource_link.current_external_tool(assignment.context)).to eq tool
|
|
# some sanity checks to make sure the update did what we thought it did
|
|
expect(different_tool.id).not_to eq tool.id
|
|
expect(assignment.external_tool_tag.content.id).to eq different_tool.id
|
|
end
|
|
end
|
|
|
|
# rubocop:disable RSpec/NestedGroups
|
|
context 'to a different LTI 1.3 tool' do
|
|
it_behaves_like 'unchanged line item and resource link check'
|
|
it_behaves_like 'assignment to line item attribute sync check'
|
|
end
|
|
|
|
context 'to a different non-LTI 1.3 tool' do
|
|
let(:different_tool_use_1_3) { false }
|
|
|
|
it_behaves_like 'unchanged line item and resource link check'
|
|
it_behaves_like 'assignment to line item attribute sync check'
|
|
end
|
|
# rubocop:enable RSpec/NestedGroups
|
|
end
|
|
|
|
context 'and the tool binding is abandoned' do
|
|
it 'does not delete the line item nor resource link' do
|
|
assignment.update!(submission_types: 'none')
|
|
assignment.reload
|
|
expect(assignment.line_items.length).to eq 1
|
|
expect(assignment.line_items.first.resource_link.current_external_tool(assignment.context)).to eq tool
|
|
end
|
|
|
|
it_behaves_like 'assignment to line item attribute sync check'
|
|
end
|
|
end
|
|
|
|
context 'given an assignment bound to a non-LTI 1.3 tool' do
|
|
let(:use_1_3) { false }
|
|
|
|
it 'does not create line items and resource links' do
|
|
expect(assignment.line_items).to be_empty
|
|
end
|
|
|
|
describe "#create_assignment_line_items!" do
|
|
subject { assignment }
|
|
|
|
it 'does not create a new line item' do
|
|
expect do
|
|
subject.create_assignment_line_item!
|
|
end.not_to change { Lti::LineItem.count }
|
|
end
|
|
|
|
it 'does not associate a line item with the assignment' do
|
|
expect(subject.line_items).to be_empty
|
|
end
|
|
|
|
it 'does not create a new resource link' do
|
|
expect do
|
|
subject.create_assignment_line_item!
|
|
end.not_to change { Lti::ResourceLink.count }
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'given an assignment not yet bound to a LTI 1.3 tool' do
|
|
let(:assignment) do
|
|
@course.assignments.create!(submission_types: 'external_tool',
|
|
**assignment_valid_attributes)
|
|
end
|
|
|
|
it 'initially has no line items nor resource links' do
|
|
expect(assignment.line_items).to be_empty
|
|
end
|
|
|
|
context 'but when a LTI 1.3 tool is subsequently added' do
|
|
before do
|
|
assignment.update!(external_tool_tag_attributes: { content: tool })
|
|
end
|
|
|
|
it_behaves_like 'line item and resource link existence check'
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'sis_source_id' do
|
|
it 'is unique' do
|
|
Assignment.create!(course: @course, name: 'some assignment', sis_source_id: 'BLAH')
|
|
expect {
|
|
Assignment.create!(course: @course, name: 'some assignment', sis_source_id: 'BLAH')
|
|
}.to raise_error(ActiveRecord::RecordNotUnique)
|
|
end
|
|
end
|
|
|
|
def setup_assignment_with_group
|
|
assignment_model(:group_category => "Study Groups", :course => @course)
|
|
@group = @a.context.groups.create!(:name => "Study Group 1", :group_category => @a.group_category)
|
|
@u1 = @a.context.enroll_user(User.create(:name => "user 1")).user
|
|
@u2 = @a.context.enroll_user(User.create(:name => "user 2")).user
|
|
@u3 = @a.context.enroll_user(User.create(:name => "user 3")).user
|
|
@group.add_user(@u1)
|
|
@group.add_user(@u2)
|
|
@assignment.reload
|
|
end
|
|
|
|
def setup_assignment_without_submission
|
|
assignment_model(:course => @course)
|
|
@assignment.reload
|
|
end
|
|
|
|
def setup_assignment_with_homework
|
|
setup_assignment_without_submission
|
|
@assignment.submit_homework(@user, {:submission_type => 'online_text_entry', :body => 'blah'})
|
|
@assignment.reload
|
|
end
|
|
|
|
def setup_assignment_with_students
|
|
@graded_notify = Notification.create!(:name => "Submission Graded")
|
|
@grade_change_notify = Notification.create!(:name => "Submission Grade Changed")
|
|
@stu1 = @student
|
|
communication_channel(@stu1, active_cc: true)
|
|
@course.enroll_student(@stu2 = user_factory(active_user: true, active_cc: true))
|
|
@assignment = @course.assignments.create(:title => "a title", :points_possible => 10)
|
|
|
|
[@stu1, @stu2].each do |stu|
|
|
[@graded_notify, @grade_change_notify].each do |notification|
|
|
notification_policy_model(
|
|
notification: notification,
|
|
communication_channel: stu.communication_channels.first
|
|
)
|
|
end
|
|
end
|
|
|
|
@sub1 = @assignment.grade_student(@stu1, grade: 9, grader: @teacher).first
|
|
@assignment.reload
|
|
end
|
|
|
|
def submit_homework(student)
|
|
file_context = @assignment.group_category.group_for(student) if @assignment.has_group_category?
|
|
file_context ||= student
|
|
a = Attachment.create! context: file_context,
|
|
filename: "homework.pdf",
|
|
uploaded_data: StringIO.new("blah blah blah")
|
|
@assignment.submit_homework(student, attachments: [a],
|
|
submission_type: "online_upload")
|
|
a
|
|
end
|
|
|
|
def zip_submissions_legacy
|
|
zip = Attachment.new filename: 'submissions.zip'
|
|
zip.user = @teacher
|
|
zip.workflow_state = 'to_be_zipped'
|
|
zip.context = @assignment
|
|
zip.save!
|
|
ContentZipper.process_attachment(zip, @teacher)
|
|
raise "zip failed" if zip.workflow_state != "zipped"
|
|
zip
|
|
end
|
|
|
|
def setup_differentiated_assignments(opts={})
|
|
if !opts[:course]
|
|
course_with_teacher(active_all: true)
|
|
end
|
|
|
|
@section1 = @course.course_sections.create!(name: 'Section One')
|
|
@section2 = @course.course_sections.create!(name: 'Section Two')
|
|
|
|
if opts[:ta]
|
|
@ta = course_with_ta(course: @course, active_all: true).user
|
|
end
|
|
|
|
@student1, @student2, @student3 = create_users(3, return_type: :record)
|
|
student_in_section(@section1, user: @student1)
|
|
student_in_section(@section2, user: @student2)
|
|
|
|
@assignment = assignment_model(course: @course, submission_types: "online_url", workflow_state: "published")
|
|
@override_s1 = differentiated_assignment(assignment: @assignment, course_section: @section1)
|
|
@override_s1.due_at = 1.day.from_now
|
|
@override_s1.save!
|
|
end
|
|
|
|
describe Assignment::MaxGradersReachedError do
|
|
subject { Assignment::MaxGradersReachedError.new }
|
|
|
|
it { is_expected.to be_a Assignment::GradeError }
|
|
|
|
it 'has an error_code of MAX_GRADERS_REACHED' do
|
|
expect(subject.error_code).to eq 'MAX_GRADERS_REACHED'
|
|
end
|
|
end
|
|
end
|