canvas-lms/spec/models/submission_spec.rb

9418 lines
376 KiB
Ruby

# frozen_string_literal: true
#
# Copyright (C) 2011 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require_relative "../lib/validates_as_url"
describe Submission do
subject(:submission) { Submission.new }
before(:once) do
course_with_teacher(active_all: true)
course_with_student(active_all: true, course: @course)
@context = @course
@assignment = @context.assignments.create!(
title: "some assignment",
workflow_state: "published"
)
@valid_attributes = {
assignment: @assignment,
user: @user,
grade: "1.5",
grader: @teacher,
url: "www.instructure.com",
workflow_state: "submitted"
}
end
it { is_expected.to validate_numericality_of(:points_deducted).is_greater_than_or_equal_to(0).allow_nil }
it { is_expected.to validate_numericality_of(:seconds_late_override).is_greater_than_or_equal_to(0).allow_nil }
it { is_expected.to validate_inclusion_of(:late_policy_status).in_array(%w[none missing late extended]).allow_nil }
it { is_expected.to validate_inclusion_of(:cached_tardiness).in_array(["missing", "late"]).allow_nil }
it { is_expected.to delegate_method(:auditable?).to(:assignment).with_prefix(true) }
it { is_expected.to delegate_method(:can_be_moderated_grader?).to(:assignment).with_prefix(true) }
describe "inferred values" do
subject do
submission.infer_values
submission.state
end
let(:student) { @student }
let(:assignment) { @assignment }
describe "workflow_state" do
context "when current state is unsubmitted and submitted_at is present" do
before do
submission.workflow_state = "unsubmitted"
submission.submission_type = "online_text_entry"
submission.submitted_at = Time.zone.now
end
it { is_expected.to be :submitted }
end
context "when current state is submitted and has_submission is false" do
before do
submission.workflow_state = "submitted"
end
it { is_expected.to be :unsubmitted }
end
context "when grade and score are present and grade matches current submission" do
before do
submission.submission_type = "online_text_entry"
submission.submitted_at = Time.zone.now
submission.grade = "5"
submission.score = 5
allow(submission).to receive(:grade_matches_current_submission).and_return(true)
end
it { is_expected.to be :graded }
end
context "when submission_type is online_quiz and latest submission is pending review" do
before do
submission.workflow_state = "pending_review"
submission.submission_type = "online_quiz"
allow(submission).to receive(:quiz_submission).and_return(double("QuizSubmission", "pending_review?" => true))
end
it { is_expected.to be :pending_review }
end
context "when workflow state is pending_review" do
before do
submission.workflow_state = "pending_review"
end
context "and the submission was graded by quizzes" do
before do
submission.grader_id = -1
submission.cached_quiz_lti = true
end
it { is_expected.to be :pending_review }
context "and the submission was manually given a late policy status of missing" do
before do
submission.grader_id = @teacher.id
submission.cached_quiz_lti = true
submission.late_policy_status = "missing"
end
it { is_expected.to be :pending_review }
end
context "and the submission was manually given a late policy status of late" do
before do
submission.grader_id = @teacher.id
submission.cached_quiz_lti = true
submission.late_policy_status = "late"
end
it { is_expected.to be :pending_review }
end
end
end
context "the submission's Lti::Result was marked as PendingManual by an external tool" do
let(:tool) { external_tool_1_3_model }
let(:result) { lti_result_model(result_overrides) }
let(:submission) { result.submission }
let(:result_overrides) do
{
assignment:,
grading_progress: "PendingManual",
result_score: assignment.points_possible,
result_maximum: assignment.points_possible,
tool:
}
end
it "marks the submission as needing review" do
submission.infer_values
expect(submission).to be_pending_review
end
end
end
end
describe ".json_serialization_full_parameters" do
it "can be provided additional methods to include in the params" do
params = Submission.json_serialization_full_parameters(methods: [:missing])
expect(params[:methods]).to include :missing
end
it "can provide an additional method with singular form (no array)" do
params = Submission.json_serialization_full_parameters(methods: :missing)
expect(params[:methods]).to include :missing
end
end
describe "#tool_default_query_params" do
context "new quiz submissions" do
before do
@course.context_external_tools.create!(
name: "Quizzes.Next",
consumer_key: "test_key",
shared_secret: "test_secret",
tool_id: "Quizzes 2",
url: "http://example.com/launch"
)
@assignment.quiz_lti!
@assignment.save!
end
let(:submission) { @assignment.submissions.find_by!(user: @student) }
it "returns grade_by_question_enabled: true when grade by question is enabled" do
@teacher.update!(preferences: { enable_speedgrader_grade_by_question: true })
query_params = submission.tool_default_query_params(@teacher)
expect(query_params[:grade_by_question_enabled]).to be true
end
it "returns grade_by_question_enabled: false when grade by question is disabled" do
query_params = submission.tool_default_query_params(@teacher)
expect(query_params[:grade_by_question_enabled]).to be false
end
end
it "returns an empty array for a non-new-quiz submission" do
expect(submission.tool_default_query_params(@teacher)).to be_empty
end
end
describe "#anonymous_id" do
subject { submission.anonymous_id }
let(:student) { @student }
let(:assignment) { @assignment }
it { is_expected.to be_blank }
it "sets an anoymous_id on validation" do
submission.validate
expect(submission.anonymous_id).to be_present
end
it "does not change if already persisted" do
submission = assignment.submissions.find_by!(user: student)
expect { submission.save! }.not_to change { submission.anonymous_id }
end
end
describe "#type_for_attempt" do
before(:once) do
@assignment.update!(submission_types: "online_text_entry,online_url")
now = Time.zone.now
Timecop.freeze(10.minutes.from_now(now)) do
@assignment.submit_homework(@student, body: "hi", submission_type: "online_text_entry")
end
Timecop.freeze(20.minutes.from_now(now)) do
@assignment.submit_homework(@student, url: "https://www.google.com", submission_type: "online_url")
end
end
let(:submission) { @assignment.submissions.find_by(user: @student) }
it "returns the correct submission type given the attempt number" do
aggregate_failures do
expect(submission.type_for_attempt(1)).to eq "online_text_entry"
expect(submission.type_for_attempt(2)).to eq "online_url"
end
end
it "returns nil if given a non-existent attempt number" do
expect(submission.type_for_attempt(3)).to be_nil
end
end
describe ".anonymous_ids_for" do
subject { Submission.anonymous_ids_for(@first_assignment) }
before do
student_with_anonymous_ids = @student
student_without_anonymous_ids = student_in_course(course: @course, active_all: true).user
@first_assignment = @course.assignments.create!
@course.assignments.create! # second_assignment
@first_assignment_submission = @first_assignment.submissions.find_by!(user: student_with_anonymous_ids)
Submission.where(user: student_without_anonymous_ids).update_all(anonymous_id: nil)
end
it "only contains submissions with anonymous_ids" do
expect(subject).to contain_exactly(@first_assignment_submission.anonymous_id)
end
end
describe "with grading periods" do
let(:in_closed_grading_period) { 9.days.ago }
let(:in_open_grading_period) { 1.day.from_now }
let(:outside_of_any_grading_period) { 10.days.from_now }
before(:once) do
@root_account = @context.root_account
group = @root_account.grading_period_groups.create!
@closed_period = group.grading_periods.create!(
title: "Closed!",
start_date: 2.weeks.ago,
end_date: 1.week.ago,
close_date: 3.days.ago
)
@open_period = group.grading_periods.create!(
title: "Open!",
start_date: 3.days.ago,
end_date: 3.days.from_now,
close_date: 5.days.from_now
)
group.enrollment_terms << @context.enrollment_term
end
describe "permissions" do
before(:once) do
@admin = user_factory(active_all: true)
@root_account.account_users.create!(user: @admin)
@teacher = user_factory(active_all: true)
@context.enroll_teacher(@teacher)
end
describe "grade" do
context "the submission is deleted" do
before(:once) do
@assignment.due_at = in_open_grading_period
@assignment.save!
submission_spec_model
@submission.update(workflow_state: "deleted")
end
it "does not have grade permissions if the user is a root account admin" do
expect(@submission.grants_right?(@admin, :grade)).to be(false)
end
it "does not have grade permissions if the user is non-root account admin with manage_grades permissions" do
expect(@submission.grants_right?(@teacher, :grade)).to be(false)
end
it "doesn't have grade permissions if the user is non-root account admin without manage_grades permissions" do
@student = user_factory(active_all: true)
@context.enroll_student(@student)
expect(@submission.grants_right?(@student, :grade)).to be(false)
end
end
context "the submission is due in an open grading period" do
before(:once) do
@assignment.due_at = in_open_grading_period
@assignment.save!
submission_spec_model
end
it "has grade permissions if the user is a root account admin" do
expect(@submission.grants_right?(@admin, :grade)).to be(true)
end
it "has grade permissions if the user is non-root account admin with manage_grades permissions" do
expect(@submission.grants_right?(@teacher, :grade)).to be(true)
end
it "doesn't have grade permissions if the user is non-root account admin without manage_grades permissions" do
@student = user_factory(active_all: true)
@context.enroll_student(@student)
expect(@submission.grants_right?(@student, :grade)).to be(false)
end
end
context "the submission is due outside of any grading period" do
before(:once) do
@assignment.due_at = outside_of_any_grading_period
@assignment.save!
submission_spec_model
end
it "has grade permissions if the user is a root account admin" do
expect(@submission.grants_right?(@admin, :grade)).to be(true)
end
it "has grade permissions if the user is non-root account admin with manage_grades permissions" do
expect(@submission.grants_right?(@teacher, :grade)).to be(true)
end
it "doesn't have grade permissions if the user is non-root account admin without manage_grades permissions" do
@student = user_factory(active_all: true)
@context.enroll_student(@student)
expect(@submission.grants_right?(@student, :grade)).to be(false)
end
end
context "when part of a moderated assignment" do
before(:once) do
@assignment.update!(
moderated_grading: true,
grader_count: 2
)
submission_spec_model(assignment: @assignment)
end
it "may not be graded if grades are not published" do
expect(@submission.grants_right?(@teacher, :grade)).to be false
end
it "sets an error message indicating moderation is in progress if grades are not published" do
@submission.grants_right?(@teacher, :grade)
expect(@submission.grading_error_message).to eq "This assignment is currently being moderated"
end
it "may be graded if grades are not published but grade_posting_in_progress is true" do
@submission.grade_posting_in_progress = true
expect(@submission.grants_right?(@teacher, :grade)).to be true
end
it "may be graded when grades for the assignment are published" do
@submission.assignment.update!(grades_published_at: Time.zone.now)
expect(@submission.grants_right?(@teacher, :grade)).to be true
end
end
end
describe "make_group_comment" do
let_once(:course) { Course.create! }
let_once(:student) { course.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user }
let_once(:student2) { course.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user }
let_once(:teacher) { course.enroll_user(User.create!, "TeacherEnrollment", enrollment_state: "active").user }
before(:once) do
all_groups = @course.group_categories.create!(name: "all groups")
all_groups.groups.create!(context: @course, name: "group 1").add_user(student)
all_groups.groups.create!(context: @course, name: "group 2").add_user(student2)
@group_assignment = course.assignments.create!(
grade_group_students_individually: false,
group_category: all_groups,
name: "group assignment"
)
@submission = @group_assignment.submissions.find_by(user: student)
end
it "allows a student to make a group comment for their own submission" do
expect(@submission.grants_right?(student, :make_group_comment)).to be true
end
it "allows a teacher to make a group comment" do
expect(@submission.grants_right?(teacher, :make_group_comment)).to be true
end
it "allows a peer reviewer to make a group comment for their assigned submission" do
@group_assignment.update!(peer_reviews: true)
peer_submission = @group_assignment.submissions.find_by(user: student2)
AssessmentRequest.create!(assessor: student2, assessor_asset: peer_submission, asset: @submission, user: student)
expect(@submission.grants_right?(student2, :make_group_comment)).to be true
end
it "does not allow a student not peer reviewing to make a group comment" do
@group_assignment.update!(peer_reviews: true)
expect(@submission.grants_right?(student2, :make_group_comment)).to be false
end
end
end
end
describe "update_quiz_submission" do
before do
submission.workflow_state = "pending_review"
submission.submission_type = "online_quiz"
end
it "does not set_final_score if kept_score equals score without deductions" do
quiz_submission_mock = double("QuizSubmission", "kept_score" => 123)
allow(submission).to receive(:quiz_submission).and_return(quiz_submission_mock)
submission.update(score: 100, points_deducted: 23, quiz_submission_id: 1)
expect(quiz_submission_mock).not_to receive(:set_final_score)
submission.update_quiz_submission
end
it "does set_final_score if kept_score differs from score without deductions" do
quiz_submission_mock = double("QuizSubmission", "kept_score" => 100)
allow(submission).to receive(:quiz_submission).and_return(quiz_submission_mock)
submission.update(score: 100, points_deducted: 23, quiz_submission_id: 1)
expect(quiz_submission_mock).to receive(:set_final_score)
submission.update_quiz_submission
end
end
describe "entered_score" do
let(:submission) { @assignment.submissions.find_by!(user_id: @student) }
it "returns nil if score is not present" do
expect(submission.entered_score).to be_nil
end
it "returns score if no points deducted" do
submission.update(score: 123)
expect(submission.entered_score).to eql(submission.score)
end
it "returns score without deduction" do
submission.update(score: 100, points_deducted: 23)
expect(submission.entered_score).to eq 123
end
it "returns the score without deduction when late policy is disabled" do
late_policy_factory(course: @course, deduct: 2.35, every: :day)
@assignment.update!(due_at: 1.hour.ago, points_possible: 10, submission_types: "online_text_entry")
@assignment.submit_homework(@student, body: "late submission")
@assignment.grade_student(@student, grade: 10, grader: @teacher)
@course.late_policy.update!(late_submission_deduction_enabled: false)
expect(submission.entered_score).to eq 10.0
end
end
describe "entered_grade" do
let(:submission) { @assignment.submissions.find_by!(user_id: @student) }
it "returns grade without deduction" do
@assignment.update(grading_type: "percent", points_possible: 100)
submission.update(score: 25.5, points_deducted: 60)
expect(submission.entered_grade).to eql("85.5%")
end
it "returns grade if grading_type is pass_fail" do
@assignment.update(grading_type: "pass_fail")
submission.update(grade: "complete")
expect(submission.entered_grade).to eql("complete")
end
it "returns the grade for a letter grade assignment with no points possible" do
@assignment.update(grading_type: "letter_grade", points_possible: 0)
submission.update(grade: "B")
expect(submission.entered_grade).to eql("B")
end
it "returns the grade for a GPA scale assignment with no points possible" do
@assignment.update(grading_type: "gpa_scale", points_possible: nil)
submission.update(grade: "B")
expect(submission.entered_grade).to eql("B")
end
end
describe "cached_due_date" do
before(:once) do
@now = Time.zone.local(2013, 10, 18)
end
let(:submission) { @assignment.submissions.find_by!(user_id: @student) }
it "gets initialized during submission creation" do
# create an invited user, so that the submission is not automatically
# created by the SubmissionLifecycleManager
student_in_course(active_all: true)
@assignment.update_attribute(:due_at, 1.day.ago)
override = @assignment.assignment_overrides.build
override.title = "Some Title"
override.set = @course.default_section
override.override_due_at(1.day.from_now)
override.save!
submission = @assignment.submissions.find_by!(user: @user)
expect(submission.cached_due_date).to eq override.reload.due_at.change(usec: 0)
end
it "does not truncate seconds off of cached due dates" do
time = DateTime.parse("2018-12-24 23:59:59")
@assignment.update_attribute(:due_at, time)
submission = @assignment.submissions.find_by!(user: @user)
expect(submission.cached_due_date.to_i).to eq time.to_i
end
context "due date changes after student submits" do
before(:once) do
Timecop.freeze(@now) do
@assignment.update!(due_at: 20.minutes.ago, submission_types: "online_text_entry")
@assignment.submit_homework(@student, body: "a body")
end
end
it "changes if the assignment due date changes" do
expect { @assignment.update!(due_at: 15.minutes.ago(@now)) }.to change {
submission.reload.cached_due_date
}.from(20.minutes.ago(@now)).to(15.minutes.ago(@now))
end
context "student overrides" do
before(:once) do
@override = @assignment.assignment_overrides.create!(due_at: 15.minutes.ago(@now), due_at_overridden: true)
end
it "changes if an override is added for the student" do
expect { @override.assignment_override_students.create!(user: @student) }.to change {
submission.reload.cached_due_date
}.from(20.minutes.ago(@now)).to(15.minutes.ago(@now))
end
it "changes if an individual override is added for the student, even when the due date is earlier " \
"than an existing override that applies to the student for the assignment" do
section = @course.course_sections.create!(name: "My Awesome Section")
student_in_section(section, user: @student)
@assignment.assignment_overrides.create!(
due_at: 10.minutes.ago(@now),
due_at_overridden: true,
set: section
)
override = @assignment.assignment_overrides.create!(
due_at: 15.minutes.ago(@now),
due_at_overridden: true
)
expect { override.assignment_override_students.create!(user: @student) }.to change {
submission.reload.cached_due_date
}.from(10.minutes.ago(@now)).to(15.minutes.ago(@now))
end
it "does not change if a non-individual-override is added for the student and the due date " \
"is earlier than an existing override that applies to the student for the assignment" do
category = @course.group_categories.create!(name: "New Group Category")
group = @course.groups.create!(group_category: category)
group.add_user(@student, "active")
assignment = @course.assignments.create!(group_category: category)
section = @course.course_sections.create!(name: "My Awesome Section")
student_in_section(section, user: @student)
assignment.assignment_overrides.create!(
due_at: 10.minutes.ago(@now),
due_at_overridden: true,
set: section
)
expect do
assignment.assignment_overrides.create!(
due_at: 15.minutes.ago(@now),
due_at_overridden: true,
set: group
)
end.not_to change { submission.reload.cached_due_date }
end
it "changes if an override is removed for the student" do
@override.assignment_override_students.create!(user: @student)
expect { @override.assignment_override_students.find_by(user_id: @student).destroy }.to change {
submission.reload.cached_due_date
}.from(15.minutes.ago(@now)).to(20.minutes.ago(@now))
end
it "changes if the due date for the override is changed" do
@override.assignment_override_students.create!(user: @student, workflow_state: "active")
expect { @override.update!(due_at: 14.minutes.ago(@now)) }.to change {
submission.reload.cached_due_date
}.from(15.minutes.ago(@now)).to(14.minutes.ago(@now))
end
end
context "section overrides" do
before(:once) do
@section = @course.course_sections.create!(name: "My Awesome Section")
end
it "changes if an override is added for the section the student is in" do
student_in_section(@section, user: @student)
expect do
@assignment.assignment_overrides.create!(
due_at: 15.minutes.ago(@now),
due_at_overridden: true,
set: @section
)
end.to change {
submission.reload.cached_due_date
}.from(20.minutes.ago(@now)).to(15.minutes.ago(@now))
end
it "changes if a student is added to the section after the override is added for the section" do
@assignment.assignment_overrides.create!(
due_at: 15.minutes.ago(@now),
due_at_overridden: true,
set: @section
)
expect { student_in_section(@section, user: @student) }.to change {
submission.reload.cached_due_date
}.from(20.minutes.ago(@now)).to(15.minutes.ago(@now))
end
it "changes if a student is removed from a section with a due date for the assignment" do
@assignment.assignment_overrides.create!(
due_at: 15.minutes.ago(@now),
due_at_overridden: true,
set: @section
)
@course.enroll_student(@student, section: @section, allow_multiple_enrollments: true).accept!
expect { @student.enrollments.find_by(course_section_id: @section).destroy }.to change {
submission.reload.cached_due_date
}.from(15.minutes.ago(@now)).to(20.minutes.ago(@now))
end
it "changes if the due date for the override is changed" do
student_in_section(@section, user: @student)
override = @assignment.assignment_overrides.create!(
due_at: 15.minutes.ago(@now),
due_at_overridden: true,
set: @section
)
expect { override.update!(due_at: 14.minutes.ago(@now)) }.to change {
submission.reload.cached_due_date
}.from(15.minutes.ago(@now)).to(14.minutes.ago(@now))
end
end
context "group overrides" do
before(:once) do
category = @course.group_categories.create!(name: "New Group Category")
@group = @course.groups.create!(group_category: category)
@assignment = @course.assignments.create!(group_category: category, due_at: 20.minutes.ago(@now))
@assignment.submit_homework(@student, body: "a body")
end
it "changes if an override is added for the group the student is in" do
@group.add_user(@student, "active")
expect do
@assignment.assignment_overrides.create!(
due_at: 15.minutes.ago(@now),
due_at_overridden: true,
set: @group
)
end.to change {
submission.reload.cached_due_date
}.from(20.minutes.ago(@now)).to(15.minutes.ago(@now))
end
it "changes if a student is added to the group after the override is added for the group" do
@assignment.assignment_overrides.create!(
due_at: 15.minutes.ago(@now),
due_at_overridden: true,
set: @group
)
expect { @group.add_user(@student, "active") }.to change {
submission.reload.cached_due_date
}.from(20.minutes.ago(@now)).to(15.minutes.ago(@now))
end
it "changes if a student is removed from a group with a due date for the assignment" do
@group.add_user(@student, "active")
@assignment.assignment_overrides.create!(
due_at: 15.minutes.ago(@now),
due_at_overridden: true,
set: @group
)
expect { @group.group_memberships.find_by(user_id: @student).destroy }.to change {
submission.reload.cached_due_date
}.from(15.minutes.ago(@now)).to(20.minutes.ago(@now))
end
it "changes if the due date for the override is changed" do
@group.add_user(@student, "active")
override = @assignment.assignment_overrides.create!(
due_at: 15.minutes.ago(@now),
due_at_overridden: true,
set: @group
)
expect { override.update!(due_at: 14.minutes.ago(@now)) }.to change {
submission.reload.cached_due_date
}.from(15.minutes.ago(@now)).to(14.minutes.ago(@now))
end
end
it "uses the individual override date, otherwise most lenient, if there are multiple overrides" do
category = @course.group_categories.create!(name: "New Group Category")
group = @course.groups.create!(group_category: category)
assignment = @course.assignments.create!(group_category: category, due_at: 20.minutes.ago(@now))
assignment.submit_homework(@student, body: "a body")
section = @course.course_sections.create!(name: "My Awesome Section")
@course.enroll_student(@student, section:, allow_multiple_enrollments: true).accept!
assignment.assignment_overrides.create!(
due_at: 6.minutes.ago(@now),
due_at_overridden: true,
set: section
)
group.add_user(@student, "active")
assignment.assignment_overrides.create!(
due_at: 21.minutes.ago(@now),
due_at_overridden: true,
set: group
)
student_override = assignment.assignment_overrides.create!(
due_at: 14.minutes.ago(@now),
due_at_overridden: true
)
override_student = student_override.assignment_override_students.create!(user: @student)
submission = assignment.submissions.find_by!(user: @student)
expect { override_student.destroy }.to change {
submission.reload.cached_due_date
}.from(14.minutes.ago(@now)).to(6.minutes.ago(@now))
expect { @student.enrollments.find_by(course_section: section).destroy }.to change {
submission.reload.cached_due_date
}.from(6.minutes.ago(@now)).to(21.minutes.ago(@now))
end
it "uses override due dates instead of assignment due dates, even if the assignment due date is more lenient" do
student_override = @assignment.assignment_overrides.create!(
due_at: 21.minutes.ago(@now),
due_at_overridden: true
)
student_override.assignment_override_students.create!(user: @student)
expect(submission.cached_due_date).to eq(21.minutes.ago(@now))
end
it "falls back to use the assignment due date if all overrides are destroyed" do
student_override = @assignment.assignment_overrides.create!(
due_at: 21.minutes.ago(@now),
due_at_overridden: true
)
override_student = student_override.assignment_override_students.create!(user: @student)
expect { override_student.destroy }.to change {
submission.reload.cached_due_date
}.from(21.minutes.ago(@now)).to(20.minutes.ago(@now))
end
end
end
describe "#excused" do
let(:submission) do
submission = @assignment.submissions.find_by!(user: @student)
submission.update!(excused: true)
submission
end
let(:custom_grade_status) do
admin = account_admin_user(account: @assignment.root_account)
@assignment.root_account.custom_grade_statuses.create!(
color: "#ABC",
name: "yolo",
created_by: admin
)
end
it "sets excused to false if the late_policy_status is being changed to a not-nil value" do
submission.update!(late_policy_status: "missing")
expect(submission).not_to be_excused
end
it "does not set excused to false if the late_policy_status is being changed to a nil value" do
# need to skip callbacks so excused does not get set to false
submission.update_column(:late_policy_status, "missing")
submission.update!(late_policy_status: nil)
expect(submission).to be_excused
end
it "sets excused to false when a custom status is set" do
expect { submission.update!(custom_grade_status:) }.to change {
submission.excused?
}.from(true).to(false)
end
it "does not set excused to false when a custom status is removed" do
# need to skip callbacks so excused does not get set to false
submission.update_column(:custom_grade_status_id, custom_grade_status.id)
expect { submission.update!(custom_grade_status: nil) }.not_to change {
submission.excused?
}.from(true)
end
end
describe "#late_policy_status" do
let(:submission) do
submission = @assignment.submissions.find_by!(user: @student)
submission.update!(late_policy_status: "late", seconds_late_override: 60)
submission
end
it "sets late_policy_status to nil if the submission is updated to be excused" do
submission.update!(excused: true)
expect(submission.late_policy_status).to be_nil
end
it "sets seconds_late_override to nil if the submission is updated to be excused" do
submission.update!(excused: true)
expect(submission.seconds_late_override).to be_nil
end
it "does not set late_policy_status to nil if the submission is updated to not be excused" do
# need to skip callbacks so late_policy_status does not get set to nil
submission.update_column(:excused, true)
submission.update!(excused: false)
expect(submission.late_policy_status).to eql "late"
end
it "does not set seconds_late_override to nil if the submission is updated to not be excused" do
# need to skip callbacks so seconds_late_override does not get set to nil
submission.update_column(:excused, true)
submission.update!(excused: false)
expect(submission.seconds_late_override).to be 60
end
context "custom statuses" do
let(:custom_grade_status) do
admin = account_admin_user(account: @assignment.root_account)
@assignment.root_account.custom_grade_statuses.create!(
color: "#ABC",
name: "yolo",
created_by: admin
)
end
it "sets late_policy_status to nil if the custom_grade_status_id is being changed to a not-nil value" do
submission.update!(custom_grade_status:)
expect(submission.late_policy_status).to be_nil
end
it "sets seconds_late_override to nil if the submission is updated to have a custom status" do
submission.update!(custom_grade_status:)
expect(submission.seconds_late_override).to be_nil
end
it "does not set late_policy_status to nil if the custom_grade_status_id is being changed to a nil value" do
# need to skip callbacks so excused does not get set to false
submission.update_column(:custom_grade_status_id, custom_grade_status.id)
submission.update!(custom_grade_status_id: nil)
expect(submission.late_policy_status).to eql "late"
end
it "does not set seconds_late_override to nil if the submission is updated to not have a custom status" do
# need to skip callbacks so seconds_late_override does not get set to nil
submission.update_column(:custom_grade_status_id, custom_grade_status.id)
submission.update!(custom_grade_status_id: nil)
expect(submission.seconds_late_override).to be 60
end
end
end
describe "seconds_late_override" do
let(:submission) { @assignment.submissions.find_by!(user: @student) }
it "sets seconds_late_override to nil if the late_policy_status is set to anything other than 'late'" do
submission.update!(late_policy_status: "late", seconds_late_override: 60)
expect do
submission.update!(late_policy_status: "missing")
end.to change { submission.seconds_late_override }.from(60).to(nil)
end
it "does not set seconds_late_override if late_policy status is not 'late'" do
submission.update!(seconds_late_override: 60)
expect(submission.seconds_late_override).to be_nil
end
end
describe "seconds_late" do
before(:once) do
@date = Time.zone.local(2017, 1, 15, 12)
Timecop.travel(@date) do
Auditors::ActiveRecord::Partitioner.process
end
@assignment.update!(due_at: 1.hour.ago(@date), submission_types: "online_text_entry")
end
let(:submission) { @assignment.submissions.find_by!(user_id: @student) }
it "returns time between submitted_at and cached_due_date" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
expect(submission.seconds_late).to eql 60.minutes.to_i
end
end
it "is adjusted if the student resubmits" do
Timecop.freeze(@date) { @assignment.submit_homework(@student, body: "a body") }
Timecop.freeze(30.minutes.from_now(@date)) do
@assignment.submit_homework(@student, body: "a body")
expect(submission.seconds_late).to eql 90.minutes.to_i
end
end
it "returns seconds_late_override if the submission has a late_policy_status of 'late' " \
"and a seconds_late_override" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
submission.update!(late_policy_status: "late", seconds_late_override: 90.minutes)
expect(submission.seconds_late).to eql 90.minutes.to_i
end
end
it "is not adjusted if the student resubmits and the submission has a late_policy_status of 'late' " \
"and a seconds_late_override" do
Timecop.freeze(@date) { @assignment.submit_homework(@student, body: "a body") }
submission.update!(late_policy_status: "late", seconds_late_override: 90.minutes)
Timecop.freeze(40.minutes.from_now(@date)) do
@assignment.submit_homework(@student, body: "a body")
expect(submission.seconds_late).to eql 90.minutes.to_i
end
end
it "returns 0 if the submission has a late_policy_status of 'late' but no seconds_late_override is present" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
submission.update!(late_policy_status: "late")
expect(submission.seconds_late).to be 0
end
end
it "is zero if it is not late" do
Timecop.freeze(2.hours.ago(@date)) do
@assignment.submit_homework(@student, body: "a body")
expect(submission.seconds_late).to be 0
end
end
it "is zero if it was turned in late but the teacher sets the late_policy_status to 'late' " \
"and sets seconds_late_override to zero" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
submission.update!(late_policy_status: "late", seconds_late_override: 0)
expect(submission.seconds_late).to be 0
end
end
it "is zero if cached_due_date is nil" do
Timecop.freeze(@date) do
@assignment.update!(due_at: nil)
@assignment.submit_homework(@student, body: "a body")
expect(submission.seconds_late).to be 0
end
end
it "subtracts 60 seconds from the time of submission when submission_type is 'online_quiz'" do
Timecop.freeze(@date) do
@assignment.update!(submission_types: "online_quiz")
@assignment.submit_homework(@student, submission_type: "online_quiz", body: "a body")
expect(submission.seconds_late).to eql 59.minutes.to_i
end
end
context "when the submission is for a new quiz" do
before do
@course.context_external_tools.create!(
name: "Quizzes.Next",
consumer_key: "test_key",
shared_secret: "test_secret",
tool_id: "Quizzes 2",
url: "http://example.com/launch"
)
@assignment.quiz_lti!
@assignment.save!
end
it "subtracts 60 seconds from the submitted_at" do
Timecop.freeze(@date) do
submission = @assignment.submissions.find_by!(user: @student)
submission.update!(submitted_at: Time.now.utc)
expect(submission.seconds_late).to eql 59.minutes.to_i
end
end
end
it "includes seconds" do
Timecop.freeze(30.seconds.from_now(@date)) do
@assignment.submit_homework(@student, body: "a body")
expect(submission.seconds_late).to eql((60.minutes + 30.seconds).to_i)
end
end
it "uses the current time if submitted_at is nil" do
Timecop.freeze(1.day.from_now(@date)) do
@assignment.grade_student(@student, score: 10, grader: @teacher)
expect(submission.seconds_late).to eql 25.hours.to_i
end
end
end
describe "#apply_late_policy" do
before(:once) do
@date = Time.zone.local(2017, 1, 15, 12)
Timecop.travel(@date) do
Auditors::ActiveRecord::Partitioner.process
end
@assignment.update!(due_at: 3.hours.ago(@date), points_possible: 1000, submission_types: "online_text_entry")
@late_policy = late_policy_model(deduct: 10.0, every: :hour, missing: 80.0)
end
let(:submission) { @assignment.submissions.find_by(user_id: @student) }
context "as a before_save" do
before(:once) do
@late_policy.update!(course_id: @course)
end
it "deducts a percentage per interval late if grade_matches_current_submission is true" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
submission.score = 700
submission.grade_matches_current_submission = true
submission.save!
expect(submission.points_deducted).to eq 300.0
end
end
it "deducts a percentage per interval late if grade_matches_current_submission is nil" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
submission.score = 700
submission.grade_matches_current_submission = nil
submission.save!
expect(submission.points_deducted).to eq 300.0
end
end
it "deducts nothing if grade_matches_current_submission is false" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
submission.score = 700
submission.grade_matches_current_submission = false
submission.save!
expect(submission.points_deducted).to be_nil
end
end
it "sets points_deducted to nil if a submission's status is changed to missing" do
submission.update!(score: 5, points_deducted: 2)
expect { submission.update!(late_policy_status: "missing") }.to change {
submission.points_deducted
}.from(2).to(nil)
end
it "sets score to raw_score if a submission has points_deducted and the status is changed to missing" do
submission.update!(score: 5, points_deducted: 2)
expect { submission.update!(late_policy_status: "missing") }.to change {
submission.score
}.from(5).to(7)
end
it "keeps the given score if a submission is set to missing and given a score" do
submission.update!(score: 5, points_deducted: 2)
expect { submission.update!(score: 3, late_policy_status: "missing") }.to change {
submission.score
}.from(5).to(3)
end
it "deducts nothing if the submission is for a checkpointed discussion" do
@course.root_account.enable_feature!(:discussion_checkpoints)
cd = DiscussionTopic.create_graded_topic!(course: @course, title: "checkpointed topic")
Checkpoints::DiscussionCheckpointCreatorService.call(
discussion_topic: cd,
checkpoint_label: CheckpointLabels::REPLY_TO_TOPIC,
dates: [{ type: "everyone", due_at: 3.hours.ago(@date) }],
points_possible: 5
)
Checkpoints::DiscussionCheckpointCreatorService.call(
discussion_topic: cd,
checkpoint_label: CheckpointLabels::REPLY_TO_ENTRY,
dates: [{ type: "everyone", due_at: 3.hours.ago(@date) }],
points_possible: 5,
replies_required: 1
)
cd_submission = cd.assignment.submissions.find_by!(user: @student)
Timecop.freeze(@date) do
cd.discussion_entries.create!(user: @student, message: "reply to topic")
cd.discussion_entries.create!(user: @student, message: "reply to entry", parent_id: cd.discussion_entries.first.id)
cd_submission.score = 10
cd_submission.save!
expect(cd_submission.points_deducted).to be_nil
end
end
end
it "deducts nothing if grading period is closed" do
grading_period = double("grading_period", closed?: true)
expect(submission).to receive(:grading_period).and_return(grading_period)
@assignment.submit_homework(@student, body: "a body")
submission.score = 700
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.score).to eq 700
expect(submission.points_deducted).to be_nil
end
it "deducts a percentage per interval late" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
submission.score = 700
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.score).to eq 400
expect(submission.points_deducted).to eq 300
end
end
it "deducts nothing if there is no late policy" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
submission.score = 700
submission.apply_late_policy(nil, @assignment)
expect(submission.score).to eq 700
expect(submission.points_deducted).to eq 0
end
end
it "deducts nothing if the submission is not late" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "gary, what have you done?")
submission.score = 700
submission.late_policy_status = "missing"
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.score).to eq 700
expect(submission.points_deducted).to be_nil
end
end
it "does not round decimal places in the score" do
Timecop.freeze(2.days.ago(@date)) do
@assignment.submit_homework(@student, body: "a body")
original_score = 1.3800000000000001
submission.score = original_score
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.score).to eq original_score
end
end
it "deducts only once even if called twice" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
submission.score = 800
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.score).to eq 500
expect(submission.points_deducted).to eq 300
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.score).to eq 500
expect(submission.points_deducted).to eq 300
end
end
it "sets the points_deducted to 0.0 if the score is set to nil and the submission is late" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
submission.update!(score: 400, points_deducted: 300)
submission.score = nil
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.points_deducted).to eq 0.0
end
end
it "sets the points_deducted to nil if the score is set to nil and the submission is not late" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
submission.update!(score: 400, points_deducted: 300)
submission.score = nil
submission.late_policy_status = "none"
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.points_deducted).to be_nil
end
end
it "applies missing policy if submission is missing" do
Timecop.freeze(1.day.from_now(@date)) do
submission.score = nil
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.score).to eq 200
end
end
context "past due date" do
it "removes missing status when missing submission is graded" do
Timecop.freeze(1.day.from_now(@date)) do
expect(submission.missing?).to be true
@assignment.grade_student(@student, score: 500, grader: @teacher)
submission.reload
expect(submission.missing?).to be false
end
end
it "does not remove missing status when missing status was given manually" do
Timecop.freeze(1.day.from_now(@date)) do
expect(submission.missing?).to be true
submission.update!(late_policy_status: "missing")
expect(submission.missing?).to be true
@assignment.grade_student(@student, score: 500, grader: @teacher)
submission.reload
expect(submission.missing?).to be true
end
end
end
it "does not remove missing status when missing status was given manually" do
@assignment.update!(due_at: 3.hours.from_now(@date), points_possible: 1000, submission_types: "online_text_entry")
submission.reload
Timecop.freeze(@date) do
expect(submission.missing?).to be false
submission.update!(late_policy_status: "missing")
expect(submission.missing?).to be true
@assignment.grade_student(@student, score: 500, grader: @teacher)
submission.reload
expect(submission.missing?).to be true
end
end
it "sets grade_matches_current_submission to true when missing policy is applied" do
Timecop.freeze(1.day.from_now(@date)) do
submission.score = nil
submission.grade_matches_current_submission = false
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.grade_matches_current_submission).to be true
end
end
it "sets the workflow state to 'graded' when submission is missing" do
Timecop.freeze(1.day.from_now(@date)) do
submission.score = nil
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.workflow_state).to eq "graded"
end
end
describe "posting of missing submissions" do
before(:once) do
late_policy_factory(course: @course, missing: 50)
end
context "when the submission was not previously posted" do
context "for an automatically-posted assignment" do
it "posts a previously-unscored submission if deducting points for missing submissions" do
submission.update!(late_policy_status: :missing)
expect(submission.posted_at).not_to be_nil
end
it "does not post the submission if missing submission deduction is not enabled" do
@course.late_policy.update!(missing_submission_deduction_enabled: false)
expect do
submission.update!(late_policy_status: :missing)
end.not_to change { submission.reload.posted_at }
end
it "does not update the posted-at date of an already-posted submission" do
@assignment.post_submissions
expect do
submission.update!(late_policy_status: :missing)
end.not_to change { submission.reload.posted_at }
end
end
it "does not post submissions if the assignment is manually posted" do
@assignment.post_policy.update!(post_manually: true)
expect do
submission.update!(late_policy_status: :missing)
end.not_to change { submission.reload.posted_at }
end
end
end
it "does not change the score of a missing submission if it already has one" do
Timecop.freeze(1.day.from_now(@date)) do
@assignment.grade_student(@student, grade: 1000, grader: @teacher)
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.score).to be 1000.0
end
end
context "with regraded" do
it "does not apply the deduction multiple times if submission saved multiple times" do
@late_policy.update!(course_id: @course)
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
# The submission is saved once in grade_student. Using sub
# here to avoid masking/using the submission in the let
# above. I want to make sure I'm using the exact same object
# as returned by grade_student.
sub = @assignment.grade_student(@student, grade: 1000, grader: @teacher).first
sub.save!
expect(sub.score).to be 700.0
end
end
end
context "assignment on paper" do
before(:once) do
@date = Time.zone.local(2017, 1, 15, 12)
Timecop.travel(@date) do
Auditors::ActiveRecord::Partitioner.process
end
@assignment.update!(due_at: 3.hours.ago(@date), points_possible: 1000, submission_types: "on_paper")
@late_policy = late_policy_factory(course: @course, deduct: 10.0, every: :hour, missing: 80.0)
end
it "does not deduct from late assignment" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
@assignment.grade_student(@student, grade: 700, grader: @teacher)
expect(submission.score).to eq 700
expect(submission.points_deducted).to be_nil
end
end
it "does not grade missing assignment" do
Timecop.freeze(@date) do
submission.apply_late_policy
expect(submission.score).to be_nil
expect(submission.points_deducted).to be_nil
end
end
it "deducts a percentage per interval late if manually marked late" do
@assignment.submit_homework(@student, body: "a body")
submission.late_policy_status = "late"
submission.seconds_late_override = 4.hours
submission.score = 700
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.score).to be 300.0
expect(submission.points_deducted).to eq 400
end
context "when change late_policy_status from late to none" do
before do
@assignment.course.update!(late_policy: @late_policy)
@assignment.submit_homework(@student, body: "a body")
submission.update!(
score: 700,
late_policy_status: "late",
seconds_late_override: 4.hours
)
end
it "removes late penalty from score" do
expect { submission.update!(late_policy_status: "none") }
.to change { submission.score }.from(300).to(700)
end
it "sets points_deducted to nil" do
expect { submission.update!(late_policy_status: "none") }
.to change { submission.points_deducted }.from(400).to(nil)
end
end
context "when change late_policy_status from late to extended" do
before do
@assignment.course.update!(late_policy: @late_policy)
@assignment.submit_homework(@student, body: "a body")
submission.update!(
score: 700,
late_policy_status: "late",
seconds_late_override: 4.hours
)
end
it "removes late penalty from score" do
expect { submission.update!(late_policy_status: "extended") }
.to change { submission.score }.from(300).to(700)
end
it "sets points_deducted to nil" do
expect { submission.update!(late_policy_status: "extended") }
.to change { submission.points_deducted }.from(400).to(nil)
end
end
context "when changing late_policy_status from none to nil" do
before do
@assignment.update!(due_at: 1.hour.from_now)
@assignment.course.update!(late_policy: @late_policy)
@assignment.submit_homework(@student, body: "a body")
submission.update!(score: 700, late_policy_status: "late", seconds_late_override: 4.hours)
end
it "applies the late policy to the score" do
expect { submission.update!(late_policy_status: "none") }
.to change { submission.score }.from(300).to(700)
end
it "applies the late policy to points_deducted" do
expect { submission.update!(late_policy_status: "none") }
.to change { submission.points_deducted }.from(400).to(nil)
end
end
it "applies missing policy if submission is manually marked missing" do
Timecop.freeze(1.day.from_now(@date)) do
submission.score = nil
submission.late_policy_status = "missing"
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.score).to eq 200
end
end
end
context "assignment expecting no submission" do
before(:once) do
@date = Time.zone.local(2017, 1, 15, 12)
Timecop.travel(@date) do
Auditors::ActiveRecord::Partitioner.process
end
@assignment.update!(due_at: 3.hours.ago(@date), points_possible: 1000, submission_types: "none")
@late_policy = late_policy_factory(course: @course, deduct: 10.0, every: :hour, missing: 80.0)
end
it "does not deduct from late assignment" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
@assignment.grade_student(@student, grade: 700, grader: @teacher)
expect(submission.score).to eq 700
expect(submission.points_deducted).to be_nil
end
end
it "does not grade missing assignment" do
Timecop.freeze(@date) do
submission.apply_late_policy
expect(submission.score).to be_nil
expect(submission.points_deducted).to be_nil
end
end
it "deducts a percentage per interval late if manually marked late" do
@assignment.submit_homework(@student, body: "a body")
submission.late_policy_status = "late"
submission.seconds_late_override = 4.hours
submission.score = 700
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.score).to eq 300
expect(submission.points_deducted).to eq 400
end
it "applies missing policy if submission is manually marked missing" do
Timecop.freeze(1.day.from_now(@date)) do
submission.score = nil
submission.late_policy_status = "missing"
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.score).to eq 200
end
end
end
context "when applied late policy deducts 100%" do
before(:once) do
@date = Time.zone.local(2017, 1, 15, 12)
Timecop.travel(@date) do
Auditors::ActiveRecord::Partitioner.process
end
@assignment.update!(due_at: @date - 12.days, points_possible: 1, submission_types: "online_text_entry")
@late_policy = late_policy_factory(course: @course, deduct: 10.0, every: :day)
end
it "sets the score to 0 when grade has three decimal points and ending in 5" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
@assignment.grade_student(@student, grade: 0.555, grader: @teacher)
expect(submission.score).to eq 0.0
end
end
end
context "when submitting to an LTI assignment" do
before(:once) do
@date = Time.zone.local(2017, 1, 15, 12)
Timecop.travel(@date) do
Auditors::ActiveRecord::Partitioner.process
end
@assignment.update!(due_at: @date - 3.hours, points_possible: 1_000, submission_types: "external_tool")
@late_policy = late_policy_factory(course: @course, deduct: 10.0, every: :hour, missing: 80.0)
end
it "deducts a percentage per interval late if submitted late" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
@assignment.grade_student(@student, grade: 700, grader: @teacher)
expect(submission.points_deducted).to eq 300
end
end
it "applies the deduction to the awarded score if submitted late" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
@assignment.grade_student(@student, grade: 700, grader: @teacher)
expect(submission.score).to eq 400
end
end
it "does not grade missing submissions" do
Timecop.freeze(@date) do
submission.apply_late_policy
expect(submission.score).to be_nil
end
end
it "deducts a percentage per interval late if the submission is manually marked late" do
@assignment.submit_homework(@student, body: "a body")
submission.late_policy_status = "late"
submission.seconds_late_override = 4.hours
submission.score = 700
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.points_deducted).to eq 400
end
it "applies the deduction to the awarded score if the submission is manually marked late" do
@assignment.submit_homework(@student, body: "a body")
submission.late_policy_status = "late"
submission.seconds_late_override = 4.hours
submission.score = 700
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.score).to eq 300
end
it "applies the missing policy if the submission is manually marked missing" do
Timecop.freeze(@date + 1.day) do
submission.score = nil
submission.late_policy_status = "missing"
submission.apply_late_policy(@late_policy, @assignment)
expect(submission.score).to eq 200
end
end
end
context "when submitting to a New Quiz LTI assignment" do
before(:once) do
@date = Time.zone.local(2017, 1, 15, 12)
Timecop.travel(@date) do
Auditors::ActiveRecord::Partitioner.process
end
@course.context_external_tools.create!(
name: "Quizzes.Next",
consumer_key: "test_key",
shared_secret: "test_secret",
tool_id: "Quizzes 2",
url: "http://example.com/launch"
)
@assignment.quiz_lti!
@assignment.save!
@late_policy = late_policy_factory(course: @course, deduct: 10.0, every: :hour, missing: 80.0)
end
it "does grade missing new quiz submissions" do
Timecop.freeze(@date) do
submission.apply_late_policy
expect(submission.score).to eq 200
end
end
end
end
describe "#apply_late_policy_before_save" do
before(:once) do
@date = Time.zone.local(2017, 3, 25, 11)
Timecop.travel(@date) do
Auditors::ActiveRecord::Partitioner.process
end
@assignment.update!(due_at: 4.days.ago(@date), points_possible: 1000, submission_types: "online_text_entry")
@late_policy = late_policy_factory(course: @course, deduct: 5.0, every: :day, missing: 80.0)
end
let(:submission) { @assignment.submissions.find_by(user_id: @student) }
it "applies the missing policy to the score when changing from excused to missing" do
@assignment.grade_student(@student, grader: @teacher, excused: true)
expect { submission.update!(late_policy_status: "missing") }.to change {
submission.score
}.from(nil).to(200)
end
it "applies the missing policy to the grade when changing from excused to missing" do
@assignment.grade_student(@student, grader: @teacher, excused: true)
expect { submission.update!(late_policy_status: "missing") }.to change {
submission.grade
}.from(nil).to("200")
end
it "applies the late policy when score changes" do
Timecop.freeze(2.days.ago(@date)) do
@assignment.submit_homework(@student, body: "a body")
@assignment.grade_student(@student, grade: 600, grader: @teacher)
expect(submission.score).to eq 500
expect(submission.points_deducted).to eq 100
end
end
it "applies the late policy when entered grade is equal to previous penalized grade" do
Timecop.freeze(2.days.ago(@date)) do
@assignment.submit_homework(@student, body: "a body")
@assignment.grade_student(@student, grade: 600, grader: @teacher)
@assignment.grade_student(@student, grade: 500, grader: @teacher)
expect(submission.score).to eq 400
end
end
context "custom grade statuses" do
let(:custom_grade_status) do
admin = account_admin_user(account: @assignment.root_account)
@assignment.root_account.custom_grade_statuses.create!(
name: "Custom Status",
color: "#ABC",
created_by: admin
)
end
it "does not apply the late policy when score changes if a custom status is applied" do
Timecop.freeze(2.days.ago(@date)) do
@assignment.submit_homework(@student, body: "a body")
submission.update!(custom_grade_status:)
expect { @assignment.grade_student(@student, grade: 600, grader: @teacher) }.not_to change {
submission.reload.points_deducted
}.from(nil)
end
end
it "applies the late policy for a late submission when a custom status is removed" do
Timecop.freeze(2.days.ago(@date)) do
submission.update!(custom_grade_status:)
@assignment.submit_homework(@student, body: "a body")
@assignment.grade_student(@student, grade: 600, grader: @teacher)
expect { submission.update!(custom_grade_status: nil) }.to change {
submission.reload.points_deducted
}.from(nil).to(100)
end
end
it "does not apply the missing policy when a custom status is applied" do
submission.update_columns(score: nil, grade: nil, posted_at: nil, workflow_state: "unsubmitted")
expect { submission.update!(custom_grade_status:) }.not_to change {
submission.reload.score
}.from(nil)
end
end
it "does not apply the late policy when what-if score changes" do
Timecop.freeze(2.days.ago(@date)) do
@assignment.submit_homework(@student, body: "a body")
@assignment.grade_student(@student, grade: 600, grader: @teacher)
end
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
submission.update!(student_entered_score: 900)
expect(submission.score).to eq 500
expect(submission.points_deducted).to eq 100
end
end
it "does not apply the late policy more than once when working with decimals with a scale of more than 2" do
Timecop.freeze(3.days.ago(@date)) do
@course.late_policy.update!(late_submission_deduction: 2.35)
@assignment.submit_homework(@student, body: "a body")
@assignment.update!(points_possible: 10)
@assignment.grade_student(@student, grade: 10, grader: @teacher)
SubmissionLifecycleManager.recompute(@assignment, update_grades: true)
expect(submission.score).to be 9.76
end
end
it "does not change a previous grade when student submits ungraded work" do
asg = @course.assignments.create!(points_possible: 1000, submission_types: "online_text_entry")
Timecop.freeze(2.days.ago(@date)) do
asg.update!(due_at: 4.days.ago(@date))
ph = asg.submissions.last
expect(ph.missing?).to be true
expect(ph.score).to eq 200
expect(ph.points_deducted).to be_nil
end
Timecop.freeze(@date) do
hw = asg.submit_homework(@student, body: "a body", submission_type: "online_text_entry")
hw.save!
expect(hw.late?).to be true
expect(hw.score).to eq 200
expect(hw.points_deducted).to be_nil
end
end
it "re-applies the late policy when seconds_late_override changes" do
Timecop.freeze(@date) do
@assignment.submit_homework(@student, body: "a body")
@assignment.grade_student(@student, grade: 800, grader: @teacher)
end
submission.update!(seconds_late_override: 3.days, late_policy_status: "late")
expect(submission.score).to eq 650
expect(submission.points_deducted).to eq 150
end
end
include_examples "url validation tests"
it "checks url validity" do
test_url_validation(submission_spec_model)
end
it "adds http:// to the body for long urls, too" do
s = submission_spec_model(submit_homework: true)
expect(s.url).to eq "http://www.instructure.com"
long_url = (("a" * 300) + ".com")
s.url = long_url
s.save!
expect(s.url).to eq "http://#{long_url}"
# make sure it adds the "http://" to the body for long urls, too
expect(s.body).to eq "http://#{long_url}"
end
it "offers the context, if one is available" do
@course = Course.new
@assignment = Assignment.new(context: @course)
expect(@assignment).to receive(:context).and_return(@course)
@submission = Submission.new
expect { @submission.context }.not_to raise_error
expect(@submission.context).to be_nil
@submission.assignment = @assignment
expect(@submission.context).to eql(@course)
end
it "has an interesting state machine" do
submission_spec_model(submit_homework: true)
expect(@submission.state).to be(:submitted)
@submission.grade_it
expect(@submission.state).to be(:graded)
end
it "is versioned" do
submission_spec_model
expect(@submission).to respond_to(:versions)
end
it "does not save new versions by default" do
submission_spec_model
expect do
@submission.save!
end.not_to change(@submission.versions, :count)
end
it "does not create a new version if only the posted_at field is updated" do
submission_spec_model
expect do
@submission.update!(posted_at: Time.zone.now)
end.not_to change {
@submission.reload.versions.count
}
end
it "does not update the most recent version if only the posted_at field is updated" do
submission_spec_model
expect do
@submission.update!(posted_at: Time.zone.now)
end.not_to change {
@submission.reload.versions.first.model.posted_at
}
end
describe "version indexing" do
it "creates a SubmissionVersion when a new submission is created" do
expect do
submission_spec_model
end.to change(SubmissionVersion, :count)
end
it "creates a SubmissionVersion when a new version is saved" do
submission_spec_model
expect do
@submission.with_versioning(explicit: true) { @submission.save }
end.to change(SubmissionVersion, :count)
end
it "does not fail preload if versionable is nil" do
submission_spec_model
version = Version.find_by(versionable: @submission)
version.update_attribute(:versionable_id, Submission.last.id + 1)
expect do
ActiveRecord::Associations.preload([version].map(&:model), :originality_reports)
end.not_to raise_error
end
it "retries if there is a conflict with an existing version number" do
submission_spec_model
version = Version.find_by(versionable: @submission)
allow(@submission.versions).to receive(:create).and_call_original
called_times = 0
expect(@submission.versions).to receive(:create) { |attributes|
called_times += 1
# This is a hacky way of mimicing a bug where two responses, received very quickly,
# would create a RecordNotUnique error when the tried to increment the version
# number at the same time.
v = Version.create(
**attributes,
versionable_id: version.versionable_id,
versionable_type: version.versionable_type
)
v.update_attribute(:number, version.number) if called_times == 1
v
}.twice
submission_versions = @submission.versions.count
@submission.with_versioning(explicit: true) do
@submission.broadcast_group_submission
end
expect(@submission.versions.count).to eq(submission_versions + 1)
end
end
it "ensures the media object exists" do
assignment_model
se = @course.enroll_student(user_factory)
expect(MediaObject).to receive(:ensure_media_object).with("fake", { context: se.user, user: se.user })
@submission = @assignment.submit_homework(se.user, media_comment_id: "fake", media_comment_type: "audio")
end
describe "#grade_change_audit" do
before(:once) { Auditors::ActiveRecord::Partitioner.process }
let_once(:submission) { @assignment.submissions.find_by(user: @student) }
it "logs submissions with grade changes" do
expect(Auditors::GradeChange).to receive(:record).once
submission.update!(grader: @teacher, score: 5)
end
it "grade change event author can be set" do
assistant = User.create!
@course.enroll_ta(assistant, enrollment_state: "active")
expect(Auditors::GradeChange).to receive(:record).once do |args|
expect(args[:submission].grader_id).to eq assistant.id
end
submission.grade_change_event_author_id = assistant.id
submission.update!(score: 5)
end
it "uses the existing grader_id as the author if grade_change_event_author_id is not set" do
@assignment.grade_student(@student, grade: 10, grader: @teacher)
expect(Auditors::GradeChange).to receive(:record).once do |args|
expect(args[:submission].grader_id).to eq @teacher.id
end
submission.reload.update!(score: 5)
end
it "logs excused submissions" do
expect(Auditors::GradeChange).to receive(:record).once
submission.update!(excused: true, grader: @user)
end
it "logs just one submission affected by assignment update" do
expect(Auditors::GradeChange).to receive(:record).twice
# only graded submissions are updated by assignment
submission.update!(score: 111, workflow_state: "graded")
@assignment.update!(points_possible: 999)
end
it "does not log ungraded submission change when assignment muted" do
expect(Auditors::GradeChange).not_to receive(:record)
@assignment.mute!
@assignment.unmute!
end
it "inserts a grade change audit record by default" do
expect(Auditors::GradeChange).to receive(:record).once
submission.grade_change_audit(force_audit: true)
end
it "does not insert a grade change audit record if grade not changed" do
expect(Auditors::GradeChange::Stream).not_to receive(:insert)
submission.grade_change_audit(force_audit: true)
end
it "inserts a grade change audit record if grade changed" do
expect(Auditors::GradeChange::Stream).to receive(:insert)
submission.score = 11
submission.save!
end
it "emits a grade change live event when force_audit" do
expect(Canvas::LiveEvents).to receive(:grade_changed).once
submission.grade_change_audit(force_audit: true)
end
it "moves mastery path along on force audit if appropriate" do
expect(ConditionalRelease::Rule).to receive(:is_trigger_assignment?).with(submission.assignment).once
submission.update! score: 1, workflow_state: :graded, posted_at: Time.now
submission.grade_change_audit(force_audit: true)
end
end
context "#graded_anonymously" do
it "saves when grade changed and set explicitly" do
submission_spec_model
expect(@submission.graded_anonymously).to be_falsey
@submission.score = 42
@submission.graded_anonymously = true
@submission.save!
expect(@submission.graded_anonymously).to be_truthy
@submission.reload
expect(@submission.graded_anonymously).to be_truthy
end
it "retains its value when grade does not change" do
submission_spec_model(graded_anonymously: true, score: 3, grade: "3")
@submission = Submission.find(@submission.id) # need new model object
expect(@submission.graded_anonymously).to be_truthy
@submission.body = "test body"
@submission.save!
@submission.reload
expect(@submission.graded_anonymously).to be_truthy
end
it "resets when grade changed and not set explicitly" do
submission_spec_model(graded_anonymously: true, score: 3, grade: "3")
@submission = Submission.find(@submission.id) # need new model object
expect(@submission.graded_anonymously).to be_truthy
@submission.score = 42
@submission.save!
@submission.reload
expect(@submission.graded_anonymously).to be_falsey
end
end
context "Discussion Topic" do
it "submitted_at does not change when a second discussion entry is created" do
course_with_student(active_all: true)
@topic = @course.discussion_topics.create(title: "some topic")
@assignment = @course.assignments.create(title: "some discussion assignment")
@assignment.submission_types = "discussion_topic"
@assignment.save!
@entry1 = @topic.discussion_entries.create(message: "first entry", user: @user)
@topic.assignment_id = @assignment.id
@topic.save!
Timecop.freeze(30.minutes.from_now) do
expect do
@topic.discussion_entries.create(message: "second entry", user: @user)
end.not_to(change { @assignment.submissions.find_by(user: @user).submitted_at })
end
end
it "does not create multiple versions on submission for discussion topics" do
course_with_student(active_all: true)
@topic = @course.discussion_topics.create(title: "some topic")
@assignment = @course.assignments.create(title: "some discussion assignment")
@assignment.submission_types = "discussion_topic"
@assignment.save!
@topic.assignment_id = @assignment.id
@topic.save!
Timecop.freeze(1.second.ago) do
@assignment.submit_homework(@student, submission_type: "discussion_topic")
end
@assignment.submit_homework(@student, submission_type: "discussion_topic")
expect(@student.submissions.first.submission_history.count).to eq 1
end
end
context "broadcast policy" do
context "Submission Notifications" do
before :once do
Notification.create(name: "Assignment Submitted", category: "TestImmediately")
Notification.create(name: "Assignment Resubmitted")
Notification.create(name: "Assignment Submitted Late")
Notification.create(name: "Group Assignment Submitted Late")
course_with_teacher(course: @course, active_all: true)
end
it "sends the correct message when an assignment is turned in on-time" do
@assignment.workflow_state = "published"
@assignment.update(due_at: Time.now + 1000)
submission_spec_model(user: @student, submit_homework: true)
expect(@submission.messages_sent.keys).to eq ["Assignment Submitted"]
end
it "does not send a message to a TA without grading rights" do
limited_role = custom_ta_role("limitedta", account: @course.account)
[:view_all_grades, :manage_grades].each do |permission|
@course.account.role_overrides.create!(permission:, enabled: false, role: limited_role)
end
limited_ta = user_factory(active_all: true, active_cc: true)
@course.enroll_user(limited_ta, "TaEnrollment", role: limited_role, enrollment_state: "active")
normal_ta = user_factory(active_all: true, active_cc: true)
@course.enroll_user(normal_ta, "TaEnrollment", enrollment_state: "active")
Notification.where(name: "Assignment Submitted").first
@assignment.workflow_state = "published"
@assignment.update(due_at: Time.now + 1000)
submission_spec_model(user: @student, submit_homework: true)
expect(@submission.messages_sent["Assignment Submitted"].map(&:user)).not_to include(limited_ta)
expect(@submission.messages_sent["Assignment Submitted"].map(&:user)).to include(normal_ta)
end
it "sends the correct message when an assignment is turned in late" do
@assignment.workflow_state = "published"
@assignment.update(due_at: Time.now - 1000)
submission_spec_model(user: @student, submit_homework: true)
expect(@submission.messages_sent.keys).to eq ["Assignment Submitted Late"]
end
it "sends the correct message when an assignment is resubmitted on-time" do
@assignment.submission_types = ["online_text_entry"]
@assignment.due_at = Time.now + 1000
@assignment.save!
@assignment.submit_homework(@student, body: "lol")
resubmission = @assignment.submit_homework(@student, body: "frd")
expect(resubmission.messages_sent.keys).to eq ["Assignment Resubmitted"]
end
it "sends the correct message when an assignment is resubmitted late" do
@assignment.submission_types = ["online_text_entry"]
@assignment.due_at = Time.now - 1000
@assignment.save!
@assignment.submit_homework(@student, body: "lol")
resubmission = @assignment.submit_homework(@student, body: "frd")
expect(resubmission.messages_sent.keys).to eq ["Assignment Submitted Late"]
end
it "sends the correct message when a group assignment is submitted late" do
@a = assignment_model(course: @context, group_category: "Study Groups", due_at: Time.now - 1000, submission_types: ["online_text_entry"])
@group1 = @a.context.groups.create!(name: "Study Group 1", group_category: @a.group_category)
@group1.add_user(@student)
submission = @a.submit_homework @student, submission_type: "online_text_entry", body: "blah"
expect(submission.messages_sent.keys).to eq ["Group Assignment Submitted Late"]
end
context "Submission Posted" do
let(:submission) { @assignment.submissions.find_by!(user: @student) }
let(:submission_posted_messages) do
Message.where(
communication_channel: @student.email_channel,
notification: @submission_posted_notification
)
end
before(:once) do
@submission_posted_notification = Notification.find_or_create_by(
category: "Grading",
name: "Submission Posted"
)
@student.update!(email: "fakeemail@example.com")
@student.email_channel.update!(workflow_state: :active)
end
it "does not send a notification when a submission is not being posted" do
expect { submission.update!(body: "hello") }.not_to change { submission_posted_messages.count }
end
context "when grade_posting_in_progress is true" do
before do
submission.grade_posting_in_progress = true
end
it "sends a notification when a submission is posted and assignment posts manually" do
@assignment.ensure_post_policy(post_manually: true)
expect do
submission.update!(posted_at: Time.zone.now)
end.to change {
submission_posted_messages.count
}.by(1)
end
it "sends a notification when a submission is posted and assignment posts automatically" do
expect do
submission.update!(posted_at: Time.zone.now)
end.to change {
submission_posted_messages.count
}.by(1)
end
end
context "when grade_posting_in_progress is false" do
before do
submission.grade_posting_in_progress = false
end
it "does not send a notification when a submission is posted and assignment posts manually" do
@assignment.ensure_post_policy(post_manually: true)
expect do
submission.update!(posted_at: Time.zone.now)
end.not_to change {
submission_posted_messages.count
}
end
it "does not send a notification when a submission is posted and assignment posts automatically" do
expect do
submission.update!(posted_at: Time.zone.now)
end.not_to change {
submission_posted_messages.count
}
end
end
end
end
context "Submission Graded" do
before :once do
Auditors::ActiveRecord::Partitioner.process
@assignment.ensure_post_policy(post_manually: false)
Notification.create(name: "Submission Graded", category: "TestImmediately")
submission_spec_model(submit_homework: true)
end
it "updates 'graded_at' on the submission when the late_policy_status is changed" do
now = Time.zone.now
Timecop.freeze(1.hour.ago(now)) do
@submission.update!(late_policy_status: "late")
end
Timecop.freeze(now) do
@submission.update!(late_policy_status: "missing")
end
expect(@submission.graded_at).to eq now
end
it "creates a message when the assignment has been graded and published" do
communication_channel(@user, { username: "somewhere@test.com" })
@submission.reload
expect(@submission.assignment).to eql(@assignment)
expect(@submission.assignment.state).to be(:published)
@submission = @assignment.grade_student(@student, grader: @teacher, score: 5).first
expect(@submission.messages_sent).to include("Submission Graded")
end
it "does not create a message for a soft-concluded student" do
@course.start_at = 2.weeks.ago
@course.conclude_at = 1.week.ago
@course.restrict_enrollments_to_course_dates = true
@course.save!
communication_channel(@user, { username: "somewhere@test.com" })
@submission.reload
expect(@submission.assignment).to eql(@assignment)
expect(@submission.assignment.state).to be(:published)
@submission = @assignment.grade_student(@student, grader: @teacher, score: 5).first
expect(@submission.messages_sent).to_not include("Submission Graded")
end
it "notifies observers" do
course_with_observer(course: @course, associated_user_id: @user.id, active_all: true, active_cc: true)
@assignment.grade_student(@student, grader: @teacher, score: 5)
expect(@observer.email_channel.messages.length).to eq 1
end
it "does not create a message when a muted assignment has been graded and published" do
communication_channel(@user, { username: "somewhere@test.com" })
@assignment.ensure_post_policy(post_manually: true)
@submission.reload
expect(@submission.assignment).to eql(@assignment)
expect(@submission.assignment.state).to be(:published)
@submission = @assignment.grade_student(@student, grader: @teacher, score: 5).first
expect(@submission.messages_sent).not_to include "Submission Graded"
end
it "does not create a message when this is a quiz submission" do
communication_channel(@user, { username: "somewhere@test.com" })
@quiz = Quizzes::Quiz.create!(context: @course)
@submission.quiz_submission = @quiz.generate_submission(@user)
@submission.save!
@submission.reload
expect(@submission.assignment).to eql(@assignment)
expect(@submission.assignment.state).to be(:published)
@submission = @assignment.grade_student(@student, grader: @teacher, score: 5).first
expect(@submission.messages_sent).not_to include("Submission Graded")
end
it "creates a hidden stream_item_instance when muted, graded, and published" do
communication_channel(@user, { username: "somewhere@test.com" })
@assignment.ensure_post_policy(post_manually: true)
expect do
@assignment.grade_student(@user, grade: 10, grader: @teacher).first
end.to change StreamItemInstance, :count
expect(@user.stream_item_instances.last).to be_hidden
end
it "hides any existing stream_item_instances when grades are hidden" do
communication_channel(@user, { username: "somewhere@test.com" })
expect do
@assignment.grade_student(@student, grader: @teacher, score: 5).first
end.to change StreamItemInstance, :count
expect(@user.stream_item_instances.last).not_to be_hidden
@assignment.hide_submissions
expect(@user.stream_item_instances.last).to be_hidden
end
it "shows hidden stream_item_instances when grades are posted" do
communication_channel(@user, { username: "somewhere@test.com" })
@assignment.ensure_post_policy(post_manually: true)
expect do
@assignment.update_submission(@student, author: @teacher, comment: "some comment")
end.to change StreamItemInstance, :count
expect(@submission.submission_comments.last).to be_hidden
expect(@user.stream_item_instances.last).to be_hidden
@assignment.post_submissions
expect(@submission.submission_comments.last).to_not be_hidden
expect(@submission.reload.submission_comments_count).to eq 1
expect(@user.stream_item_instances.last).to_not be_hidden
end
it "does not create hidden stream_item_instances for instructors when muted, graded, and published" do
communication_channel(@teacher, { username: "somewhere@test.com" })
@assignment.mute!
expect do
@submission.add_comment(author: @student, comment: "some comment")
end.to change StreamItemInstance, :count
expect(@teacher.stream_item_instances.last).to_not be_hidden
end
it "does not hide any existing stream_item_instances for instructors when muted" do
communication_channel(@teacher, { username: "somewhere@test.com" })
expect do
@submission.add_comment(author: @student, comment: "some comment")
end.to change StreamItemInstance, :count
expect(@teacher.stream_item_instances.last).to_not be_hidden
@assignment.mute!
@teacher.reload
expect(@teacher.stream_item_instances.last).to_not be_hidden
end
it "does not create a message for admins and teachers with quiz submissions" do
course_with_teacher(active_all: true)
assignment = @course.assignments.create!(
title: "assignment",
points_possible: 10
)
quiz = @course.quizzes.build(
assignment_id: assignment.id,
title: "test quiz",
points_possible: 10
)
quiz.workflow_state = "available"
quiz.save!
user = account_admin_user
communication_channel(user, { username: "admin@example.com" })
submission = quiz.generate_submission(user, false)
Quizzes::SubmissionGrader.new(submission).grade_submission
communication_channel(@teacher, { username: "chang@example.com" })
submission2 = quiz.generate_submission(@teacher, false)
Quizzes::SubmissionGrader.new(submission2).grade_submission
expect(submission.submission.messages_sent).not_to include("Submission Graded")
expect(submission2.submission.messages_sent).not_to include("Submission Graded")
end
end
it "creates a stream_item_instance when graded and published" do
Notification.create name: "Submission Graded"
submission_spec_model
communication_channel(@user, { username: "somewhere@test.com" })
expect do
@assignment.grade_student(@user, grade: 10, grader: @teacher)
end.to change StreamItemInstance, :count
end
it "creates a stream_item_instance when graded, and then made it visible when unmuted" do
Notification.create name: "Submission Graded"
submission_spec_model
communication_channel(@user, { username: "somewhere@test.com" })
@assignment.mute!
expect do
@assignment.grade_student(@user, grade: 10, grader: @teacher)
end.to change StreamItemInstance, :count
@assignment.unmute!
stream_item_ids = StreamItem.where(asset_type: "Submission", asset_id: @assignment.submissions.all).pluck(:id)
stream_item_instances = StreamItemInstance.where(stream_item_id: stream_item_ids)
stream_item_instances.each { |sii| expect(sii).not_to be_hidden }
end
context "Submission Grade Changed" do
before :once do
Auditors::ActiveRecord::Partitioner.process
@assignment.ensure_post_policy(post_manually: false)
end
it "creates a message when the score is changed and the grades were already published" do
Notification.create(name: "Submission Grade Changed")
allow(@assignment).to receive_messages(score_to_grade: "10.0", due_at: Time.zone.now - 100)
submission_spec_model
communication_channel(@user, { username: "somewhere@test.com" })
s = @assignment.grade_student(@user, grade: 10, grader: @teacher)[0] # @submission
s.graded_at = Time.zone.parse("Jan 1 2000")
s.save
@submission = @assignment.grade_student(@user, grade: 9, grader: @teacher)[0]
expect(@submission).to eql(s)
expect(@submission.messages_sent).to include("Submission Grade Changed")
end
it "does not create a grade changed message when theres a quiz attached" do
Notification.create(name: "Submission Grade Changed")
allow(@assignment).to receive_messages(score_to_grade: "10.0", due_at: Time.now - 100)
submission_spec_model
@quiz = Quizzes::Quiz.create!(context: @course)
@submission.quiz_submission = @quiz.generate_submission(@user)
@submission.save!
communication_channel(@user, { username: "somewhere@test.com" })
s = @assignment.grade_student(@user, grade: 10, grader: @teacher)[0] # @submission
s.graded_at = Time.zone.parse("Jan 1 2000")
s.save
@submission = @assignment.grade_student(@user, grade: 9, grader: @teacher)[0]
expect(@submission).to eql(s)
expect(@submission.messages_sent).not_to include("Submission Grade Changed")
end
it "does not create a message when grades were already published for an assignment with hidden grades" do
@assignment.ensure_post_policy(post_manually: true)
Notification.create(name: "Submission Grade Changed")
allow(@assignment).to receive_messages(score_to_grade: "10.0", due_at: Time.zone.now - 100)
submission_spec_model
communication_channel(@user, { username: "somewhere@test.com" })
s = @assignment.grade_student(@user, grade: 10, grader: @teacher)[0] # @submission
s.graded_at = Time.zone.parse("Jan 1 2000")
s.save
@submission = @assignment.grade_student(@user, grade: 9, grader: @teacher)[0]
expect(@submission).to eql(s)
expect(@submission.messages_sent).not_to include("Submission Grade Changed")
end
it "does not create a message when the submission was recently graded" do
Notification.create(name: "Submission Grade Changed")
allow(@assignment).to receive_messages(score_to_grade: "10.0", due_at: Time.zone.now - 100)
submission_spec_model
communication_channel(@user, { username: "somewhere@test.com" })
s = @assignment.grade_student(@user, grade: 10, grader: @teacher)[0] # @submission
@submission = @assignment.grade_student(@user, grade: 9, grader: @teacher)[0]
expect(@submission).to eql(s)
expect(@submission.messages_sent).not_to include("Submission Grade Changed")
end
end
end
describe "permission policy" do
describe "can :grade" do
before do
@submission = Submission.new
@grader = User.new
end
it "delegates to can_grade?" do
[true, false].each do |value|
allow(@submission).to receive(:can_grade?).with(@grader).and_return(value)
expect(@submission.grants_right?(@grader, :grade)).to eq(value)
end
end
end
describe "can :read_grade" do
before(:once) do
@course = Course.create!
@student = @course.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user
@assignment = @course.assignments.create!
@submission = @assignment.submissions.find_by(user: @student)
end
it "returns true when their submission is posted and assignment manually posts" do
@assignment.ensure_post_policy(post_manually: true)
@submission.update!(posted_at: Time.zone.now)
expect(@submission.grants_right?(@student, :read_grade)).to be true
end
it "returns false when their submission is not posted and assignment manually posts" do
@assignment.ensure_post_policy(post_manually: true)
expect(@submission.grants_right?(@student, :read_grade)).to be false
end
it "returns true when their submission is posted and assignment automatically posts" do
@assignment.ensure_post_policy(post_manually: false)
@submission.update!(posted_at: Time.zone.now)
expect(@submission.grants_right?(@student, :read_grade)).to be true
end
it "returns true when their submission is not posted and assignment automatically posts" do
@assignment.ensure_post_policy(post_manually: false)
expect(@submission.grants_right?(@student, :read_grade)).to be true
end
end
describe "can :comment" do
before(:once) do
@course = Course.create!
@student = @course.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user
@assignment = @course.assignments.create!
@submission = @assignment.submissions.find_by(user: @student)
end
it "allows students to comment on own submission" do
expect(@submission.grants_right?(@student, :comment)).to be true
end
it "does not allow students in limited access accounts to comment on submissions" do
@course.root_account.enable_feature!(:allow_limited_access_for_students)
@course.account.settings[:enable_limited_access_for_students] = true
@course.account.save!
expect(@submission.grants_right?(@student, :comment)).to be false
end
end
end
describe "computation of scores" do
before(:once) do
@assignment.ensure_post_policy(post_manually: false)
@assignment.update!(points_possible: 10)
submission_spec_model
end
let(:scores) do
enrollment = Enrollment.where(user_id: @submission.user_id, course_id: @submission.context).first
enrollment.scores.order(:grading_period_id)
end
let(:grading_period_scores) do
scores.where.not(grading_period_id: nil)
end
let(:course_scores) do
scores.where(course_score: true)
end
let(:course_and_grading_period_scores) do
scores.where(course_score: true).or(scores.where.not(grading_period_id: nil).where(assignment_group_id: nil))
end
it "recomputes course scores when the submission score changes" do
expect { @assignment.grade_student(@student, grader: @teacher, score: 5) }.to change {
course_scores.pluck(:current_score)
}.from([nil]).to([50.0])
end
context "with grading periods" do
before(:once) do
@now = Time.zone.now
course = @submission.context
assignment_outside_of_period = course.assignments.create!(
due_at: 10.days.from_now(@now),
points_possible: 10
)
assignment_outside_of_period.grade_student(@user, grade: 8, grader: @teacher)
@assignment.update!(due_at: @now)
@root_account = course.root_account
group = @root_account.grading_period_groups.create!
group.enrollment_terms << course.enrollment_term
@grading_period = group.grading_periods.create!(
title: "Current Grading Period",
start_date: 5.days.ago(@now),
end_date: 5.days.from_now(@now)
)
end
it "updates the course score and grading period score if a submission " \
"in a grading period is graded" do
expect { @assignment.grade_student(@student, grader: @teacher, score: 5) }.to change {
course_and_grading_period_scores.pluck(:current_score)
}.from([nil, 80.0]).to([50.0, 65.0])
end
it "only updates the course score (not the grading period score) if a submission " \
"not in a grading period is graded" do
day_after_grading_period_ends = 1.day.from_now(@grading_period.end_date)
@assignment.update!(due_at: day_after_grading_period_ends)
expect { @assignment.grade_student(@student, grader: @teacher, score: 5) }.to change {
course_and_grading_period_scores.pluck(:current_score)
}.from([nil, 80.0]).to([nil, 65.0])
end
end
end
describe "#can_grade?" do
before do
@account = Account.new
@course = Course.new(account: @account)
@assignment = Assignment.new(course: @course)
@submission = Submission.new(assignment: @assignment)
@grader = User.new
@grader.id = 10
@student = User.new
@student.id = 42
allow(@course).to receive(:account_membership_allows).with(@grader).and_return(true)
allow(@course).to receive(:grants_right?).with(@grader, nil, :manage_grades).and_return(true)
@assignment.course = @course
allow(@assignment).to receive(:published?).and_return(true)
grading_period = double("grading_period", closed?: false)
allow(@submission).to receive(:grading_period).and_return(grading_period)
@submission.grader = @grader
@submission.user = @student
end
it 'returns true for published assignments if the grader is a teacher who is allowed to
manage grades' do
expect(@submission.grants_right?(@grader, :grade)).to be_truthy
end
context "when assignment is unpublished" do
before do
allow(@assignment).to receive(:published?).and_return(false)
@status = @submission.grants_right?(@grader, :grade)
end
it "returns false" do
expect(@status).to be_falsey
end
it "sets an appropriate error message" do
expect(@submission.grading_error_message).to include("unpublished")
end
end
context "when the grader does not have the right to manage grades for the course" do
before do
allow(@course).to receive(:grants_right?).with(@grader, nil, :manage_grades).and_return(false)
@status = @submission.grants_right?(@grader, :grade)
end
it "returns false" do
expect(@status).to be_falsey
end
it "sets an appropriate error message" do
expect(@submission.grading_error_message).to include("manage grades")
end
end
context "when the grader is a teacher and the assignment is in a closed grading period" do
before do
allow(@course).to receive(:account_membership_allows).with(@grader).and_return(false)
grading_period = double("grading_period", closed?: true)
allow(@submission).to receive(:grading_period).and_return(grading_period)
@status = @submission.grants_right?(@grader, :grade)
end
it "returns false" do
expect(@status).to be_falsey
end
it "sets an appropriate error message" do
expect(@submission.grading_error_message).to include("closed grading period")
end
end
context "when grader_id is a teacher's id and the assignment is in a closed grading period" do
before do
allow(@course).to receive(:account_membership_allows).with(@grader).and_return(false)
grading_period = double("grading_period", closed?: true)
allow(@submission).to receive(:grading_period).and_return(grading_period)
@submission.grader = nil
@submission.grader_id = 10
@status = @submission.grants_right?(@grader, :grade)
end
it "returns false" do
expect(@status).to be_falsey
end
it "sets an appropriate error message" do
expect(@submission.grading_error_message).to include("closed grading period")
end
end
it 'returns true if the grader is an admin even if the assignment is in
a closed grading period' do
allow(@course).to receive(:account_membership_allows).with(@grader).and_return(true)
grading_period = double("grading_period", closed?: false)
allow(@submission).to receive(:grading_period).and_return(grading_period)
expect(@submission.grants_right?(@grader, :grade)).to be_truthy
end
end
describe "#can_autograde?" do
before do
@account = Account.new
@course = Course.new(account: @account)
@assignment = Assignment.new(course: @course)
@submission = Submission.new(assignment: @assignment)
@submission.grader_id = -1
@submission.user_id = 10
allow(@assignment).to receive(:published?).and_return(true)
grading_period = double("grading_period", closed?: false)
allow(@submission).to receive(:grading_period).and_return(grading_period)
end
it 'returns true for published assignments with an autograder and when the assignment is not
in a closed grading period' do
expect(@submission.can_autograde?).to be_truthy
end
context "when assignment is unpublished" do
before do
allow(@assignment).to receive(:published?).and_return(false)
@status = @submission.can_autograde?
end
it "returns false" do
expect(@status).to be_falsey
end
it "sets an appropriate error message" do
expect(@submission.grading_error_message).to include("unpublished")
end
end
context "when the grader is not an autograder" do
before do
@submission.grader_id = 1
@status = @submission.can_autograde?
end
it "returns false" do
expect(@status).to be_falsey
end
it "sets an appropriate error message" do
expect(@submission.grading_error_message).to include("autograded")
end
end
context "when the assignment is in a closed grading period for the student" do
before do
grading_period = double("grading_period", closed?: true)
allow(@submission).to receive(:grading_period).and_return(grading_period)
@status = @submission.can_autograde?
end
it "returns false" do
expect(@status).to be_falsey
end
it "sets an appropriate error message" do
expect(@submission.grading_error_message).to include("closed grading period")
end
end
end
describe "#can_read_submission_user_name?" do
before(:once) do
@course = Course.create!
@student = @course.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user
assignment = @course.assignments.create!(anonymous_grading: true)
@submission = assignment.submissions.find_by(user: @student)
end
context "anonymous assignments" do
it "returns true when the user is the submission's owner" do
expect(@submission.can_read_submission_user_name?(@student, nil)).to be true
end
it "returns false when the user is not the submission's owner" do
teacher = User.create!
@course.enroll_teacher(teacher, enrollment_state: :active)
expect(@submission.can_read_submission_user_name?(@teacher, nil)).to be false
end
end
end
describe "#user_can_read_grade?" do
before(:once) do
@course = Course.create!
@student = @course.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user
@assignment = @course.assignments.create!
@submission = @assignment.submissions.find_by(user: @student)
end
it "returns true when their submission is posted and assignment manually posts" do
@assignment.ensure_post_policy(post_manually: true)
@submission.update!(posted_at: Time.zone.now)
expect(@submission.user_can_read_grade?(@student)).to be true
end
it "returns false when their submission is not posted and assignment manually posts" do
@assignment.ensure_post_policy(post_manually: true)
expect(@submission.user_can_read_grade?(@student)).to be false
end
it "returns true when their submission is posted and assignment automatically posts" do
@assignment.ensure_post_policy(post_manually: false)
@submission.update!(posted_at: Time.zone.now)
expect(@submission.user_can_read_grade?(@student)).to be true
end
it "returns true when their submission is not posted and assignment automatically posts" do
@assignment.ensure_post_policy(post_manually: false)
expect(@submission.user_can_read_grade?(@student)).to be true
end
end
context "OriginalityReport" do
let(:attachment) { attachment_model(context: group) }
let(:course) { course_model }
let(:submission) { submission_model }
let(:group) { Group.create!(name: "test group", context: course) }
let(:originality_report) do
submission.update(attachment_ids: attachment.id.to_s)
OriginalityReport.create!(attachment:, originality_score: "1", submission:)
end
describe "#originality_data" do
it "generates the originality data" do
originality_report.originality_report_url = "http://example.com"
originality_report.save!
expect(submission.originality_data).to eq(
{
attachment.asset_string => {
similarity_score: originality_report.originality_score,
state: originality_report.state,
attachment_id: originality_report.attachment_id,
report_url: originality_report.originality_report_url,
status: originality_report.workflow_state,
error_message: nil,
created_at: originality_report.created_at,
updated_at: originality_report.updated_at
}
}
)
end
context "multiple originality reports for the same attachment" do
let(:preferred_report) do
OriginalityReport.create!(attachment:,
submission:,
workflow_state: preferred_state,
originality_score: (preferred_state == "scored") ? 1 : nil)
end
let(:other_report) do
OriginalityReport.create!(attachment:,
submission:,
workflow_state: other_state,
originality_score: (other_state == "scored") ? 2 : nil)
end
before do
submission.update(attachment_ids: attachment.id.to_s)
end
OriginalityReport::ORDERED_VALID_WORKFLOW_STATES.each do |state|
context "and both reports have a workflow_state of #{state}" do
let(:preferred_state) { state }
let(:other_state) { state }
it "chooses the more up-to-date report" do
preferred_report.update!(updated_at: 1.minute.from_now)
other_report.update!(updated_at: Time.zone.now)
report_data = submission.originality_data[attachment.asset_string]
expect(report_data[:similarity_score]).to eq preferred_report.originality_score
end
end
end
shared_examples_for "submission with duplicate reports with different states" do
it "uses the preferred report" do
preferred_report
other_report
report_data = submission.originality_data[attachment.asset_string]
expect(report_data[:similarity_score]).to eq preferred_report.originality_score
expect(report_data[:status]).to eq preferred_report.workflow_state
end
it "uses the preferred report even if the other report was updated later" do
preferred_report.update(updated_at: Time.zone.now)
other_report.update(updated_at: 1.minute.from_now)
report_data = submission.originality_data[attachment.asset_string]
expect(report_data[:similarity_score]).to eq preferred_report.originality_score
expect(report_data[:status]).to eq preferred_report.workflow_state
end
end
context "and the reports have differing workflow_states" do
context "of scored and error" do
let(:preferred_state) { "scored" }
let(:other_state) { "error" }
it_behaves_like "submission with duplicate reports with different states"
end
context "of scored and pending" do
let(:preferred_state) { "scored" }
let(:other_state) { "pending" }
it_behaves_like "submission with duplicate reports with different states"
end
context "of error and pending" do
let(:preferred_state) { "error" }
let(:other_state) { "pending" }
it_behaves_like "submission with duplicate reports with different states"
end
end
end
it "includes tii data" do
tii_data = {
similarity_score: 10,
state: "acceptable",
report_url: "http://example.com",
status: "scored"
}
submission.turnitin_data[attachment.asset_string] = tii_data
expect(submission.originality_data).to eq({
attachment.asset_string => tii_data
})
end
it "overrites the tii data with the originality data" do
originality_report.originality_report_url = "http://example.com"
originality_report.save!
tii_data = {
similarity_score: 10,
state: "acceptable",
report_url: "http://example.com/tii",
status: "pending"
}
submission.turnitin_data[attachment.asset_string] = tii_data
expect(submission.originality_data).to eq(
{
attachment.asset_string => {
similarity_score: originality_report.originality_score,
attachment_id: attachment.id,
state: originality_report.state,
report_url: originality_report.originality_report_url,
status: originality_report.workflow_state,
error_message: nil,
created_at: originality_report.created_at,
updated_at: originality_report.updated_at
}
}
)
end
it "does not cause error if originality score is nil" do
originality_report.update(originality_score: nil)
expect { submission.originality_data }.not_to raise_error
end
it "rounds the score to 2 decimal places" do
originality_report.originality_score = 2.94997
originality_report.save!
expect(submission.originality_data[attachment.asset_string][:similarity_score]).to eq(2.95)
end
it "filters out :provider key and value" do
originality_report.originality_report_url = "http://example.com"
originality_report.save!
tii_data = {
provider: "vericite",
similarity_score: 10,
state: "acceptable",
report_url: "http://example.com/tii",
status: "pending"
}
submission.turnitin_data[attachment.asset_string] = tii_data
expect(submission.originality_data).not_to include :vericite
end
it "finds originality data text entry submissions" do
submission.update!(attachment_ids: attachment.id.to_s)
originality_report.update!(attachment: nil)
expect(submission.originality_data).to eq({
OriginalityReport.submission_asset_key(submission) => {
similarity_score: originality_report.originality_score,
attachment_id: nil,
state: originality_report.state,
report_url: originality_report.originality_report_url,
status: originality_report.workflow_state,
error_message: nil,
created_at: originality_report.created_at,
updated_at: originality_report.updated_at,
}
})
end
context "when originality report has an error message" do
subject { submission.originality_data[attachment.asset_string] }
let(:error_message) { "We can't process that file :(" }
before { originality_report.update!(error_message:) }
it "includes the error message" do
expect(subject[:error_message]).to eq error_message
end
end
end
describe "#attachment_ids_for_version" do
let(:attachments) do
[
attachment_model(filename: "submission-a.doc", context: @student),
attachment_model(filename: "submission-b.doc", context: @student),
attachment_model(filename: "submission-c.doc", context: @student)
]
end
let(:single_attachment) { attachment_model(filename: "single.doc", context: @student) }
before { student_in_course(active_all: true) }
it "includes attachment ids from 'attachment_id'" do
submission = @assignment.submit_homework(@student, submission_type: "online_upload", attachments:)
submission.update!(attachment_id: single_attachment)
expect(submission.attachment_ids_for_version).to match_array attachments.map(&:id) + [single_attachment.id]
end
end
describe "#has_originality_report?" do
let(:test_course) do
test_course = course_model
test_course.enroll_teacher(test_teacher, enrollment_state: "active")
test_course.enroll_student(test_student, enrollment_state: "active")
test_course
end
let(:test_teacher) { User.create }
let(:test_student) { User.create }
let(:assignment) { Assignment.create!(title: "test assignment", context: test_course) }
let(:attachment) { attachment_model(filename: "submission.doc", context: test_student) }
let(:report_url) { "http://www.test-score.com" }
it "returns true for standard reports" do
submission = assignment.submit_homework(test_student, attachments: [attachment])
OriginalityReport.create!(
attachment:,
submission:,
originality_score: 0.5,
originality_report_url: report_url
)
expect(submission.has_originality_report?).to be true
end
it "returns true for text entry reports" do
submission = assignment.submit_homework(test_student, body: "hi")
OriginalityReport.create!(
submission:,
originality_score: 0.5,
originality_report_url: report_url
)
expect(submission.has_originality_report?).to be true
end
it "returns true for group reports" do
user_two = test_student.dup
user_two.update!(lti_context_id: SecureRandom.uuid, lti_id: SecureRandom.uuid, uuid: CanvasSlug.generate_securish_uuid)
assignment.course.enroll_student(user_two)
group = group_model(context: assignment.course)
group.update!(users: [user_two, test_student])
submission = assignment.submit_homework(test_student, submission_type: "online_upload", attachments: [attachment])
submission_two = assignment.submit_homework(user_two, submission_type: "online_upload", attachments: [attachment])
submission.update!(group:)
submission_two.update!(group:)
assignment.submissions.each do |s|
s.update!(group:, turnitin_data: { blah: 1 })
end
report = OriginalityReport.create!(originality_score: "1", submission:, attachment:)
report.copy_to_group_submissions!
expect(assignment.submissions.map(&:has_originality_report?)).to match_array [true, true]
end
it "returns false when no reports are present" do
submission = assignment.submit_homework(test_student, attachments: [attachment])
expect(submission.has_originality_report?).to be false
end
end
describe "#originality_report_url" do
let_once(:test_course) do
test_course = course_model
test_course.enroll_teacher(test_teacher, enrollment_state: "active")
test_course.enroll_student(test_student, enrollment_state: "active")
test_course
end
let_once(:test_teacher) { User.create }
let_once(:test_student) { User.create }
let_once(:assignment) { Assignment.create!(title: "test assignment", context: test_course) }
let_once(:attachment) { attachment_model(filename: "submission.doc", context: test_student) }
let_once(:submission) { assignment.submit_homework(test_student, attachments: [attachment]) }
let_once(:report_url) { "http://www.test-score.com" }
let(:originality_report) do
OriginalityReport.create!(attachment:,
submission:,
originality_score: 0.5,
originality_report_url: report_url)
end
it "returns nil if no originality report exists for the submission" do
originality_report.destroy
expect(submission.originality_report_url(attachment.asset_string, test_teacher)).to be_nil
end
it "returns nil if no report url is present in the report" do
originality_report.update_attribute(:originality_report_url, nil)
expect(submission.originality_report_url(attachment.asset_string, test_teacher)).to be_nil
end
it "returns the originality_report_url if present" do
originality_report
expect(submission.originality_report_url(attachment.asset_string, test_teacher)).to eq(report_url)
end
it "returns the report url for text entry submission reports" do
originality_report.update!(attachment: nil)
expect(submission.originality_report_url(submission.asset_string, test_teacher)).to eq report_url
end
it "requires the :grade permission" do
unauthorized_user = User.new
expect(submission.originality_report_url(attachment.asset_string, unauthorized_user)).to be_nil
end
context "when there are multiple originality reports" do
context "for text entry submissions" do
let(:other_submission) { assignment.submit_homework(test_student, body: "hello world") }
let(:other_report_url) { "https://another-report.com" }
let(:other_report) do
OriginalityReport.create!(attachment: nil,
submission: other_submission,
originality_score: 0.4,
originality_report_url: other_report_url)
end
it "can use attempt number to find the report url" do
originality_report.update!(attachment: nil)
other_report
expect(other_submission.attempt).to be > submission.attempt
expect(submission.originality_report_url(submission.asset_string,
test_teacher,
submission.attempt.to_s)).to eq report_url
expect(submission.originality_report_url(submission.asset_string,
test_teacher,
other_submission.attempt.to_s)).to eq(other_report_url)
end
end
context "for multiple attachments" do
let(:other_attachment) { attachment_model(filename: "submission-b.doc", context: test_student) }
let(:other_report_url) { "http://another-report.com" }
let(:other_report) do
OriginalityReport.create!(attachment: other_attachment,
submission:,
originality_score: 0.4,
originality_report_url: other_report_url)
end
it "considers all attachments in submission history valid" do
Timecop.freeze(2.days.ago) do
assignment.submit_homework(test_student,
submission_type: "online_upload",
attachments: [attachment])
end
Timecop.freeze(1.day.ago) do
assignment.submit_homework(test_student,
submission_type: "online_upload",
attachments: [other_attachment])
end
originality_report
other_report
expect(submission.originality_report_url(attachment.asset_string, test_teacher))
.to eq(report_url)
expect(submission.originality_report_url(other_attachment.asset_string, test_teacher))
.to eq(other_report_url)
end
it "gives the correct url for each attachment" do
assignment.submit_homework(test_student,
submission_type: "online_upload",
attachments: [attachment, other_attachment])
originality_report
other_report
expect(submission.originality_report_url(attachment.asset_string, test_teacher))
.to eq(report_url)
expect(submission.originality_report_url(other_attachment.asset_string, test_teacher))
.to eq(other_report_url)
end
# This combines having multiple attachments with some duplicate OriginalityReports.
context "with some duplicate reports for an attachment" do
let(:duplicate_url) { "http://duplicate.com" }
let(:duplicate_report) do
OriginalityReport.create!(attachment:,
submission:,
workflow_state: "pending",
originality_report_url: duplicate_url)
end
before do
assignment.submit_homework(test_student,
submission_type: "online_upload",
attachments: [attachment, other_attachment])
end
it "uses the scored report's URL" do
originality_report
other_report
duplicate_report
expect(submission.originality_report_url(attachment.asset_string, test_teacher))
.to eq(report_url)
end
it "uses the scored report's URL even if the other report is newer" do
originality_report.update(updated_at: 1.day.ago)
other_report
duplicate_report.update(updated_at: 1.day.from_now)
expect(submission.originality_report_url(attachment.asset_string, test_teacher))
.to eq(report_url)
end
it "can still get other attachment's URLs" do
originality_report
other_report
duplicate_report
expect(submission.originality_report_url(other_attachment.asset_string, test_teacher))
.to eq(other_report_url)
end
end
end
# If we have multiple reports for the same attachment-submission combo, then
# those reports are considered duplicates. However, they might have different states
# so we have to be sure we're using the correct report.
context "and the reports are for the same attachment" do
let(:preferred_url) { "http://preferred-score.com" }
let(:other_url) { "http://other-score.com" }
let(:preferred_report) do
OriginalityReport.create!(attachment:,
submission:,
originality_score: (preferred_state == "scored") ? 1 : nil,
workflow_state: preferred_state,
originality_report_url: preferred_url)
end
let(:other_report) do
OriginalityReport.create!(attachment:,
submission:,
originality_score: (other_state == "scored") ? 2 : nil,
workflow_state: other_state,
originality_report_url: other_url)
end
before do
submission.update(attachment_ids: attachment.id.to_s)
end
OriginalityReport::ORDERED_VALID_WORKFLOW_STATES.each do |state|
context "and have the same workflow_state of #{state}" do
let(:preferred_state) { state }
let(:other_state) { state }
it "chooses the more up-to-date report's URL" do
preferred_report.update(updated_at: 1.minute.from_now)
other_report.update(updated_at: Time.zone.now)
expect(submission.originality_report_url(attachment.asset_string,
test_teacher)).to eq preferred_url
end
end
end
shared_examples_for "submission with duplicate reports with different states" do
it "chooses the preferred report's URL" do
preferred_report
other_report
expect(submission.originality_report_url(attachment.asset_string,
test_teacher)).to eq preferred_url
end
it "chooses the preferred report's URL even when the other report is newer" do
preferred_report.update(updated_at: Time.zone.now)
other_report.update(updated_at: 1.minute.from_now)
expect(submission.originality_report_url(attachment.asset_string,
test_teacher)).to eq preferred_url
end
end
context "and the reports have differing workflow_states" do
context "of scored and error" do
let(:preferred_state) { "scored" }
let(:other_state) { "error" }
it_behaves_like "submission with duplicate reports with different states"
end
context "of scored and pending" do
let(:preferred_state) { "scored" }
let(:other_state) { "pending" }
it_behaves_like "submission with duplicate reports with different states"
end
context "of error and pending" do
let(:preferred_state) { "error" }
let(:other_state) { "pending" }
it_behaves_like "submission with duplicate reports with different states"
end
end
end
end
end
end
context "turnitin" do
context "Turnitin LTI" do
let(:lti_tii_data) do
{
"attachment_42" => {
status: "error",
outcome_response: {
"outcomes_tool_placement_url" => "https://api.turnitin.com/api/lti/1p0/invalid?lang=en_us",
"paperid" => "607954245",
"lis_result_sourcedid" => "10-5-42-8-invalid"
},
public_error_message: "Turnitin has not returned a score after 11 attempts to retrieve one."
}
}
end
let(:submission) { Submission.new }
describe "#turnitinable_by_lti?" do
it "returns true if there is an associated lti tool and data stored" do
submission.turnitin_data = lti_tii_data
expect(submission.turnitinable_by_lti?).to be true
end
end
describe "#resubmit_lti_tii" do
let(:tool) do
@course.context_external_tools.create(
name: "a",
consumer_key: "12345",
shared_secret: "secret",
url: "http://example.com/launch"
)
end
it "resubmits errored tii attachments" do
a = @course.assignments.create!(title: "test",
submission_types: "external_tool",
external_tool_tag_attributes: { url: tool.url })
submission.assignment = a
submission.turnitin_data = lti_tii_data
submission.user = @user
outcome_response_processor_mock = double("outcome_response_processor")
expect(outcome_response_processor_mock).to receive(:resubmit).with(submission, "attachment_42")
allow(Turnitin::OutcomeResponseProcessor).to receive(:new).and_return(outcome_response_processor_mock)
submission.retrieve_lti_tii_score
end
it "resubmits errored tii attachments even if turnitin_data has non-hash values" do
a = @course.assignments.create!(title: "test",
submission_types: "external_tool",
external_tool_tag_attributes: { url: tool.url })
submission.assignment = a
submission.turnitin_data = lti_tii_data.merge(last_processed_attempt: 1)
submission.user = @user
outcome_response_processor_mock = double("outcome_response_processor")
expect(outcome_response_processor_mock).to receive(:resubmit).with(submission, "attachment_42")
allow(Turnitin::OutcomeResponseProcessor).to receive(:new).and_return(outcome_response_processor_mock)
submission.retrieve_lti_tii_score
end
end
end
context "submission" do
def init_turnitin_api
@turnitin_api = Turnitin::Client.new("test_account", "sekret")
expect(@submission.context).to receive(:turnitin_settings).at_least(1).and_return([:placeholder])
expect(Turnitin::Client).to receive(:new).at_least(1).with(:placeholder).and_return(@turnitin_api)
end
before(:once) do
setup_account_for_turnitin(@assignment.context.account)
@assignment.submission_types = "online_upload,online_text_entry"
@assignment.turnitin_enabled = true
@assignment.save!
@submission = @assignment.submit_homework(@user, { body: "hello there", submission_type: "online_text_entry" })
end
it "submits to turnitin after a delay" do
job = Delayed::Job.list_jobs(:future, 100).find { |j| j.tag == "Submission#submit_to_turnitin" }
expect(job).not_to be_nil
expect(job.run_at).to be > Time.now.utc
end
it "initially sets turnitin submission to pending" do
init_turnitin_api
expect(@turnitin_api).to receive(:createOrUpdateAssignment).with(@assignment, @assignment.turnitin_settings).and_return({ assignment_id: "1234" })
expect(@turnitin_api).to receive(:enrollStudent).with(@context, @user).and_return(double(success?: true))
expect(@turnitin_api).to receive(:submitPaper).and_return({
@submission.asset_string => {
object_id: "12345"
}
})
@submission.submit_to_turnitin
expect(@submission.reload.turnitin_data[@submission.asset_string][:status]).to eq "pending"
end
it "schedules a retry if something fails initially" do
init_turnitin_api
expect(@turnitin_api).to receive(:createOrUpdateAssignment).with(@assignment, @assignment.turnitin_settings).and_return({ assignment_id: "1234" })
expect(@turnitin_api).to receive(:enrollStudent).with(@context, @user).and_return(double(success?: false))
@submission.submit_to_turnitin
expect(Delayed::Job.list_jobs(:future, 100).count { |j| j.tag == "Submission#submit_to_turnitin" }).to eq 2
end
it "sets status as failed if something fails on a retry" do
init_turnitin_api
expect(@assignment).to receive(:create_in_turnitin).and_return(false)
expect(@turnitin_api).to receive(:enrollStudent).with(@context, @user).and_return(double(success?: false, error?: true, error_hash: {}))
expect(@turnitin_api).not_to receive(:submitPaper)
@submission.submit_to_turnitin(Submission::TURNITIN_RETRY)
expect(@submission.reload.turnitin_data[:status]).to eq "error"
end
it "sets status back to pending on retry" do
init_turnitin_api
# first a submission, to get us into failed state
expect(@assignment).to receive(:create_in_turnitin).and_return(false)
expect(@turnitin_api).to receive(:enrollStudent).with(@context, @user).and_return(double(success?: false, error?: true, error_hash: {}))
expect(@turnitin_api).not_to receive(:submitPaper)
@submission.submit_to_turnitin(Submission::TURNITIN_RETRY)
expect(@submission.reload.turnitin_data[:status]).to eq "error"
# resubmit
@submission.resubmit_to_turnitin
expect(@submission.reload.turnitin_data[:status]).to be_nil
expect(@submission.turnitin_data[@submission.asset_string][:status]).to eq "pending"
end
it "sets status to scored on success" do
init_turnitin_api
@submission.turnitin_data ||= {}
@submission.turnitin_data[@submission.asset_string] = { object_id: "1234", status: "pending" }
expect(@turnitin_api).to receive(:generateReport).with(@submission, @submission.asset_string).and_return({
similarity_score: 56,
web_overlap: 22,
publication_overlap: 0,
student_overlap: 33
})
@submission.check_turnitin_status
expect(@submission.reload.turnitin_data[@submission.asset_string][:status]).to eq "scored"
end
it "sets status as failed if something fails after several attempts" do
init_turnitin_api
@submission.turnitin_data ||= {}
@submission.turnitin_data[@submission.asset_string] = { object_id: "1234", status: "pending" }
expect(@turnitin_api).to receive(:generateReport).with(@submission, @submission.asset_string).and_return({})
expects_job_with_tag("Submission#check_turnitin_status") do
@submission.check_turnitin_status(Submission::TURNITIN_STATUS_RETRY - 1)
expect(@submission.reload.turnitin_data[@submission.asset_string][:status]).to eq "pending"
end
@submission.check_turnitin_status(Submission::TURNITIN_STATUS_RETRY)
@submission.reload
updated_data = @submission.turnitin_data[@submission.asset_string]
expect(updated_data[:status]).to eq "error"
end
it "checks status for all assets" do
init_turnitin_api
@submission.turnitin_data ||= {}
@submission.turnitin_data[@submission.asset_string] = { object_id: "1234", status: "pending" }
@submission.turnitin_data["other_asset"] = { object_id: "xxyy", status: "pending" }
expect(@turnitin_api).to receive(:generateReport).with(@submission, @submission.asset_string).and_return({
similarity_score: 56, web_overlap: 22, publication_overlap: 0, student_overlap: 33
})
expect(@turnitin_api).to receive(:generateReport).with(@submission, "other_asset").and_return({ similarity_score: 20 })
@submission.check_turnitin_status
@submission.reload
expect(@submission.turnitin_data[@submission.asset_string][:status]).to eq "scored"
expect(@submission.turnitin_data["other_asset"][:status]).to eq "scored"
end
it "does not blow up if submission_type has changed when job runs" do
@submission.submission_type = "online_url"
expect(@submission.context).not_to receive(:turnitin_settings)
expect { @submission.submit_to_turnitin }.not_to raise_error
end
end
describe "group" do
before(:once) do
@teacher = User.create(name: "some teacher")
@student = User.create(name: "a student")
@student1 = User.create(name: "student 1")
@context.enroll_teacher(@teacher)
@context.enroll_student(@student)
@context.enroll_student(@student1)
setup_account_for_turnitin(@context.account)
@a = assignment_model(course: @context, group_category: "Study Groups")
@a.submission_types = "online_upload,online_text_entry"
@a.turnitin_enabled = true
@a.save!
@group1 = @a.context.groups.create!(name: "Study Group 1", group_category: @a.group_category)
@group1.add_user(@student)
@group1.add_user(@student1)
end
it "submits to turnitin for the original submitter" do
submission = @a.submit_homework @student, submission_type: "online_text_entry", body: "blah"
Submission.where(assignment_id: @a).each do |s|
if s.id == submission.id
expect(s.turnitin_data[:last_processed_attempt]).to be > 0
else
expect(s.turnitin_data).to eq({})
end
end
end
end
context "report" do
before :once do
@assignment.submission_types = "online_upload,online_text_entry"
@assignment.turnitin_enabled = true
@assignment.turnitin_settings = @assignment.turnitin_settings # rubocop:disable Lint/SelfAssignment
@assignment.save!
@submission = @assignment.submit_homework(@user, { body: "hello there", submission_type: "online_text_entry" })
@submission.turnitin_data = {
"submission_#{@submission.id}" => {
web_overlap: 92,
error: true,
publication_overlap: 0,
state: "failure",
object_id: "123456789",
student_overlap: 90,
similarity_score: 92
}
}
@submission.save!
end
before do
api = Turnitin::Client.new("test_account", "sekret")
expect(Turnitin::Client).to receive(:new).at_least(1).and_return(api)
expect(api).to receive(:sendRequest).with(:generate_report, 1, include(oid: "123456789")).at_least(1).and_return("http://foo.bar")
end
it "lets teachers view the turnitin report" do
@teacher = User.create
@context.enroll_teacher(@teacher)
expect(@submission).to be_grants_right(@teacher, nil, :view_turnitin_report)
expect(@submission.turnitin_report_url("submission_#{@submission.id}", @teacher)).not_to be_nil
end
it "lets students view the turnitin report after grading" do
@assignment.turnitin_settings[:originality_report_visibility] = "after_grading"
@assignment.save!
@submission.reload
expect(@submission).not_to be_grants_right(@user, nil, :view_turnitin_report)
expect(@submission.turnitin_report_url("submission_#{@submission.id}", @user)).to be_nil
@submission.score = 1
@submission.grade_it!
AdheresToPolicy::Cache.clear
expect(@submission).to be_grants_right(@user, nil, :view_turnitin_report)
expect(@submission.turnitin_report_url("submission_#{@submission.id}", @user)).not_to be_nil
end
it "lets students view the turnitin report immediately if the visibility setting allows it" do
@assignment.turnitin_settings[:originality_report_visibility] = "after_grading"
@assignment.save
@submission.reload
expect(@submission).not_to be_grants_right(@user, nil, :view_turnitin_report)
expect(@submission.turnitin_report_url("submission_#{@submission.id}", @user)).to be_nil
@assignment.turnitin_settings[:originality_report_visibility] = "immediate"
@assignment.save
@submission.reload
AdheresToPolicy::Cache.clear
expect(@submission).to be_grants_right(@user, nil, :view_turnitin_report)
expect(@submission.turnitin_report_url("submission_#{@submission.id}", @user)).not_to be_nil
end
it "lets students view the turnitin report after the due date if the visibility setting allows it" do
@assignment.turnitin_settings[:originality_report_visibility] = "after_due_date"
@assignment.due_at = Time.now + 1.day
@assignment.save
@submission.reload
expect(@submission).not_to be_grants_right(@user, nil, :view_turnitin_report)
expect(@submission.turnitin_report_url("submission_#{@submission.id}", @user)).to be_nil
@assignment.due_at = Time.now - 1.day
@assignment.save
@submission.reload
AdheresToPolicy::Cache.clear
expect(@submission).to be_grants_right(@user, nil, :view_turnitin_report)
expect(@submission.turnitin_report_url("submission_#{@submission.id}", @user)).not_to be_nil
end
end
end
describe "'view_turnitin_report' right" do
subject { @submission }
let(:teacher) { @teacher }
let(:student) { @student }
before :once do
@assignment.update!(submission_types: "online_upload,online_text_entry")
@submission = @assignment.submit_homework(student, { body: "hello there", submission_type: "online_text_entry" })
@submission.update!(turnitin_data: {
"submission_#{@submission.id}" => {
web_overlap: 92,
error: true,
publication_overlap: 0,
state: "failure",
object_id: "123456789",
student_overlap: 90,
similarity_score: 92
}
})
end
it "is available when the plagiarism report is from turnitin" do
expect(@submission).to be_grants_right(teacher, nil, :view_turnitin_report)
end
it "is available when the plagiarism report is blank (defaults to turnitin)" do
@submission.turnitin_data.delete(:provider)
expect(@submission).to be_grants_right(teacher, nil, :view_turnitin_report)
end
it "is not available when the plagiarism report is from vericite" do
@submission.turnitin_data[:provider] = "vericite"
expect(@submission).not_to be_grants_right(teacher, nil, :view_turnitin_report)
end
it "is available when the plagiarism data shows vericite and there is an LTI 2 assignment configuration for something else" do
@submission.turnitin_data[:provider] = "vericite"
AssignmentConfigurationToolLookup.create!(
assignment: @assignment,
tool_product_code: "turnitin-lti",
tool_type: "Lti::MessageHandler",
context_type: "Account",
tool_resource_type_code: "code",
tool_vendor_code: "turnitin.com"
)
expect(@submission).to be_grants_right(teacher, nil, :view_turnitin_report)
end
it "is not available when the plagiarism data shows vericite and there is an LTI 2 assignment configuration for vericite" do
@submission.turnitin_data[:provider] = "vericite"
AssignmentConfigurationToolLookup.create!(
assignment: @assignment,
tool_product_code: "vericite",
tool_type: "Lti::MessageHandler",
context_type: "Account",
tool_resource_type_code: "code",
tool_vendor_code: "vericite"
)
expect(@submission).not_to be_grants_right(teacher, nil, :view_turnitin_report)
end
it { expect(@submission).to be_grants_right(student, nil, :view_turnitin_report) }
context "when originality report visibility is after_grading" do
before do
@assignment.update!(
turnitin_settings: @assignment.turnitin_settings.merge(originality_report_visibility: "after_grading")
)
end
it { is_expected.not_to be_grants_right(student, nil, :view_turnitin_report) }
context "when the submission is graded" do
subject(:submission) { @assignment.grade_student(student, grade: 10, grader: teacher).first }
it { is_expected.to be_grants_right(student, nil, :view_turnitin_report) }
end
end
context "when originality report visibility is after_due_date" do
before do
@assignment.update!(
turnitin_settings: @assignment.turnitin_settings.merge(originality_report_visibility: "after_due_date")
)
end
it { is_expected.not_to be_grants_right(student, nil, :view_turnitin_report) }
context "when assignment.due_date is in the past" do
before { @assignment.update!(due_at: 1.day.ago) }
it { is_expected.to be_grants_right(student, nil, :view_turnitin_report) }
end
end
context "when originality report visibility is never" do
before do
@assignment.update!(
turnitin_settings: @assignment.turnitin_settings.merge(originality_report_visibility: "never")
)
end
it { is_expected.not_to be_grants_right(student, nil, :view_turnitin_report) }
context "when the teacher's enrollment is concluded" do
before do
@course.enrollments.where(user: teacher).each(&:conclude)
end
it "still allows the teacher (with view_all_grades) to see reports" do
expect(@submission).to be_grants_right(teacher, nil, :view_turnitin_report)
end
end
end
end
describe "'view_vericite_report' right" do
let(:teacher) do
user = User.create
@context.enroll_teacher(user)
user
end
before :once do
Auditors::ActiveRecord::Partitioner.process
@assignment.update!(submission_types: "online_upload,online_text_entry")
@submission = @assignment.submit_homework(@user, { body: "hello there", submission_type: "online_text_entry" })
@submission.turnitin_data = {
"submission_#{@submission.id}" => {
web_overlap: 92,
error: true,
publication_overlap: 0,
state: "failure",
object_id: "123456789",
student_overlap: 90,
similarity_score: 92
},
:provider => "vericite"
}
@submission.save!
end
it "is available when the plagiarism report is from vericite" do
expect(@submission).to be_grants_right(teacher, nil, :view_vericite_report)
end
it "is not available when the plagiarism report is from turnitin" do
@submission.turnitin_data[:provider] = "turnitin"
expect(@submission).not_to be_grants_right(teacher, nil, :view_vericite_report)
end
it "is not available when the plagiarism report is blank (defaults to turnitin)" do
@submission.turnitin_data.delete(:provider)
expect(@submission).not_to be_grants_right(teacher, nil, :view_vericite_report)
end
end
describe "#not_submitted?" do
let(:submission) { @assignment.submissions.find_by(user: @student) }
it "returns true if the workflow state is unsubmitted (same as unsubmitted?)" do
expect(submission).to be_unsubmitted
expect(submission).to be_not_submitted
end
it "returns true if the student has been graded but has not submitted (different from unsubmitted?)" do
@assignment.grade_student(@student, grade: 3, grader: @teacher)
expect(submission).not_to be_unsubmitted
expect(submission).to be_not_submitted
end
it "returns false if the student has submitted (same as unsubmitted?)" do
@assignment.submit_homework(@student, body: "hi")
expect(submission).not_to be_unsubmitted
expect(submission).not_to be_not_submitted
end
end
describe "#external_tool_url" do
let(:submission) { Submission.new }
let(:lti_submission) { @assignment.submit_homework(@user, submission_type: "basic_lti_launch", url: "http://www.example.com") }
context "submission_type of 'basic_lti_launch'" do
it "returns a url containing the submitted url" do
expect(lti_submission.external_tool_url).to eq(lti_submission.url)
end
it "accepts query params to be included in the URL" do
url = lti_submission.external_tool_url(query_params: { foo: false })
query_params = Rack::Utils.parse_query(URI(url).query)
expect(query_params["foo"]).to eq("false")
end
end
context 'submission_type of anything other than "basic_lti_launch"' do
it "returns nothing" do
expect(submission.external_tool_url).to be_nil
end
end
end
it "returns the correct quiz_submission_version" do
# set up the data to have a submission with a quiz submission with multiple versions
course_factory
quiz = @course.quizzes.create!
quiz_submission = quiz.generate_submission @user, false
quiz_submission.save
@assignment.submissions.find_by!(user: @user).update!(quiz_submission_id: quiz_submission.id)
submission = @assignment.submit_homework @user, submission_type: "online_quiz"
submission.quiz_submission_id = quiz_submission.id
# set the microseconds of the submission.submitted_at to be less than the
# quiz_submission.finished_at.
# first set them to be exactly the same (with microseconds)
time_to_i = submission.submitted_at.to_i
usec = submission.submitted_at.usec
timestamp = "#{time_to_i}.#{usec}".to_f
quiz_submission.finished_at = Time.at(timestamp)
quiz_submission.save
# get the data in a strange state where the quiz_submission.finished_at is
# microseconds older than the submission (caused the bug in #6048)
quiz_submission.finished_at = Time.at(timestamp + 0.00001)
quiz_submission.save
# verify the data is weird, to_i says they are equal, but the usecs are off
expect(quiz_submission.finished_at.to_i).to eq submission.submitted_at.to_i
expect(quiz_submission.finished_at.usec).to be > submission.submitted_at.usec
# create the versions that Submission#quiz_submission_version uses
quiz_submission.with_versioning do
quiz_submission.save
quiz_submission.save
end
# the real test, quiz_submission_version shouldn't care about usecs
expect(submission.reload.quiz_submission_version).to eq 2
end
it "returns only comments readable by the user" do
course_with_teacher(active_all: true)
@course.default_post_policy.update!(post_manually: false)
@student1 = student_in_course(active_user: true).user
@student2 = student_in_course(active_user: true).user
@assignment = @course.assignments.new(title: "some assignment")
@assignment.submission_types = "online_text_entry"
@assignment.workflow_state = "published"
@assignment.save
@submission = @assignment.submit_homework(@student1, body: "some message")
@submission.add_comment(author: @teacher, comment: "a")
@submission.add_comment(author: @teacher, comment: "b", hidden: true)
@submission.add_comment(author: @student1, comment: "c")
@submission.add_comment(author: @student2, comment: "d")
@submission.add_comment(author: @teacher, comment: "e", draft: true)
@submission.reload
@submission.limit_comments(@teacher)
expect(@submission.submission_comments.count).to be 5
expect(@submission.visible_submission_comments.count).to be 4
@submission.limit_comments(@student1)
expect(@submission.submission_comments.count).to be 4
expect(@submission.visible_submission_comments.count).to be 4
@submission.limit_comments(@student2)
expect(@submission.submission_comments.count).to be 1
expect(@submission.visible_submission_comments.count).to be 1
end
describe "read/unread state" do
it "is read if a submission exists with no grade" do
@submission = @assignment.submit_homework(@user)
expect(@submission.read?(@user)).to be_truthy
end
it "is unread after assignment is graded" do
@submission = @assignment.grade_student(@user, grade: 3, grader: @teacher).first
expect(@submission.unread?(@user)).to be_truthy
end
it "is unread after submission is graded" do
@assignment.submit_homework(@user)
@submission = @assignment.grade_student(@user, grade: 3, grader: @teacher).first
expect(@submission.unread?(@user)).to be_truthy
end
it "is unread after submission is commented on by teacher" do
@student = @user
course_with_teacher(course: @context, active_all: true)
@submission = @assignment.update_submission(@student, { commenter: @teacher, comment: "good!" }).first
expect(@submission.unread?(@user)).to be_truthy
end
it "is read after submission is commented on by teacher and then teacher deletes comment" do
student = @user
submission = @assignment.submission_for_student(@student)
submission.add_comment(author: @teacher, comment: "some comment")
expect(submission.unread?(student)).to be_truthy
content_participation_count = ContentParticipationCount.where(user_id: student.id).first
expect(content_participation_count.unread_count).to eq 1
comment = submission.submission_comments.first
expect do
comment.updating_user = @current_user
comment.destroy!
end.to change { SubmissionComment.count }.from(1).to(0)
expect(submission.read?(student)).to be_truthy
content_participation_count = ContentParticipationCount.where(user_id: student.id).first
expect(content_participation_count.unread_count).to eq 0
end
it "is read after submission is commented on twice by teacher and then teacher deletes the first comment" do
student = @user
submission = @assignment.submission_for_student(student)
submission.add_comment(author: @teacher, comment: "some comment")
expect(submission.unread?(student)).to be_truthy
content_participation_count = ContentParticipationCount.where(user_id: student.id).first
expect(content_participation_count.unread_count).to eq 1
submission.add_comment(author: @teacher, comment: "some comment")
expect(submission.unread?(student)).to be_truthy
content_participation_count = ContentParticipationCount.where(user_id: student.id).first
expect(content_participation_count.unread_count).to eq 1
comment = submission.submission_comments.first
expect do
comment.updating_user = @current_user
comment.destroy!
end.to change { SubmissionComment.count }.from(2).to(1)
expect(submission.unread?(student)).to be_truthy
content_participation_count = ContentParticipationCount.where(user_id: student.id).first
expect(content_participation_count.unread_count).to eq 1
end
it "is read after submission is commented on by teacher, student views comment, teacher comments again, and then teacher deletes the not viewed comment" do
student = @user
submission = @assignment.submission_for_student(student)
submission.add_comment(author: @teacher, comment: "some comment")
expect(submission.unread?(student)).to be_truthy
content_participation_count = ContentParticipationCount.where(user_id: student.id).first
expect(content_participation_count.unread_count).to eq 1
submission.mark_submission_comments_read(student)
submission.mark_item_read("comment")
expect(submission.read?(student)).to be_truthy
content_participation_count = ContentParticipationCount.where(user_id: student.id).first
expect(content_participation_count.unread_count).to eq 0
submission.add_comment(author: @teacher, comment: "some comment")
expect(submission.unread?(student)).to be_truthy
comment = submission.submission_comments[1]
expect do
comment.updating_user = @current_user
comment.destroy!
end.to change { SubmissionComment.count }.from(2).to(1)
expect(submission.read?(student)).to be_truthy
content_participation_count = ContentParticipationCount.where(user_id: student.id).first
expect(content_participation_count.unread_count).to eq 0
end
it "is unread after submission is commented on by teacher, student views comment, teacher comments again, and then teacher deletes the viewed comment" do
student = @user
submission = @assignment.submission_for_student(student)
submission.add_comment(author: @teacher, comment: "some comment")
expect(submission.unread?(student)).to be_truthy
content_participation_count = ContentParticipationCount.where(user_id: student.id).first
expect(content_participation_count.unread_count).to eq 1
submission.mark_submission_comments_read(student)
submission.mark_item_read("comment")
expect(submission.read?(student)).to be_truthy
content_participation_count = ContentParticipationCount.where(user_id: @student.id).first
expect(content_participation_count.unread_count).to eq 0
submission.add_comment(author: @teacher, comment: "some comment")
expect(submission.unread?(student)).to be_truthy
comment = submission.submission_comments.first
expect do
comment.updating_user = @current_user
comment.destroy!
end.to change { SubmissionComment.count }.from(2).to(1)
expect(submission.unread?(student)).to be_truthy
content_participation_count = ContentParticipationCount.where(user_id: student.id).first
expect(content_participation_count.unread_count).to eq 1
end
it "is read if other submission fields change" do
@submission = @assignment.submit_homework(@user)
@submission.workflow_state = "graded"
@submission.graded_at = Time.now
@submission.save!
expect(@submission.read?(@user)).to be_truthy
end
it "mark read/unread" do
@submission = @assignment.submit_homework(@user)
@submission.workflow_state = "graded"
@submission.graded_at = Time.now
@submission.save!
expect(@submission.read?(@user)).to be_truthy
@submission.mark_unread(@user)
expect(@submission.read?(@user)).to be_falsey
@submission.mark_read(@user)
expect(@submission.read?(@user)).to be_truthy
end
it "is unread after submission is graded by teacher" do
@student = @user
@assignment.submit_homework(@student)
@submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
expect(@submission.read?(@student)).to be_falsey
end
it "is unread after submission is graded and commented on by teacher" do
@student = @user
@assignment.submit_homework(@student)
@submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
@submission = @assignment.update_submission(@student, { commenter: @teacher, comment: "good!" }).first
expect(@submission.read?(@student)).to be_falsey
end
it "is unread after grade is read and teacher posts a comment" do
@student = @user
@assignment.submit_homework(@student)
@submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
@submission.mark_item_read("grade")
@submission = @assignment.update_submission(@student, { commenter: @teacher, comment: "good!" }).first
expect(@submission.reload.read?(@student)).to be_falsey
end
it "is read after grade is read and student posts a comment" do
@student = @user
@assignment.submit_homework(@student)
@submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
@submission.mark_item_read("grade")
@submission = @assignment.update_submission(@student, { commenter: @student, comment: "good!" }).first
expect(@submission.reload.read?(@student)).to be_truthy
end
it "is unread after student and teacher post a comment" do
@student = @user
@assignment.submit_homework(@student)
@submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
@assignment.update_submission(@student, { commenter: @student, comment: "good!" })
@assignment.update_submission(@student, { commenter: @teacher, comment: "good!" })
expect(@submission.read?(@student)).to be_falsey
end
it "is unread if there is any unread rubric" do
@student = @user
@assignment.submit_homework(@student)
@submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
ContentParticipation.participate(content: @submission, user: @student, content_item: "rubric")
expect(@submission.read?(@student)).to be_falsey
end
it "is read if grade and rubric are read" do
@student = @user
@assignment.submit_homework(@student)
@submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
ContentParticipation.participate(content: @submission, user: @student, content_item: "rubric")
@submission.mark_item_read("grade")
@submission.mark_item_read("rubric")
expect(@submission.read?(@student)).to be_truthy
end
it "changes the state from read to unread" do
@student = @user
@assignment.submit_homework(@student)
@submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
@assignment.update_submission(@student, { commenter: @teacher, comment: "good!" })
@submission.mark_item_unread("comment")
expect(@submission.unread?(@student)).to be_truthy
end
it "marks submission comments as read" do
@student = @user
@assignment.submit_homework(@student)
@submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
@assignment.update_submission(@student, { commenter: @teacher, comment: "good!" })
@submission.mark_submission_comments_read(@student)
visible_comment = @submission.visible_submission_comments[0]
viewed_comment = visible_comment.viewed_submission_comments[0]
expect(viewed_comment.user).to eql @student
expect(viewed_comment.submission_comment).to eql visible_comment
end
end
describe "mute" do
let(:submission) { Submission.new }
before do
submission.published_score = 100
submission.published_grade = "A"
submission.graded_at = Time.now
submission.grade = "B"
submission.score = 90
submission.mute
end
specify { expect(submission.published_score).to be_nil }
specify { expect(submission.published_grade).to be_nil }
specify { expect(submission.graded_at).to be_nil }
specify { expect(submission.grade).to be_nil }
specify { expect(submission.score).to be_nil }
end
describe "muted_assignment?" do
it "returns true if assignment is muted" do
assignment = double(muted?: true)
@submission = Submission.new
expect(@submission).to receive(:assignment).and_return(assignment)
expect(@submission.muted_assignment?).to be true
end
it "returns false if assignment is not muted" do
assignment = double(muted?: false)
@submission = Submission.new
expect(@submission).to receive(:assignment).and_return(assignment)
expect(@submission.muted_assignment?).to be false
end
end
describe "without_graded_submission?" do
let(:submission) { Submission.new }
it "returns false if submission does not has_submission?" do
allow(submission).to receive_messages(has_submission?: false, graded?: true)
expect(submission.without_graded_submission?).to be false
end
it "returns false if submission does is not graded" do
allow(submission).to receive_messages(has_submission?: true, graded?: false)
expect(submission.without_graded_submission?).to be false
end
it "returns true if submission is not graded and has no submission" do
allow(submission).to receive_messages(has_submission?: false, graded?: false)
expect(submission.without_graded_submission?).to be true
end
end
describe "graded?" do
it "is false before graded" do
submission, _ = @assignment.find_or_create_submission(@user)
expect(submission).to_not be_graded
end
it "is true for graded assignments" do
submission = @assignment.grade_student(@user, grade: 1, grader: @teacher)[0]
expect(submission).to be_graded
end
it "is also true for excused assignments" do
submission, _ = @assignment.find_or_create_submission(@user)
submission.excused = true
expect(submission).to be_graded
end
end
describe "autograded" do
let(:submission) { Submission.new }
it "returns false when its not autograded" do
submission = Submission.new
expect(submission).to_not be_autograded
submission.grader_id = Shard.global_id_for(@user.id)
expect(submission).to_not be_autograded
end
it "returns true when its autograded" do
submission = Submission.new
submission.grader_id = -1
expect(submission).to be_autograded
end
end
describe "past_due" do
before :once do
Auditors::ActiveRecord::Partitioner.process
submission_spec_model
@submission1 = @submission
add_section("overridden section")
u2 = student_in_section(@course_section, active_all: true)
submission_spec_model(user: u2)
@submission2 = @submission
@assignment.update_attribute(:due_at, 1.day.ago)
@submission1.reload
@submission2.reload
end
it "updates when an assignment's due date is changed" do
expect(@submission1).to be_past_due
@assignment.reload.update_attribute(:due_at, 1.day.from_now)
expect(@submission1.reload).not_to be_past_due
end
it "updates when an applicable override is changed" do
expect(@submission1).to be_past_due
expect(@submission2).to be_past_due
assignment_override_model assignment: @assignment,
due_at: 1.day.from_now,
set: @course_section
expect(@submission1.reload).to be_past_due
expect(@submission2.reload).not_to be_past_due
end
it "gives a quiz submission 30 extra seconds before making it past due" do
quiz_with_graded_submission([{ question_data: { :name => "question 1", :points_possible => 1, "question_type" => "essay_question" } }]) do
{
"text_after_answers" => "",
"question_#{@questions[0].id}" => "<p>Lorem ipsum answer.</p>",
"context_id" => @course.id.to_s,
"context_type" => "Course",
"user_id" => @user.id.to_s,
"quiz_id" => @quiz.id.to_s,
"course_id" => @course.id.to_s,
"question_text" => "Lorem ipsum question",
}
end
@assignment.due_at = "20130101T23:59Z"
@assignment.save!
submission = @quiz_submission.submission.reload
submission.write_attribute(:submitted_at, @assignment.due_at + 3.days)
expect(submission).to be_past_due
submission.write_attribute(:submitted_at, @assignment.due_at + 30.seconds)
expect(submission).not_to be_past_due
end
end
describe "late" do
before :once do
submission_spec_model(submit_homework: true)
end
it "is false if not past due" do
@submission.submitted_at = 2.days.ago
@submission.cached_due_date = 1.day.ago
expect(@submission).not_to be_late
end
it "is false if not submitted, even if past due" do
@submission.submission_type = nil
@submission.cached_due_date = 1.day.ago # forces submitted_at to be nil
expect(@submission).not_to be_late
end
it "is true if submitted and past due" do
@submission.submitted_at = 1.day.ago
@submission.cached_due_date = 2.days.ago
expect(@submission).to be_late
end
end
describe "scope: not_submitted_or_graded" do
before do
@assignment = @course.assignments.create!(submission_types: "online_text_entry")
@submission = @assignment.submissions.find_by(user: @student)
end
it "includes submissions where the student has not submitted and has not been graded" do
expect(Submission.not_submitted_or_graded).to include @submission
end
it "excludes submissions where the student has submitted" do
@assignment.submit_homework(@student, body: "hi")
expect(Submission.not_submitted_or_graded).not_to include @submission
end
it "excludes submissions where the student has been graded" do
@assignment.grade_student(@student, grader: @teacher, grade: 10)
expect(Submission.not_submitted_or_graded).not_to include @submission
end
it "excludes excused submissions" do
@assignment.grade_student(@student, grader: @teacher, excused: true)
expect(Submission.not_submitted_or_graded).not_to include @submission
end
end
describe "scope: postable" do
subject(:submissions) { assignment.submissions.postable }
let(:assignment) { @course.assignments.create! }
let(:submission) { assignment.submissions.find_by(user: @student) }
before do
assignment.ensure_post_policy(post_manually: true)
end
it "does not include submissions that neither have grades nor hidden comments" do
submission.add_comment(author: @teacher, comment: "good job!", hidden: false)
expect(subject).not_to include(submission)
end
it "includes submissions with hidden comments" do
submission.add_comment(author: @teacher, comment: "good job!", hidden: true)
expect(subject).to include(submission)
end
it "includes submissions with a grade" do
assignment.grade_student(@student, grader: @teacher, grade: 10)
expect(subject).to include(submission)
end
it "includes submissions that are excused" do
assignment.grade_student(@student, grader: @teacher, excused: true)
expect(subject).to include(submission)
end
end
describe "scope: with_hidden_comments" do
subject(:submissions) { assignment.submissions.with_hidden_comments }
let(:assignment) { @course.assignments.create! }
let(:submission) { assignment.submissions.find_by(user: @student) }
before do
assignment.grade_student(@student, grader: @teacher, score: 5)
end
it "does not include submissions without a hidden comment" do
submission.add_comment(author: @teacher, comment: "good job!", hidden: false)
expect(subject).not_to include(submission)
end
it "includes submissions with hidden comments" do
submission.add_comment(author: @teacher, comment: "good job!", hidden: true)
expect(subject).to include(submission)
end
end
describe "scope: anonymized" do
subject(:submissions) { assignment.all_submissions.anonymized }
let(:assignment) { @course.assignments.create! }
let(:first_student) { @student }
let(:second_student) { student_in_course(course: @course, active_all: true).user }
let(:submission_with_anonymous_id) { submission_model(assignment:, user: first_student) }
let(:submission_without_anonymous_id) do
submission_model(assignment:, user: second_student).tap do |submission|
submission.update_attribute(:anonymous_id, nil)
end
end
it "only contains submissions that have anonymous_ids" do
expect(subject).to contain_exactly(submission_with_anonymous_id)
end
end
describe "scope: due_in_past" do
subject(:submissions) { student.submissions.due_in_past }
let(:future_assignment) { @course.assignments.create!(due_at: 2.days.from_now) }
let(:past_assignment) { @course.assignments.create!(due_at: 2.days.ago) }
let(:whenever_assignment) { @course.assignments.create!(due_at: nil) }
let(:student) { @student }
let(:future_submission) { future_assignment.submission_for_student(student) }
let(:past_submission) { past_assignment.submission_for_student(student) }
let(:whenever_submission) { whenever_assignment.submission_for_student(student) }
it "includes submissions with a due date in the past" do
expect(subject).to include(past_submission)
end
it "excludes submissions with a due date in the future" do
expect(subject).not_to include(future_submission)
end
it "excludes submissions without a due date" do
expect(subject).not_to include(whenever_submission)
end
end
describe "scope: excused" do
before :once do
submission_spec_model
end
it "includes submission when excused is true" do
@submission.update(excused: true)
expect(Submission.excused).to include @submission
end
it "does not include submission when excused is false" do
@submission.update(excused: false)
expect(Submission.excused).not_to include @submission
end
it "does not include the submission when excused is nil" do
@submission.update(excused: nil)
expect(Submission.excused).not_to include @submission
end
end
describe "scope: unposted" do
before :once do
submission_spec_model
end
it "includes submission when posted_at is nil" do
@submission.update(posted_at: nil, grade: 10, score: 10)
expect(Submission.unposted).to include @submission
end
it "does not include submission when posted_at is not nil" do
@submission.update(posted_at: Time.zone.now, grade: 10, score: 10)
expect(Submission.unposted).not_to include @submission
end
end
describe "scope: missing" do
context "not submitted" do
before :once do
@now = Time.zone.now
submission_spec_model(cached_due_date: 1.day.ago(@now), submission_type: nil)
@submission.assignment.update!(submission_types: "online_upload")
end
it "excludes an otherwise missing submission that has been marked with a custom status" do
@submission.update!(grader_id: nil)
admin = account_admin_user(account: @course.root_account)
custom_grade_status = @course.root_account.custom_grade_statuses.create!(
name: "Custom Status",
color: "#ABC",
created_by: admin
)
expect { @submission.update!(custom_grade_status:) }.to change {
Submission.missing.include?(@submission)
}.from(true).to(false)
end
it "includes submission when due date has passed with no submission, late_policy_status is nil, excused is nil and grader is nil" do
@submission.update(grader_id: nil)
expect(Submission.missing).to include @submission
end
it 'includes submission when late_policy_status is "missing"' do
@submission.update(late_policy_status: "missing")
expect(Submission.missing).to include @submission
end
it "includes submission when late_policy_status is not nil, not missing" do
@submission.update(late_policy_status: "none")
expect(Submission.missing).to include @submission
end
it "excludes submission when past due and excused" do
@submission.update(excused: true)
expect(Submission.missing).to be_empty
end
it "excludes submission when past due and extended" do
@submission.update(late_policy_status: "extended")
expect(Submission.missing).to be_empty
end
it "excludes submission when past due and assignment does not expect a submission" do
@submission.assignment.update(submission_types: "none")
expect(Submission.missing).to be_empty
end
it "excludes submission when it is excused and late_policy_status is missing" do
@submission.update(excused: true, late_policy_status: "missing")
expect(Submission.missing).to be_empty
end
it "includes submission when late_policy_status is missing and assignment does not expect a submission" do
@submission.update(late_policy_status: "missing")
@submission.assignment.update(submission_types: "none")
expect(Submission.missing).to include @submission
end
it "excludes submission when due date has not passed" do
@submission.update(cached_due_date: 1.day.from_now(@now))
expect(Submission.missing).to be_empty
end
it "includes missing quiz_lti assignments" do
@course.context_external_tools.create!(
name: "Quizzes.Next",
consumer_key: "test_key",
shared_secret: "test_secret",
tool_id: "Quizzes 2",
url: "http://example.com/launch"
)
@assignment.quiz_lti!
@assignment.due_at = 1.day.ago(@now)
@assignment.save!
@submission.update(grader_id: nil)
expect(Submission.missing).to include @submission
end
end
context "submitted" do
before :once do
@now = Time.zone.now
submission_spec_model(cached_due_date: 1.day.ago(@now), submission_type: nil, submit_homework: true)
@submission.assignment.update!(submission_types: "online_upload")
end
it "excludes submission when late_policy_status is nil" do
expect(Submission.missing).to be_empty
end
it 'includes submission when late_policy_status is "missing"' do
@submission.update(late_policy_status: "missing")
expect(Submission.missing).not_to be_empty
end
it "excludes submission when late_policy_status is not nil, not missing" do
@submission.update(late_policy_status: "foo")
expect(Submission.missing).to be_empty
end
it "excludes submission when late_policy_status is extended" do
@submission.update(late_policy_status: "extended")
expect(Submission.missing).to be_empty
end
it "excludes submission when submitted before the due date" do
@submission.update(submitted_at: 2.days.ago(@now))
expect(Submission.missing).to be_empty
end
it "excludes submission when submitted after the due date" do
@submission.update(submitted_at: @now)
expect(Submission.missing).to be_empty
end
end
end
describe "scope: in_current_grading_period_for_courses" do
before :once do
@term = Account.default.enrollment_terms.create!(start_at: 10.years.ago)
@course.enrollment_term_id = @term.id
@course.save!
period_group = Account.default.grading_period_groups.create!
period_group.enrollment_terms << @course.enrollment_term
now = Time.zone.now
period_group.grading_periods.create!(
title: "Closed Period",
start_date: 5.months.ago(now),
end_date: 2.months.ago(now),
close_date: 2.months.ago(now)
)
period_group.grading_periods.create!(
title: "Current Period",
start_date: 2.months.ago(now),
end_date: 2.months.from_now(now),
close_date: 2.months.from_now(now)
)
@course.assignments.create!(
name: "Assignment in closed period",
workflow_state: "published",
submission_types: "online_text_entry",
due_at: 4.months.ago(now)
)
@course.assignments.create!(
name: "Assignment in current period",
workflow_state: "published",
submission_types: "online_text_entry",
due_at: 1.month.ago
)
end
it "only returns submissions in the current grading period" do
submissions = Submission.in_current_grading_period_for_courses([@course.id])
expect(submissions.map { |s| s.assignment.name }.sort).to eq(["Assignment in current period", "some assignment"].sort)
end
it "includes assignments without a due date" do
@course.assignments.create!(
name: "Assignment without due date",
workflow_state: "published",
submission_types: "online_text_entry"
)
submissions = Submission.in_current_grading_period_for_courses([@course.id])
expect(submissions.count).to be(3)
expect(submissions.map { |s| s.assignment.name }).to include("Assignment without due date")
end
it "includes assignments from all specified courses" do
course1 = @course
course_factory(enrollment_term_id: @term.id, active_all: true)
@course.enroll_student(@student, enrollment_state: :active)
@course.assignments.create!(
name: "Another in current period",
workflow_state: "published",
submission_types: "online_text_entry",
due_at: 1.month.ago
)
submissions = Submission.in_current_grading_period_for_courses([course1.id, @course.id])
expect(submissions.count).to be(3)
expect(submissions.map { |s| s.assignment.name }).to include("Another in current period")
end
it "ignores courses not included in array" do
course_factory(enrollment_term_id: @term.id, active_all: true)
@course.enroll_student(@student, enrollment_state: :active)
@course.assignments.create!(
name: "Another in current period",
workflow_state: "published",
submission_types: "online_text_entry",
due_at: 1.month.ago
)
submissions = Submission.in_current_grading_period_for_courses([@course.id])
expect(submissions.count).to be(1)
expect(submissions.first.assignment.name).to eq("Another in current period")
end
it "includes all submissions from courses without grading periods" do
course1 = @course
course_factory(active_all: true)
@course.enroll_student(@student, enrollment_state: :active)
@course.assignments.create!(
name: "No GP 1",
workflow_state: "published",
submission_types: "online_text_entry",
due_at: 1.month.ago
)
@course.assignments.create!(
name: "No GP 2",
workflow_state: "published",
submission_types: "online_text_entry"
)
submissions = Submission.in_current_grading_period_for_courses([course1.id, @course.id])
expect(submissions.map { |s| s.assignment.name }.sort).to eq(["Assignment in current period", "No GP 1", "No GP 2", "some assignment"].sort)
end
end
describe "#late?" do
before(:once) do
@course = Course.create!
student = User.create!
@course.enroll_student(student, enrollment_state: "active")
now = Time.zone.now
assignment = @course.assignments.create!(submission_types: "online_text_entry", due_at: 10.days.ago(now))
@submission = assignment.submit_homework(student, body: "Submitting late :(")
end
it "returns true if the submission is past due" do
expect(@submission).to be_late
end
it "returns false if the submission is excused" do
@submission.excused = true
expect(@submission).not_to be_late
end
it "returns false if the submission is past due but has its late_policy_status set to something other than 'late'" do
@submission.late_policy_status = "missing"
expect(@submission).not_to be_late
end
it "returns false when an otherwise late submission has a custom status" do
admin = account_admin_user(account: @course.root_account)
custom_grade_status = @course.root_account.custom_grade_statuses.create!(
name: "Custom Status",
color: "#ABC",
created_by: admin
)
expect { @submission.update!(custom_grade_status:) }.to change {
@submission.late?
}.from(true).to(false)
end
end
describe "#extended?" do
before(:once) do
@course = Course.create!
student = User.create!
@course.enroll_student(student, enrollment_state: "active")
assignment = @course.assignments.create!(submission_types: "online_text_entry")
@submission = assignment.submissions.find_by(user: student)
end
it "returns false when a custom status has been applied" do
@submission.update(late_policy_status: "extended")
admin = account_admin_user(account: @course.root_account)
custom_grade_status = @course.root_account.custom_grade_statuses.create!(
name: "Custom Status",
color: "#ABC",
created_by: admin
)
expect { @submission.update!(custom_grade_status:) }.to change {
@submission.extended?
}.from(true).to(false)
end
end
describe "#missing" do
before :once do
@now = Time.zone.now
submission_spec_model(cached_due_date: 1.day.ago(@now), submission_type: nil, submit_homework: true)
@submission.assignment.update!(submission_types: "on_paper")
@another_assignment = assignment_model(course: @course, due_at: 1.day.ago)
@another_submission = @another_assignment.submissions.last
end
submissions_that_cant_be_missing = %w[none on_paper external_tool]
%w[none
on_paper
online_quiz
discussion_topic
external_tool
online_upload
online_text_entry
online_url
media_recording].each do |sub_type|
should_not_be_missing = submissions_that_cant_be_missing.include?(sub_type)
expected_status = should_not_be_missing ? "false" : "true"
it "returns #{expected_status} when late_policy_status is nil and submission_type is #{sub_type}" do
@another_assignment.update(submission_types: sub_type)
if should_not_be_missing
expect(@another_submission.reload).not_to be_missing
else
expect(@another_submission.reload).to be_missing
end
end
end
it "returns false when an otherwise missing submission has a custom status" do
@another_assignment.update!(submission_types: "online_upload")
admin = account_admin_user(account: @course.root_account)
custom_grade_status = @course.root_account.custom_grade_statuses.create!(
name: "Custom Status",
color: "#ABC",
created_by: admin
)
expect { @another_submission.update!(custom_grade_status:) }.to change {
@another_submission.missing?
}.from(true).to(false)
end
it "returns false when late_policy_status is nil standalone" do
expect(@submission).not_to be_missing
end
it 'returns true when late_policy_status is "missing"' do
@submission.update(late_policy_status: "missing")
expect(@submission).to be_missing
end
it 'returns false when the submission is excused and late_policy_status is "missing"' do
@submission.excused = true
@submission.late_policy_status = "missing"
expect(@submission).not_to be_missing
end
it "returns false when late_policy_status is not nil, not missing" do
@submission.update(late_policy_status: "late")
expect(@submission).not_to be_missing
end
it "returns false when not past due" do
@submission.update(submitted_at: 2.days.ago(@now))
expect(@submission).not_to be_missing
end
it "returns false when past due and submitted" do
@submission.update(submitted_at: @now)
expect(@submission).not_to be_missing
end
it "returns false when past due, not submitted, assignment does not expect a submission, is excused" do
@submission.assignment.update(submission_types: "none")
@submission.update(excused: true)
@submission.update_columns(submission_type: nil)
expect(@submission).not_to be_missing
end
it "returns false when past due, not submitted, assignment does not expect a submission, not excused, and no score" do
@submission.assignment.update(submission_types: "none")
@submission.update_columns(submission_type: nil)
expect(@submission).not_to be_missing
end
it 'returns false when past due, not submitted, assignment does not expect a submission, not excused, has a score, workflow state is not "graded"' do
@submission.update(score: 1)
@submission.update_columns(submission_type: nil)
expect(@submission).not_to be_missing
end
it 'returns false when past due, not submitted, assignment does not expect a submission, not excused, has a score, workflow state is "graded", and score is 0' do
@submission.update(score: 0, workflow_state: "graded")
@submission.update_columns(submission_type: nil)
expect(@submission).not_to be_missing
end
it 'returns false when past due, not submitted, assignment does not expect a submission, not excused, has a score, workflow state is "graded", and score is greater than 0' do
@submission.update(score: 1, workflow_state: "graded")
@submission.update_columns(submission_type: nil)
expect(@submission).not_to be_missing
end
it "returns true for missing quiz_lti submissions" do
@course.context_external_tools.create!(
name: "Quizzes.Next",
consumer_key: "test_key",
shared_secret: "test_secret",
tool_id: "Quizzes 2",
url: "http://example.com/launch"
)
@another_assignment.quiz_lti!
@another_assignment.save!
@another_submission.reload
expect(@another_submission).to be_missing
end
it "returns true for missing quiz_lti submissions when cached_quiz_lti is false but assignment.quiz_lti is true" do
@course.context_external_tools.create!(
name: "Quizzes.Next",
consumer_key: "test_key",
shared_secret: "test_secret",
tool_id: "Quizzes 2",
url: "http://example.com/launch"
)
@another_assignment.quiz_lti!
@another_assignment.save!
@another_submission.reload
@another_submission.update!(cached_quiz_lti: false)
expect(@another_submission).to be_missing
end
end
describe "update_attachment_associations" do
before do
course_with_student active_all: true
@assignment = @course.assignments.create!
end
it "doesn't include random attachment ids" do
f = Attachment.create! uploaded_data: StringIO.new("blah"),
context: @course,
filename: "blah.txt"
sub = @assignment.submit_homework(@user, attachments: [f])
expect(sub.attachments).to eq []
end
it "includes attachments in a user group that are not in a section group" do
@group = @course.groups.create!
@group.add_user(@user)
f = Attachment.create! uploaded_data: StringIO.new("blah"),
context: @group,
filename: "blah.txt",
user: @user
sub = @assignment.submit_homework(@user, attachments: [f])
expect(sub.attachments).to eq [f]
end
end
describe "versioned_attachments" do
it "includes user attachments" do
student_in_course(active_all: true)
att = attachment_model(filename: "submission.doc", context: @student)
sub = @assignment.submit_homework(@student, attachments: [att])
expect(sub.versioned_attachments).to eq [att]
end
it "does not include attachments with a context of Submission" do
student_in_course(active_all: true)
att = attachment_model(filename: "submission.doc", context: @student)
sub = @assignment.submit_homework(@student, attachments: [att])
sub.attachments.update_all(context_type: "Submission", context_id: sub.id)
expect(sub.reload.versioned_attachments).to be_empty
end
it "includes attachments owned by other users in a group for a group submission" do
student1, student2 = n_students_in_course(2, { course: @course })
assignment = @course.assignments.create!(name: "A1", submission_types: "online_upload")
group_category = @course.group_categories.create!(name: "Project Groups")
group = group_category.groups.create!(name: "A Team", context: @course)
group.add_user(student1)
group.add_user(student2)
assignment.update(group_category:)
user_attachment = attachment_model(context: student1)
assignment.submit_homework(student1, submission_type: "online_upload", attachments: [user_attachment])
[student1, student2].each do |student|
submission = assignment.submission_for_student(student)
submission.versioned_attachments
expect(submission.versioned_attachments).to include user_attachment
end
end
it "includes attachments uploaded from group by user without matching section id" do
@group = @course.groups.create!
@group.add_user(@user)
f = Attachment.create! uploaded_data: StringIO.new("blah"),
context: @group,
filename: "blah.txt",
user: @user
sub = @assignment.submit_homework(@user, attachments: [f])
expect(sub.versioned_attachments).to eq [f]
end
end
describe "includes_attachment?" do
it "includes current attachments" do
spoiler = attachment_model(context: @student)
attachment_model context: @student
sub = @assignment.submit_homework @student, attachments: [@attachment]
expect(sub.attachments).to eq([@attachment])
expect(sub.includes_attachment?(spoiler)).to be false
expect(sub.includes_attachment?(@attachment)).to be true
end
it "includes attachments to previous versions" do
old_attachment_1 = attachment_model(context: @student)
old_attachment_2 = attachment_model(context: @student)
@assignment.submit_homework @student, attachments: [old_attachment_1, old_attachment_2]
attachment_model context: @student
sub = @assignment.submit_homework @student, attachments: [@attachment]
expect(sub.attachments.to_a).to eq([@attachment])
expect(sub.includes_attachment?(old_attachment_1)).to be true
expect(sub.includes_attachment?(old_attachment_2)).to be true
end
end
describe "#versioned_originality_reports" do
it "loads originality reports for the submission" do
student_in_course(active_all: true)
attachment = attachment_model(filename: "submission.doc", context: @student)
submission = @assignment.submit_homework(@student, attachments: [attachment])
report = OriginalityReport.create!(attachment:, originality_score: "1", submission:)
expect(submission.versioned_originality_reports).to eq [report]
end
it "memoizes the loaded originality reports" do
student_in_course(active_all: true)
attachment = attachment_model(filename: "submission.doc", context: @student)
submission = @assignment.submit_homework(@student, attachments: [attachment])
OriginalityReport.create!(attachment:, originality_score: "1", submission:)
submission.versioned_originality_reports
expect(OriginalityReport).not_to receive(:where)
submission.versioned_originality_reports
end
it "returns an empty array when there are no reports" do
student_in_course(active_all: true)
attachment = attachment_model(filename: "submission.doc", context: @student)
submission = @assignment.submit_homework(@student, attachments: [attachment])
expect(submission.versioned_originality_reports).to eq []
end
it "returns an empty array when there are no attachments" do
student_in_course(active_all: true)
submission = @assignment.submit_homework(@student, body: "Oh my!")
expect(submission.versioned_originality_reports).to eq []
end
it "works correctly on originality reports without submission times with multiple text entry or same attachment ids" do
reports = []
submissions = (1..3).map do |i|
sub = @assignment.submit_homework(@student, body: "body #{i}")
report = OriginalityReport.create!(attachment: nil, originality_score: i, submission: sub)
report.update_columns(submission_time: nil)
reports << report
sub
end
attachment = attachment_model(filename: "submission.doc", context: @student)
submissions += (1..3).map do |i|
sub = @assignment.submit_homework(@student, attachments: [attachment])
report = OriginalityReport.create!(attachment:, originality_score: i, submission: sub)
report.update_columns(submission_time: nil)
reports << report
sub
end
submissions[0..2].each_with_index do |s, i|
expect(s.versioned_originality_reports).to match_array reports[i..2]
end
submissions[3..].each_with_index do |s, i|
expect(s.versioned_originality_reports).to match_array reports[(i + 3)..]
end
end
end
describe "#bulk_load_versioned_originality_reports" do
before :once do
student_in_course(active_all: true)
end
it "bulk loads originality reports for many submissions at once" do
originality_reports = []
submissions = Array.new(3) do |i|
student_in_course(active_all: true)
attachments = [
attachment_model(filename: "submission#{i}-a.doc", context: @student),
attachment_model(filename: "submission#{i}-b.doc", context: @student)
]
sub = @assignment.submit_homework(@student, attachments:)
originality_reports << attachments.map do |a|
OriginalityReport.create!(attachment: a, originality_score: "1", submission: sub)
end
sub
end
Submission.bulk_load_versioned_originality_reports(submissions)
submissions.each_with_index do |s, i|
expect(s.versioned_originality_reports).to eq originality_reports[i]
end
end
it "avoids N+1s in the bulk load" do
attachment = attachment_model(filename: "submission.doc", context: @student)
submission = @assignment.submit_homework(@student, attachments: [attachment])
OriginalityReport.create!(attachment:, originality_score: "1", submission:)
Submission.bulk_load_versioned_originality_reports([submission])
expect(OriginalityReport).not_to receive(:where)
submission.versioned_originality_reports
end
it "ignores invalid attachment ids" do
s = @assignment.submit_homework(@student, submission_type: "online_url", url: "http://example.com")
s.update_attribute(:attachment_ids, "99999999")
Submission.bulk_load_versioned_originality_reports([s])
expect(s.versioned_originality_reports).to eq []
end
it "loads only the originality reports that pertain to that version" do
originality_reports = []
attachment = attachment_model(filename: "submission-a.doc", context: @student)
Timecop.freeze(10.seconds.ago) do
sub = @assignment.submit_homework(@student, submission_type: "online_upload", attachments: [attachment])
originality_reports <<
OriginalityReport.create!(attachment:, originality_score: "1", submission: sub)
end
attachment = attachment_model(filename: "submission-b.doc", context: @student)
Timecop.freeze(5.seconds.ago) do
sub = @assignment.submit_homework(@student, attachments: [attachment])
originality_reports <<
OriginalityReport.create!(attachment:, originality_score: "1", submission: sub)
end
attachment = attachment_model(filename: "submission-c.doc", context: @student)
Timecop.freeze(1.second.ago) do
sub = @assignment.submit_homework(@student, attachments: [attachment])
originality_reports <<
OriginalityReport.create!(attachment:, originality_score: "1", submission: sub)
end
submission = @assignment.submission_for_student(@student)
Submission.bulk_load_versioned_originality_reports(submission.submission_history)
submission.submission_history.each_with_index do |s, index|
expect(s.versioned_originality_reports.first).to eq originality_reports[index]
end
end
it "works with unsubmitted submissions" do
submissions = @assignment.submissions.where(user: @student)
Submission.bulk_load_versioned_originality_reports(submissions)
submissions.each do |s|
expect(s.versioned_originality_reports).to eq []
end
end
it "works correctly on originality reports without submission times with multiple text entry or same attachment ids" do
reports = []
submissions = (1..3).map do |i|
sub = @assignment.submit_homework(@student, body: "body #{i}")
report = OriginalityReport.create!(attachment: nil, originality_score: i, submission: sub)
report.update_columns(submission_time: nil)
reports << report
sub
end
attachment = attachment_model(filename: "submission.doc", context: @student)
submissions += (1..3).map do |i|
sub = @assignment.submit_homework(@student, attachments: [attachment])
report = OriginalityReport.create!(attachment:, originality_score: i, submission: sub)
report.update_columns(submission_time: nil)
reports << report
sub
end
Submission.bulk_load_versioned_originality_reports(submissions)
submissions[0..2].each_with_index do |s, i|
expect(s.versioned_originality_reports).to match_array reports[i..2]
end
submissions[3..].each_with_index do |s, i|
expect(s.versioned_originality_reports).to match_array reports[(i + 3)..]
end
end
end
context "bulk loading attachments" do
def ensure_attachments_arent_queried
expect(Attachment).not_to receive(:where)
end
def submission_for_some_user
student_in_course active_all: true
@assignment.submit_homework(@student,
submission_type: "online_url",
url: "http://example.com")
end
describe "#bulk_load_versioned_attachments" do
it "loads attachments for many submissions at once" do
attachments = []
submissions = Array.new(3) do |i|
student_in_course(active_all: true)
attachments << [
attachment_model(filename: "submission#{i}-a.doc", context: @student),
attachment_model(filename: "submission#{i}-b.doc", context: @student)
]
@assignment.submit_homework @student, attachments: attachments[i]
end
Submission.bulk_load_versioned_attachments(submissions)
ensure_attachments_arent_queried
submissions.each_with_index do |s, i|
expect(s.versioned_attachments).to eq attachments[i]
end
end
it "filters out deleted attachments" do
student = student_in_course(active_all: true).user
attachment = attachment_model(filename: "submission.doc", context: student)
submission = @assignment.submit_homework(student, attachments: [attachment])
attachment.destroy_permanently!
submission_with_attachments = Submission.bulk_load_versioned_attachments([submission]).first
expect(submission_with_attachments.versioned_attachments).to be_empty
end
it "includes url submission attachments" do
s = submission_for_some_user
s.attachment = attachment_model(filename: "screenshot.jpg",
context: @student)
Submission.bulk_load_versioned_attachments([s])
ensure_attachments_arent_queried
expect(s.versioned_attachments).to eq [s.attachment]
end
it "handles bad data" do
s = submission_for_some_user
s.update_attribute(:attachment_ids, "99999999")
Submission.bulk_load_versioned_attachments([s])
expect(s.versioned_attachments).to eq []
end
it "handles submission histories with different attachments" do
student_in_course(active_all: true)
attachments = [attachment_model(filename: "submission-a.doc", context: @student)]
Timecop.freeze(10.seconds.ago) do
@assignment.submit_homework(@student,
submission_type: "online_upload",
attachments: [attachments[0]])
end
attachments << attachment_model(filename: "submission-b.doc", context: @student)
Timecop.freeze(5.seconds.ago) do
@assignment.submit_homework @student, attachments: [attachments[1]]
end
attachments << attachment_model(filename: "submission-c.doc", context: @student)
Timecop.freeze(1.second.ago) do
@assignment.submit_homework @student, attachments: [attachments[2]]
end
submission = @assignment.submission_for_student(@student)
Submission.bulk_load_versioned_attachments(submission.submission_history)
submission.submission_history.each_with_index do |s, index|
expect(s.attachment_ids.to_i).to eq attachments[index].id
end
end
end
describe "#bulk_load_attachments_for_submissions" do
it "loads attachments for many submissions at once and returns a hash" do
expected_attachments_for_submissions = {}
submissions = Array.new(3) do |i|
student_in_course(active_all: true)
attachment = [attachment_model(filename: "submission#{i}.doc", context: @student)]
sub = @assignment.submit_homework @student, attachments: attachment
expected_attachments_for_submissions[sub] = attachment
sub
end
result = Submission.bulk_load_attachments_for_submissions(submissions)
ensure_attachments_arent_queried
expect(result).to eq(expected_attachments_for_submissions)
end
it "handles bad data" do
s = submission_for_some_user
s.update_attribute(:attachment_ids, "99999999")
expected_attachments_for_submissions = { s => [] }
result = Submission.bulk_load_attachments_for_submissions(s)
expect(result).to eq(expected_attachments_for_submissions)
end
it "filters out attachment associations that don't point to an attachment" do
student = student_in_course(active_all: true).user
attachment = attachment_model(filename: "submission.doc", context: student)
submission = @assignment.submit_homework(student, attachments: [attachment])
submission.attachment_associations.find_by(attachment_id: attachment.id).update!(attachment_id: nil)
attachments = Submission.bulk_load_attachments_for_submissions([submission]).first.second
expect(attachments).to be_empty
end
it "filters out attachment associations that point to deleted attachments" do
student = student_in_course(active_all: true).user
attachment = attachment_model(filename: "submission.doc", context: student)
submission = @assignment.submit_homework(student, attachments: [attachment])
attachment.destroy_permanently!
attachments = Submission.bulk_load_attachments_for_submissions([submission]).first.second
expect(attachments).to be_empty
end
it "includes valid attachments and filters out deleted attachments" do
student = student_in_course(active_all: true).user
attachment = attachment_model(filename: "submission.doc", context: student)
submission = @assignment.submit_homework(student, attachments: [attachment])
attachment.destroy_permanently!
another_student = student_in_course(active_all: true).user
another_attachment = attachment_model(filename: "submission.doc", context: another_student)
another_submission = @assignment.submit_homework(another_student, attachments: [another_attachment])
bulk_loaded_submissions = Submission.bulk_load_attachments_for_submissions([submission, another_submission])
submission_attachments = bulk_loaded_submissions.find { |s| s.first.id == submission.id }.second
expect(submission_attachments).to be_empty
another_submission_attachments = bulk_loaded_submissions.find { |s| s.first.id == another_submission.id }.second
expect(another_submission_attachments).not_to be_empty
end
end
end
describe "#assign_assessor" do
def peer_review_assignment
assignment = @course.assignments.build(title: "Peer review",
due_at: Time.now - 1.day,
points_possible: 5,
submission_types: "online_text_entry")
assignment.peer_reviews_assigned = true
assignment.peer_reviews = true
assignment.automatic_peer_reviews = true
assignment.save!
assignment
end
before do
student_in_course(active_all: true)
@student2 = user_factory
@student2_enrollment = @course.enroll_student(@student2)
@student2_enrollment.accept!
@assignment = peer_review_assignment
@student1_homework = @assignment.submit_homework(@student, body: "Lorem ipsum dolor")
@student2_homework = @assignment.submit_homework(@student2, body: "Sit amet consectetuer")
end
it "sends a reminder notification" do
expect_any_instance_of(AssessmentRequest).to receive(:send_reminder!).once
submission1, submission2 = @assignment.submissions
submission1.assign_assessor(submission2)
end
it "does not allow read access when assignment's peer reviews are off" do
@student1_homework.assign_assessor(@student2_homework)
expect(@student1_homework.grants_right?(@student2, :read)).to be true
@assignment.peer_reviews = false
@assignment.save!
@student1_homework.reload
AdheresToPolicy::Cache.clear
expect(@student1_homework.grants_right?(@student2, :read)).to be false
end
it "does not allow read access when other student's enrollment is not active" do
@student2_homework.assign_assessor(@student1_homework)
@student2_enrollment.conclude
expect(@student2_homework.grants_right?(@student, :read)).to be false
end
end
describe "#get_web_snapshot" do
it "does not blow up if web snapshotting fails" do
sub = submission_spec_model
expect(CutyCapt).to receive(:enabled?).and_return(true)
expect(CutyCapt).to receive(:snapshot_attachment_for_url).with(sub.url, context: sub).and_return(nil)
sub.get_web_snapshot
end
end
describe "capturing screenshots for online_url submissions" do
let_once(:course) { Course.create! }
let_once(:student) { course.enroll_student(User.create!, active_all: true).user }
let(:assignment) { course.assignments.create!(submission_types: ["online_url"]) }
let(:sub) { assignments.find_by(user: student) }
let(:submitted_url) { "https://example.com" }
let(:get_web_snapshot_jobs) { Delayed::Job.where(tag: "Submission#get_web_snapshot").order(:id) }
before do
allow(CutyCapt).to receive(:enabled?).and_return(true)
end
it "calls #get_web_snapshot when it's the first submission attempt" do
expect do
assignment.submit_homework(student, submission_type: "online_url", url: submitted_url)
end.to change {
get_web_snapshot_jobs.count
}.by(1)
end
it "calls #get_web_snapshot when it's not the first submission attempt" do
assignment.submit_homework(student, submission_type: "online_url", url: submitted_url)
expect do
assignment.submit_homework(student, submission_type: "online_url", url: "https://example.com/different")
end.to change {
get_web_snapshot_jobs.count
}.by(1)
end
it "calls #get_web_snapshot when it's not the first submission attempt and the url hasn't changed" do
assignment.submit_homework(student, submission_type: "online_url", url: submitted_url)
expect do
assignment.submit_homework(student, submission_type: "online_url", url: submitted_url)
end.to change {
get_web_snapshot_jobs.count
}.by(1)
end
it "does not call #get_web_snapshot when a url is not included" do
expect do
assignment.submit_homework(student, submission_type: "online_url", url: nil)
end.not_to change {
get_web_snapshot_jobs.count
}
end
end
describe "#submit_attachments_to_canvadocs" do
it "creates crocodoc documents" do
allow(Canvas::Crocodoc).to receive(:enabled?).and_return true
s = @assignment.submit_homework(@user,
submission_type: "online_text_entry",
body: "hi")
# creates crocodoc documents
a1 = crocodocable_attachment_model context: @user
s.attachments = [a1]
s.save
cd = a1.crocodoc_document
expect(cd).not_to be_nil
# shouldn't mess with existing crocodoc documents
a2 = crocodocable_attachment_model context: @user
s.attachments = [a1, a2]
s.save
expect(a1.reload_crocodoc_document).to eq cd
expect(a2.crocodoc_document).to eq a2.crocodoc_document
end
context "canvadocs_submissions records" do
before(:once) do
@student1, @student2 = n_students_in_course(2)
@attachment = crocodocable_attachment_model(context: @student1)
@assignment = @course.assignments.create! name: "A1",
submission_types: "online_upload"
end
before do
allow(Canvadocs).to receive_messages(enabled?: true, annotations_supported?: true, config: nil)
end
it "ties submissions to canvadocs" do
s = @assignment.submit_homework(@student1,
submission_type: "online_upload",
attachments: [@attachment])
expect(@attachment.canvadoc.submissions).to eq [s]
end
context "preferred_plugins" do
it "does not send o365 as preferred plugins by default" do
@assignment.submit_homework(@student1,
submission_type: "online_upload",
attachments: [@attachment])
job = Delayed::Job.where(strand: "canvadocs").last
expect(job.payload_object.kwargs[:preferred_plugins]).to eq [
Canvadocs::RENDER_PDFJS,
Canvadocs::RENDER_BOX,
Canvadocs::RENDER_CROCODOC
]
end
it "sends o365 as a preferred plugin when the 'Prefer Office 365 file viewer' account setting is enabled" do
@assignment.context.root_account.settings[:canvadocs_prefer_office_online] = true
@assignment.context.root_account.save!
@assignment.submit_homework(@student1,
submission_type: "online_upload",
attachments: [@attachment])
job = Delayed::Job.where(strand: "canvadocs").last
expect(job.payload_object.kwargs[:preferred_plugins]).to eq [
Canvadocs::RENDER_O365,
Canvadocs::RENDER_PDFJS,
Canvadocs::RENDER_BOX,
Canvadocs::RENDER_CROCODOC
]
end
end
it "create records for each group submission" do
gc = @course.group_categories.create! name: "Project Groups"
group = gc.groups.create! name: "A Team", context: @course
group.add_user(@student1)
group.add_user(@student2)
@assignment.update_attribute :group_category, gc
@assignment.submit_homework(@student1,
submission_type: "online_upload",
attachments: [@attachment])
[@student1, @student2].each do |student|
submission = @assignment.submission_for_student(student)
expect(@attachment.canvadoc.submissions).to include submission
end
end
end
it "doesn't create jobs for non-previewable documents" do
job_scope = Delayed::Job.where(strand: "canvadocs")
orig_job_count = job_scope.count
attachment = attachment_model(context: @user)
@assignment.submit_homework(@user,
submission_type: "online_upload",
attachments: [attachment])
expect(job_scope.count).to eq orig_job_count
end
end
describe "#annotation_context" do
before(:once) do
@attachment = attachment_model(context: @user)
@assignment.update!(annotatable_attachment_id: @attachment.id)
@submission = @assignment.submissions.find_by(user: @user)
end
it "creates a canvadocs_annotation_context if draft is true" do
new_student = @course.enroll_student(User.create!).user
new_submission = @assignment.submissions.find_by(user: new_student)
expect do
new_submission.annotation_context(draft: true)
end.to change {
new_submission.canvadocs_annotation_contexts.where(attachment: @attachment, submission_attempt: nil).count
}.by(1)
end
it "returns the already existing canvadocs_annotation_context when passed draft multiple times" do
existing_context = @submission.annotation_context(draft: true)
expect(@submission.annotation_context(draft: true)).to eq existing_context
end
it "returns nil if a canvadocs_annotation_context does not exist" do
expect(@submission.annotation_context(attempt: 1)).to be_nil
end
it "returns the annotation_context if one exists for the attempt" do
@submission.update!(attempt: 1)
context = @submission.canvadocs_annotation_contexts.create!(
attachment: @attachment,
submission_attempt: @submission.attempt
)
expect(@submission.annotation_context(attempt: @submission.attempt)).to eq context
end
end
describe ".process_bulk_update" do
before(:once) do
course_with_teacher active_all: true
@u1, @u2 = n_students_in_course(2)
@a1, @a2 = Array.new(2) do
@course.assignments.create! points_possible: 10
end
@progress = Progress.create!(context: @course, tag: "submissions_update")
end
it "updates submissions on an assignment" do
Submission.process_bulk_update(@progress, @course, nil, @teacher, {
@a1.id.to_s => {
@u1.id => { posted_grade: 5 },
@u2.id => { posted_grade: 10 }
}
})
expect(@a1.submission_for_student(@u1).grade).to eql "5"
expect(@a1.submission_for_student(@u2).grade).to eql "10"
end
it "only recalculates scores for users with changed submissions" do
data1 = { @a1.id.to_s => { @u1.id => { posted_grade: 5 }, @u2.id => { posted_grade: 10 } } }
data2 = { @a1.id.to_s => { @u1.id => { posted_grade: 5 }, @u2.id => { posted_grade: 11 } } } # leave u1 the same
Submission.process_bulk_update(@progress, @course, nil, @teacher, data1)
expect_any_instantiation_of(@course).to receive(:recompute_student_scores).with([@u2.id])
Submission.process_bulk_update(@progress, @course, nil, @teacher, data2)
end
it "updates submissions on multiple assignments" do
Submission.process_bulk_update(@progress, @course, nil, @teacher, {
@a1.id => {
@u1.id => { posted_grade: 5 },
@u2.id => { posted_grade: 10 }
},
@a2.id.to_s => {
@u1.id => { posted_grade: 10 },
@u2.id => { posted_grade: 5 }
}
})
expect(@a1.submission_for_student(@u1).grade).to eql "5"
expect(@a1.submission_for_student(@u2).grade).to eql "10"
expect(@a2.submission_for_student(@u1).grade).to eql "10"
expect(@a2.submission_for_student(@u2).grade).to eql "5"
end
it "maintains grade when only updating comments" do
@a1.grade_student(@u1, grade: 3, grader: @teacher)
Submission.process_bulk_update(@progress,
@course,
nil,
@teacher,
{
@a1.id => {
@u1.id => { text_comment: "comment" }
}
})
expect(@a1.submission_for_student(@u1).grade).to eql "3"
end
it "nils grade when receiving empty posted_grade" do
@a1.grade_student(@u1, grade: 3, grader: @teacher)
Submission.process_bulk_update(@progress,
@course,
nil,
@teacher,
{
@a1.id => {
@u1.id => { posted_grade: nil }
}
})
expect(@a1.submission_for_student(@u1).grade).to be_nil
end
it "does not explode if the assignment is deleted" do
@a1.destroy
expect do
Submission.process_bulk_update(@progress, @course, nil, @teacher, {
@a1.id.to_s => {
@u1.id => { posted_grade: 5 },
@u2.id => { posted_grade: 10 }
}
})
end.to_not raise_error
expect(@progress.reload.failed?).to be_truthy
expect(@a1.submission_for_student(@u1).grade).to be_nil
expect(@a1.submission_for_student(@u2).grade).to be_nil
end
it "does not update grader_id if submission is blank or missing with -" do
Submission.process_bulk_update(@progress,
@course,
nil,
@teacher,
{
@a1.id => {
@u1.id => { posted_grade: nil }
},
@a1.id => {
@u2.id => { posted_grade: "-" }
}
})
submission1 = @a1.submission_for_student(@u1)
submission2 = @a1.submission_for_student(@u2)
expect(submission1.grade).to be_nil
expect(submission2.grade).to be_nil
expect(submission1.grader_id).to be_nil
expect(submission2.grader_id).to be_nil
end
describe "submitting comments via bulk update" do
let(:auto_assignment) { @a1 }
let(:manual_assignment) do
@a2.post_policy.update!(post_manually: true)
@a2
end
it "sets the comment to visible if the assignment is automatically posted" do
Submission.process_bulk_update(@progress, @course, nil, @teacher, {
auto_assignment.id.to_s => {
@u1.id => { text_comment: "hello there" }
}
})
comment = auto_assignment.submission_for_student(@u1).submission_comments.last
expect(comment).not_to be_hidden
end
it "sets the comment to visible if the relevant submission has already been posted" do
auto_assignment.grade_student(@u1, grade: 0, grader: @teacher)
Submission.process_bulk_update(@progress, @course, nil, @teacher, {
auto_assignment.id.to_s => {
@u1.id => { text_comment: "hello there" }
}
})
comment = auto_assignment.submission_for_student(@u1).submission_comments.last
expect(comment).not_to be_hidden
end
it "sets the comment to visible if a grade is also included in the update" do
Submission.process_bulk_update(@progress, @course, nil, @teacher, {
auto_assignment.id.to_s => {
@u1.id => { posted_grade: 0, text_comment: "hello there" }
}
})
comment = auto_assignment.submission_for_student(@u1).submission_comments.last
expect(comment).not_to be_hidden
end
context "for a manually-posted assignment" do
let(:submission) { manual_assignment.submission_for_student(@u1) }
it "shows the comment if the associated submission is already posted" do
manual_assignment.post_submissions(submission_ids: [submission.id])
Submission.process_bulk_update(@progress, @course, nil, @teacher, {
manual_assignment.id.to_s => {
@u1.id => { text_comment: "hello there" }
}
})
expect(submission.submission_comments.last).not_to be_hidden
end
it "leaves the comment hidden if the associated submission is not posted" do
Submission.process_bulk_update(@progress, @course, nil, @teacher, {
manual_assignment.id.to_s => {
@u1.id => { text_comment: "clandestine comment" }
}
})
expect(submission.submission_comments.last).to be_hidden
end
end
end
end
describe "find_or_create_provisional_grade!" do
before(:once) do
@assignment.grader_count = 1
@assignment.moderated_grading = true
@assignment.final_grader = @teacher
@assignment.save!
submission_spec_model
@teacher2 = User.create(name: "some teacher 2")
@context.enroll_teacher(@teacher2)
end
context "when force_save is true" do
it do
expect { @submission.find_or_create_provisional_grade!(@teacher, force_save: true) }
.to change { AnonymousOrModerationEvent.provisional_grade_created.count }.by(1)
end
it do
expect { @submission.find_or_create_provisional_grade!(@teacher, force_save: true) }
.to_not change { AnonymousOrModerationEvent.provisional_grade_updated.count }
end
context "given an existing provisional grade" do
before(:once) { @submission.find_or_create_provisional_grade!(@teacher, force_save: true) }
it do
expect { @submission.find_or_create_provisional_grade!(@teacher, force_save: true) }
.to change { AnonymousOrModerationEvent.provisional_grade_updated.count }.by(1)
end
it do
expect { @submission.find_or_create_provisional_grade!(@teacher, force_save: true) }
.not_to change { AnonymousOrModerationEvent.provisional_grade_created.count }
end
end
end
it "properly creates a provisional grade with all default values but scorer" do
@submission.find_or_create_provisional_grade!(@teacher)
expect(@submission.provisional_grades.length).to be 1
pg = @submission.provisional_grades.first
expect(pg.scorer_id).to eql @teacher.id
expect(pg.final).to be false
expect(pg.graded_anonymously).to be_nil
expect(pg.grade).to be_nil
expect(pg.score).to be_nil
expect(pg.source_provisional_grade).to be_nil
end
it "properly amends information to an existing provisional grade" do
@submission.find_or_create_provisional_grade!(@teacher)
@submission.find_or_create_provisional_grade!(
@teacher,
score: 15.0,
grade: "20",
graded_anonymously: true
)
expect(@submission.provisional_grades.length).to be 1
pg = @submission.provisional_grades.first
expect(pg.scorer_id).to eql @teacher.id
expect(pg.final).to be false
expect(pg.graded_anonymously).to be true
expect(pg.grade).to eql "20"
expect(pg.score).to be 15.0
expect(pg.source_provisional_grade).to be_nil
end
it "computes provisional grade grade if not given" do
@submission.find_or_create_provisional_grade!(@teacher)
@submission.find_or_create_provisional_grade!(
@teacher,
score: 15.0
)
expect(@submission.provisional_grades.length).to be 1
pg = @submission.provisional_grades.first
expect(pg.grade).to eql "15"
end
it "does not update grade or score if not given" do
@submission.find_or_create_provisional_grade!(@teacher, grade: "20", score: 12.0)
expect(@submission.provisional_grades.first.grade).to eql "20"
expect(@submission.provisional_grades.first.score).to be 12.0
@submission.find_or_create_provisional_grade!(@teacher)
expect(@submission.provisional_grades.first.grade).to eql "20"
expect(@submission.provisional_grades.first.score).to be 12.0
end
it "does not update graded_anonymously if not given" do
@submission.find_or_create_provisional_grade!(@teacher, graded_anonymously: true)
expect(@submission.provisional_grades.first.graded_anonymously).to be true
@submission.find_or_create_provisional_grade!(@teacher)
expect(@submission.provisional_grades.first.graded_anonymously).to be true
end
it "raises an exception if final is true and user is not allowed to select final grade" do
expect { @submission.find_or_create_provisional_grade!(@student, final: true) }
.to raise_error(Assignment::GradeError, "User not authorized to give final provisional grades")
end
it "raises an exception if grade is not final and student does not need a provisional grade" do
@assignment.grade_student(@student, grade: 2, grader: @teacher2, provisional: true)
third_teacher = User.create!
@course.enroll_teacher(third_teacher, enrollment_state: :active)
expect { @submission.find_or_create_provisional_grade!(third_teacher, final: false) }
.to raise_error(Assignment::GradeError, "Student already has the maximum number of provisional grades")
end
it "raises an exception if the grade is final and no non-final provisional grades exist" do
expect { @submission.find_or_create_provisional_grade!(@teacher, final: true) }
.to raise_error(Assignment::GradeError,
"Cannot give a final mark for a student with no other provisional grades")
end
it "raises an exception if the grade has been selected and is associated with a provisional grader" do
pg = @submission.find_or_create_provisional_grade!(@teacher2, grade: "2", score: 2)
selection = @assignment.moderated_grading_selections.where(student: @submission.user).first
selection.provisional_grade = pg
selection.save!
expect do
@submission.find_or_create_provisional_grade!(@teacher2, grade: "3", score: 3)
end.to raise_error(Assignment::GradeError) do |error|
expect(error.error_code).to eq Assignment::GradeError::PROVISIONAL_GRADE_MODIFY_SELECTED
end
end
it "does not raise an exception if the grade has been selected and is associated with the final grader" do
pg = @submission.find_or_create_provisional_grade!(@teacher, grade: "2", score: 2)
selection = @assignment.moderated_grading_selections.where(student: @submission.user).first
selection.provisional_grade = pg
selection.save!
expect do
@submission.find_or_create_provisional_grade!(@teacher, grade: "3", score: 3)
end.not_to raise_error
end
it "sets the source provisional grade if one is provided" do
new_source = ModeratedGrading::ProvisionalGrade.new
provisional_grade = @submission.find_or_create_provisional_grade!(@teacher, source_provisional_grade: new_source)
expect(provisional_grade.source_provisional_grade).to be new_source
end
it "does not wipe out the existing source provisional grade, if a source_provisional_grade is not provided" do
@submission.find_or_create_provisional_grade!(@teacher)
expect { @submission.find_or_create_provisional_grade!(@teacher, force_save: true) }
.not_to change { @submission.provisional_grades.last.source_provisional_grade }
end
end
describe "moderated_grading_allow_list" do
before(:once) do
@student = @user
@assignment.update!(
final_grader: @teacher,
grader_comments_visible_to_graders: false,
grader_count: 3,
moderated_grading: true,
submission_types: :online_text_entry
)
@assignment.submit_homework(@student, body: "my submission", submission_type: :online_text_entry)
@submission = @assignment.submissions.find_by(user: @student)
end
let(:user_ids_in_allow_list) { allow_list.map { |user| user.fetch(:global_id)&.to_i } }
it "returns nil when the assignment is not moderated" do
# Skipping validations here because they'd prevent turning off Moderated Grading
# for an assignment with graded submissions.
@assignment.update_column(:moderated_grading, false)
expect(@submission.moderated_grading_allow_list).to be_nil
end
it "returns nil when the user is not present" do
expect(@submission.moderated_grading_allow_list(nil)).to be_nil
end
it "can be passed a collection of attachments for checking if crocodoc is available" do
attachment = double
expect(attachment).to receive(:crocodoc_available?).and_return(true)
@submission.moderated_grading_allow_list(loaded_attachments: [attachment])
end
it "returns a collection of moderated grading ids" do
moderated_grading_ids = @student.moderated_grading_ids(false)
expect(@submission.moderated_grading_allow_list.first).to eq moderated_grading_ids
end
it "calls moderation_allow_list_for_user to generate the allow_list" do
expect(@submission).to receive(:moderation_allow_list_for_user).with(@student).once.and_call_original
@submission.moderated_grading_allow_list
end
end
describe "moderation_allow_list_for_user" do
before(:once) do
@student = @user
@provisional_grader = User.create!
@other_provisional_grader = User.create!
@course.enroll_teacher(@provisional_grader, enrollment_state: :active)
@course.enroll_teacher(@other_provisional_grader, enrollment_state: :active)
@eligible_provisional_grader = User.create!
@course.enroll_teacher(@eligible_provisional_grader, enrollment_state: :active)
@admin = account_admin_user(account: @course.root_account)
@assignment.update!(
final_grader: @teacher,
grader_comments_visible_to_graders: false,
grader_count: 3,
moderated_grading: true,
submission_types: :online_text_entry
)
@assignment.submit_homework(@student, body: "my submission", submission_type: :online_text_entry)
@submission = @assignment.submissions.find_by(user: @student)
@assignment.grade_student(@student, grader: @teacher, provisional: true, score: 5)
@assignment.grade_student(@student, grader: @provisional_grader, provisional: true, score: 1)
@assignment.grade_student(@student, grader: @other_provisional_grader, provisional: true, score: 3)
end
let(:user_ids_in_allow_list) { allow_list.map { |user| user.fetch(:global_id)&.to_i } }
it "returns an empty array when the assignment is not moderated" do
# Skipping validations here because they'd prevent turning off Moderated Grading
# for an assignment with graded submissions.
@assignment.update_column(:moderated_grading, false)
expect(@submission.moderation_allow_list_for_user(@teacher)).to be_empty
end
it "returns an empty array when the user is not present" do
expect(@submission.moderation_allow_list_for_user(nil)).to be_empty
end
it "returns an empty array when the user is not permitted to view annotations for the submission" do
other_student = User.create!
@course.enroll_student(other_student, enrollment_state: :active)
expect(@submission.moderation_allow_list_for_user(other_student)).to be_empty
end
it "always includes the submission owner when the assignment is of type Student Annotation" do
ta = @course.enroll_ta(User.create!).user
@assignment.update!(grader_count: 2, submission_types: "student_annotation")
expect(@submission.moderation_allow_list_for_user(ta)).to eq [@student]
end
context "when the submission is not posted" do
context "when the user is the final grader" do
let(:allow_list) { @submission.moderation_allow_list_for_user(@teacher) }
it "includes the current user" do
expect(allow_list).to include @teacher
end
it "includes all provisional graders" do
expect(allow_list).to include(*@assignment.moderation_grader_users)
end
it "includes the student" do
expect(allow_list).to include @student
end
it "does not include eligible provisional graders" do
expect(allow_list).not_to include @eligible_provisional_grader
end
it "does not include duplicates" do
expect(allow_list.uniq).to eq allow_list
end
it "does not include nil values" do
expect(allow_list).not_to include nil
end
end
context "when the user is a provisional grader" do
let(:allow_list) { @submission.moderation_allow_list_for_user(@provisional_grader) }
context "when grader comments are visible to other graders" do
before(:once) do
@assignment.update!(grader_comments_visible_to_graders: true)
end
it "includes all provisional graders" do
expect(allow_list).to include(*@assignment.moderation_grader_users)
end
it "includes the final grader" do
expect(allow_list).to include @teacher
end
it "includes the student" do
expect(allow_list).to include @student
end
it "does not include eligible provisional graders" do
expect(allow_list).not_to include @eligible_provisional_grader
end
it "does not include duplicates" do
expect(allow_list.uniq).to eq allow_list
end
it "does not include nil values" do
expect(allow_list).not_to include nil
end
end
context "when grader comments are not visible to other graders" do
it "includes the current user" do
expect(allow_list).to include @provisional_grader
end
it "does not include other provisional graders" do
expect(allow_list).not_to include @other_provisional_grader
end
it "does not include the final grader" do
expect(allow_list).not_to include @teacher
end
it "includes the student" do
expect(allow_list).to include @student
end
it "does not include eligible provisional graders" do
expect(allow_list).not_to include @eligible_provisional_grader
end
it "does not include duplicates" do
expect(allow_list.uniq).to eq allow_list
end
it "does not include nil values" do
expect(allow_list).not_to include nil
end
end
end
context "when the user is an eligible provisional grader" do
let(:allow_list) { @submission.moderation_allow_list_for_user(@eligible_provisional_grader) }
context "when grader comments are visible to other graders" do
before(:once) do
@assignment.update!(grader_comments_visible_to_graders: true)
end
it "includes the current user" do
expect(allow_list).to include @eligible_provisional_grader
end
it "includes all provisional graders" do
expect(allow_list).to include(*@assignment.moderation_grader_users)
end
it "includes the final grader" do
expect(allow_list).to include @teacher
end
it "includes the student" do
expect(allow_list).to include @student
end
it "does not include other eligible provisional graders" do
other_eligible_provisional_grader = User.create!
@course.enroll_teacher(other_eligible_provisional_grader, enrollment_state: :active)
expect(allow_list).not_to include other_eligible_provisional_grader
end
it "does not include duplicates" do
expect(allow_list.uniq).to eq allow_list
end
it "does not include nil values" do
expect(allow_list).not_to include nil
end
end
context "when grader comments are not visible to other graders" do
it "includes the current user" do
expect(allow_list).to include @eligible_provisional_grader
end
it "does not include provisional graders" do
expect(allow_list).not_to include(*@assignment.moderation_grader_users)
end
it "does not include the final grader" do
expect(allow_list).not_to include @teacher
end
it "includes the student" do
expect(allow_list).to include @student
end
it "does not include other eligible provisional graders" do
other_eligible_provisional_grader = User.create!
@course.enroll_teacher(other_eligible_provisional_grader, enrollment_state: :active)
expect(allow_list).not_to include other_eligible_provisional_grader
end
it "does not include duplicates" do
expect(allow_list.uniq).to eq allow_list
end
it "does not include nil values" do
expect(allow_list).not_to include nil
end
end
end
context "when the user is an admin" do
let(:allow_list) { @submission.moderation_allow_list_for_user(@admin) }
it "includes the current user" do
expect(allow_list).to include @admin
end
it "includes all provisional graders" do
expect(allow_list).to include(*@assignment.moderation_grader_users)
end
it "includes the final grader" do
expect(allow_list).to include @teacher
end
it "includes the student" do
expect(allow_list).to include @student
end
it "does not include eligible provisional graders" do
expect(allow_list).not_to include @eligible_provisional_grader
end
it "does not include duplicates" do
expect(allow_list.uniq).to eq allow_list
end
it "does not include nil values" do
expect(allow_list).not_to include nil
end
end
context "when the user is a student" do
let(:allow_list) { @submission.moderation_allow_list_for_user(@student) }
it "includes the current user" do
expect(allow_list).to include @student
end
it "does not include the admin" do
expect(allow_list).not_to include @admin
end
it "does not include provisional graders" do
expect(allow_list).not_to include(*@assignment.moderation_grader_users)
end
it "does not include eligible provisional graders" do
expect(allow_list).not_to include @eligible_provisional_grader
end
it "does not include duplicates" do
expect(allow_list.uniq).to eq allow_list
end
it "does not include nil values" do
expect(allow_list).not_to include nil
end
end
end
context "when the submission is posted" do
before(:once) do
provisional_grade = @submission.find_or_create_provisional_grade!(@provisional_grader)
selection = @assignment.moderated_grading_selections.find_by(student: @student)
selection.update!(provisional_grade:)
provisional_grade.publish!
@assignment.update!(grades_published_at: 1.hour.ago)
@assignment.post_submissions
@submission.reload
end
context "when the user is the final grader" do
let(:allow_list) { @submission.moderation_allow_list_for_user(@teacher) }
it "includes the current user" do
expect(allow_list).to include @teacher
end
it "includes the provisional grader whose grade was selected" do
expect(allow_list).to include @provisional_grader
end
it "does not include the provisional grader whose grade was not selected" do
expect(allow_list).not_to include @other_provisional_grader
end
it "includes the student" do
expect(allow_list).to include @student
end
it "does not include eligible provisional graders" do
expect(allow_list).not_to include @eligible_provisional_grader
end
it "does not include duplicates" do
expect(allow_list.uniq).to eq allow_list
end
it "does not include nil values" do
expect(allow_list).not_to include nil
end
it "does not raise an error when the submission has no grader" do
@submission.update!(grader: nil, score: nil)
expect { allow_list }.not_to raise_error
end
end
context "when the user is a provisional grader" do
let(:allow_list) { @submission.moderation_allow_list_for_user(@provisional_grader) }
it "includes the current user" do
expect(allow_list).to include @provisional_grader
end
it "does not include other provisional graders whose grades were not selected" do
expect(allow_list).not_to include @other_provisional_grader
end
it "does not include the final grader if their grade was not selected" do
expect(allow_list).not_to include @teacher
end
it "includes the student" do
expect(allow_list).to include @student
end
it "does not include eligible provisional graders" do
expect(allow_list).not_to include @eligible_provisional_grader
end
it "does not include duplicates" do
expect(allow_list.uniq).to eq allow_list
end
it "does not include nil values" do
expect(allow_list).not_to include nil
end
it "does not raise an error when the submission has no grader" do
@submission.update!(grader: nil, score: nil)
expect { allow_list }.not_to raise_error
end
end
context "when the user is an eligible provisional grader" do
let(:allow_list) { @submission.moderation_allow_list_for_user(@eligible_provisional_grader) }
it "includes the current user" do
expect(allow_list).to include @eligible_provisional_grader
end
it "includes the provisional grader whose grade was selected" do
expect(allow_list).to include @provisional_grader
end
it "does not include the provisional grader whose grade was not selected" do
expect(allow_list).not_to include @other_provisional_grader
end
it "does not include the final grader if their grade was not selected" do
expect(allow_list).not_to include @teacher
end
it "includes the student" do
expect(allow_list).to include @student
end
it "does not include other eligible provisional graders" do
other_eligible_provisional_grader = User.create!
@course.enroll_teacher(other_eligible_provisional_grader, enrollment_state: :active)
expect(allow_list).not_to include other_eligible_provisional_grader
end
it "does not include duplicates" do
expect(allow_list.uniq).to eq allow_list
end
it "does not include nil values" do
expect(allow_list).not_to include nil
end
it "does not raise an error when the submission has no grader" do
@submission.update!(grader: nil, score: nil)
expect { allow_list }.not_to raise_error
end
end
context "when the user is an admin" do
let(:allow_list) { @submission.moderation_allow_list_for_user(@admin) }
it "includes the current user" do
expect(allow_list).to include @admin
end
it "includes the provisional grader whose grade was selected" do
expect(allow_list).to include @provisional_grader
end
it "does not include the provisional grader whose grade was not selected" do
expect(allow_list).not_to include @other_provisional_grader
end
it "does not include the final grader if their grade was not selected" do
expect(allow_list).not_to include @teacher
end
it "includes the student" do
expect(allow_list).to include @student
end
it "does not include eligible provisional graders" do
expect(allow_list).not_to include @eligible_provisional_grader
end
it "does not include duplicates" do
expect(allow_list.uniq).to eq allow_list
end
it "does not include nil values" do
expect(allow_list).not_to include nil
end
it "does not raise an error when the submission has no grader" do
@submission.update!(grader: nil, score: nil)
expect { allow_list }.not_to raise_error
end
end
context "when the user is a student" do
let(:allow_list) { @submission.moderation_allow_list_for_user(@student) }
it "includes the current user" do
expect(allow_list).to include @student
end
it "includes the provisional grader whose grade was selected" do
expect(allow_list).to include @provisional_grader
end
it "does not include the provisional grader whose grade was not selected" do
expect(allow_list).not_to include @other_provisional_grader
end
it "does not include the final grader if their grade was not selected" do
expect(allow_list).not_to include @teacher
end
it "does not include eligible provisional graders" do
expect(allow_list).not_to include @eligible_provisional_grader
end
it "does not include duplicates" do
expect(allow_list.uniq).to eq allow_list
end
it "does not include nil values" do
expect(allow_list).not_to include nil
end
it "does not raise an error when the submission has no grader" do
@submission.update!(grader: nil, score: nil)
expect { allow_list }.not_to raise_error
end
end
end
end
describe "anonymous_identities" do
let(:submission) { @assignment.submissions.first }
it "includes the student in the list" do
expect(submission.anonymous_identities).to have_key @student.id
end
it "includes the anonymous name" do
expect(submission.anonymous_identities.dig(@student.id, :name)).to eq "Student"
end
it "includes the anonymous id" do
expect(submission.anonymous_identities.dig(@student.id, :id)).to eq submission.anonymous_id
end
end
describe "#visible_rubric_assessments_for" do
subject { @submission.visible_rubric_assessments_for(@viewing_user) }
before :once do
submission_model assignment: @assignment, user: @student
@viewing_user = @teacher
@assessed_user = @student
rubric_association_model association_object: @assignment, purpose: "grading"
[@teacher, @student].each do |user|
@rubric_association.rubric_assessments.create!({
artifact: @submission,
assessment_type: "grading",
assessor: user,
rubric: @rubric,
user: @assessed_user
})
end
@teacher_assessment = @submission.rubric_assessments.where(assessor_id: @teacher).first
@student_assessment = @submission.rubric_assessments.where(assessor_id: @student).first
end
context "when the submission is unposted and the viewing user cannot :read_grade" do
before(:once) do
@assignment.post_policy.update!(post_manually: true)
@viewing_user = @student
end
it "excludes assessments by other users" do
expect(subject).not_to include(@teacher_assessment)
end
it "includes assessments authored by the viewing user" do
course = Course.create!
assessed_student = course.enroll_student(User.create!, workflow_state: "active").user
assessing_student = course.enroll_student(User.create!, workflow_state: "active").user
assignment = course.assignments.create!(peer_reviews: true)
rubric_association = rubric_association_model(context: course, association_object: assignment, purpose: "grading")
submission = assignment.submission_for_student(assessed_student)
submission.assessment_requests.create!(
user: assessed_student,
assessor: assessing_student,
assessor_asset: assignment.submission_for_student(assessing_student)
)
peer_review_assessment = rubric_association.rubric_assessments.create!({
artifact: submission,
assessment_type: "grading",
assessor: assessing_student,
rubric: rubric_association.rubric,
user: assessed_student
})
expect(submission.visible_rubric_assessments_for(assessing_student)).to include(peer_review_assessment)
end
end
it "returns the rubric assessments if user can :read_grade" do
expect(subject).to contain_exactly(@teacher_assessment, @student_assessment)
end
it "returns the rubric assessments if the submission is posted" do
@submission.update!(posted_at: Time.zone.now)
expect(subject).to contain_exactly(@teacher_assessment, @student_assessment)
end
it "does not return rubric assessments if assignment has no rubric" do
@assignment.rubric_association.destroy!
expect(subject).not_to include(@teacher_assessment)
end
it "only returns rubric assessments from associated rubrics" do
other = @rubric_association.dup
other.save!
other_assessment = other.rubric_assessments.create!({
artifact: @submission,
assessment_type: "grading",
assessor: @teacher,
rubric: @rubric,
user: @assessed_user
})
expect(subject).to eq([other_assessment])
end
context "attempt argument" do
before(:once) do
@submission2 = @assignment.submit_homework(@student, body: "bar", submitted_at: 1.hour.since)
end
it "returns an empty list if no rubric assessments exist for the desired attempt" do
expect(
@submission2.visible_rubric_assessments_for(@viewing_user, attempt: @submission2.attempt)
).to be_empty
end
it "can find historic rubric assessments of older attempts" do
expect(
@submission2.visible_rubric_assessments_for(@viewing_user, attempt: @submission.attempt)
).to contain_exactly(@teacher_assessment, @student_assessment)
end
it "returns assessments for every attempt if attempt is nil" do
@teacher_assessment.update!(artifact_attempt: 0)
@student_assessment.update!(artifact_attempt: 1)
expect(
@submission2.visible_rubric_assessments_for(@viewing_user, attempt: nil)
).to contain_exactly(@teacher_assessment, @student_assessment)
end
it "specifically returns assessments with a nil artifact_attempt if an attempt of 0 is specified" do
assignment = @course.assignments.create!(submission_types: "online_text_entry")
rubric_association = rubric_association_model(association_object: assignment, purpose: "grading")
submission = assignment.submission_for_student(@student)
assessment_before_submitting = rubric_association.rubric_assessments.create!({
artifact: submission,
assessment_type: "grading",
assessor: @student,
rubric: rubric_association.rubric,
user: @student
})
submission = assignment.submit_homework(@student, body: "hi")
rubric_association.rubric_assessments.create!({
artifact: submission,
assessment_type: "grading",
assessor: @teacher,
rubric: rubric_association.rubric,
user: @student
})
expect(submission.visible_rubric_assessments_for(@student, attempt: 0))
.to contain_exactly(assessment_before_submitting)
end
end
context "anonymous peer reviews" do
before(:once) do
course = Course.create!
@reviewed_student = course.enroll_student(User.create!, workflow_state: "active").user
@reviewing_student = course.enroll_student(User.create!, workflow_state: "active").user
@grading_teacher = course.enroll_teacher(User.create!, workflow_state: "active").user
assignment = course.assignments.create!(peer_reviews: true, anonymous_peer_reviews: true)
rubric_association = rubric_association_model(context: course, association_object: assignment, purpose: "grading")
@submission = assignment.submission_for_student(@reviewed_student)
@submission.assessment_requests.create!(
user: @reviewed_student,
assessor: @reviewing_student,
assessor_asset: assignment.submission_for_student(@reviewing_student)
)
rubric_association.rubric_assessments.create!({
artifact: @submission,
assessment_type: "peer_review",
assessor: @reviewing_student,
rubric: rubric_association.rubric,
user: @reviewed_student
})
rubric_association.rubric_assessments.create!({
artifact: @submission,
assessment_type: "grading",
assessor: @grading_teacher,
rubric: rubric_association.rubric,
user: @reviewed_student
})
end
it "viewed by reviewed_student include rubric assessments from teachers with identity attached" do
expect(@submission.visible_rubric_assessments_for(@reviewed_student)[0].assessor).to eql(@grading_teacher)
end
it "viewed by reviewed_student does not include peer reviewer's identity when viewed by the reviewee" do
expect(@submission.visible_rubric_assessments_for(@reviewed_student)[1].assessor).to be_nil
end
it "includes peer reviewer's identity when viewed by the reviewer" do
expect(@submission.visible_rubric_assessments_for(@reviewing_student)[0].assessor).to eql(@reviewing_student)
end
end
end
describe "#rubric_assessment" do
let(:submission) { @assignment.submission_for_student(@student) }
it "excludes non-grading assessments" do
grading_rubric_association = rubric_association_model(association_object: @assignment, purpose: "grading")
grading_assessment = grading_rubric_association.rubric_assessments.create!(
artifact: submission,
assessment_type: "grading",
assessor: @teacher,
rubric: grading_rubric_association.rubric,
user: @student
)
non_grading_rubric_association = rubric_association_model(association_object: @assignment, purpose: "pleasurable event")
non_grading_rubric_association.rubric_assessments.create!(
artifact: submission,
assessment_type: "pleasurable event",
assessor: @teacher,
rubric: non_grading_rubric_association.rubric,
user: @student
)
expect(submission.rubric_assessment).to eq grading_assessment
end
it "prioritizes assessments with a non-nil rubric_association when multiple grading assessments exist" do
old_rubric_association = rubric_association_model(association_object: @assignment, purpose: "grading")
old_assessment = old_rubric_association.rubric_assessments.create!(
artifact: submission,
assessment_type: "grading",
assessor: @teacher,
rubric: old_rubric_association.rubric,
user: @student
)
old_rubric_association.destroy
new_rubric_association = rubric_association_model(association_object: @assignment, purpose: "grading")
new_assessment = new_rubric_association.rubric_assessments.create!(
artifact: submission,
assessment_type: "grading",
assessor: @teacher,
rubric: new_rubric_association.rubric,
user: @student
)
aggregate_failures do
expect(submission.rubric_assessments).to contain_exactly(old_assessment, new_assessment)
expect(submission.rubric_assessment).to eq new_assessment
end
end
end
describe "#add_comment" do
before(:once) do
submission_spec_model
end
it "creates a draft comment when passed true in the draft_comment option" do
comment = @submission.add_comment(author: @teacher, comment: "42", draft_comment: true)
expect(comment).to be_draft
end
it "creates a final comment when not passed in a draft_comment option" do
comment = @submission.add_comment(author: @teacher, comment: "42")
expect(comment).not_to be_draft
end
it "creates a final comment when passed false in the draft_comment option" do
comment = @submission.add_comment(author: @teacher, comment: "42", draft_comment: false)
expect(comment).not_to be_draft
end
it "creates a comment without an author when skip_author option is true" do
comment = @submission.add_comment(comment: "42", skip_author: true)
expect(comment.author).to be_nil
end
it "allows you to specify submission attempt for the comment" do
@submission.update!(attempt: 4)
comment = @submission.add_comment(author: @teacher, comment: "42", attempt: 3)
expect(comment.attempt).to eq 3
end
it "sets the attempt to latest submission attempt when an attempt option is not specified" do
@submission.update!(attempt: 5, workflow_state: "graded")
comment = @submission.add_comment(author: @teacher, comment: "42")
expect(comment.attempt).to eq 5
end
it "sets comment hidden to false if comment causes posting" do
@assignment.ensure_post_policy(post_manually: false)
@assignment.grade_student(@student, grader: @teacher, score: 5)
@submission.update!(posted_at: nil)
comment = @submission.add_comment(author: @teacher, comment: "a comment!", hidden: true)
expect(comment).not_to be_hidden
end
it "does not set comment hidden to false if comment does not cause posting" do
@assignment.ensure_post_policy(post_manually: true)
@assignment.grade_student(@student, grader: @teacher, score: 5)
@submission.update!(posted_at: nil)
comment = @submission.add_comment(author: @teacher, comment: "a comment!", hidden: true)
expect(comment).to be_hidden
end
describe "audit event logging" do
let(:course) { Course.create! }
let(:assignment) { course.assignments.create!(title: "ok", anonymous_grading: true) }
let(:student) { course.enroll_student(User.create!, enrollment_state: "active").user }
let(:teacher) { course.enroll_teacher(User.create!, enrollment_state: "active").user }
let(:submission) { assignment.submissions.find_by!(user: student) }
let(:comment_params) { { comment: "my great submission", author: student } }
let(:last_event) { AnonymousOrModerationEvent.where(assignment:, submission:).last }
context "for an auditable assignment" do
it "creates an event when a non-draft comment is published" do
expect { submission.add_comment(comment_params) }.to change {
AnonymousOrModerationEvent.where(assignment:, submission:).count
}.by(1)
end
it 'sets "submission_comment_created" as the event type' do
submission.add_comment(comment_params)
expect(last_event.event_type).to eq "submission_comment_created"
end
it "sets the user ID to the author of the comment" do
submission.add_comment(comment_params)
expect(last_event.user_id).to eq student.id
end
it "does not create events for draft comments" do
draft_params = comment_params.merge(draft_comment: true)
expect { submission.add_comment(draft_params) }.not_to change {
AnonymousOrModerationEvent.where(assignment:, submission:).count
}
end
describe "auditable attributes" do
it 'captures the value of the "comment" attribute' do
submission.add_comment(comment_params)
expect(last_event.payload["comment"]).to eq "my great submission"
end
it 'captures the value of the "author_id" attribute' do
submission.add_comment(comment_params)
expect(last_event.payload["author_id"]).to eq student.id
end
it 'captures the value of the "media_comment_id" attribute' do
submission.add_comment(comment_params.merge(media_comment_id: 12))
expect(last_event.payload["media_comment_id"]).to eq "12"
end
it 'captures the value of the "media_comment_type" attribute' do
submission.add_comment(comment_params.merge(media_comment_type: "audio"))
expect(last_event.payload["media_comment_type"]).to eq "audio"
end
it 'captures the value of the "group_comment_id" attribute' do
submission.add_comment(comment_params.merge(group_comment_id: 12))
expect(last_event.payload["group_comment_id"]).to eq "12"
end
it 'captures the value of the "assessment_request" attribute' do
assessment_request = submission.assessment_requests.create!(
user: student,
assessor: student,
assessor_asset: submission
)
submission.add_comment(comment_params.merge(assessment_request:))
expect(last_event.payload["assessment_request_id"]).to eq assessment_request.id
end
it 'captures the value of the "attachments" attribute' do
attachment = Attachment.create!(
filename: "my_great_file.txt",
uploaded_data: StringIO.new("hello!"),
context: course
)
submission.add_comment(comment_params.merge(attachments: [attachment]))
expect(last_event.payload["attachment_ids"]).to eq attachment.id.to_s
end
it 'captures the value of the "anonymous" attribute' do
assignment.update!(anonymous_peer_reviews: true)
submission.add_comment(comment_params)
expect(last_event.payload["anonymous"]).to be true
end
it 'captures the value of the "provisional_grade_id" attribute' do
assignment.update!(moderated_grading: true, final_grader: teacher, grader_count: 1)
provisional_grade = submission.find_or_create_provisional_grade!(teacher)
provisional_comment_params = comment_params.merge(provisional: true, author: teacher)
submission.add_comment(provisional_comment_params)
expect(last_event.payload["provisional_grade_id"]).to eq provisional_grade.id
end
end
describe "external tool autograding" do
let(:external_tool) do
Account.default.context_external_tools.create!(
name: "Undertow",
url: "http://www.example.com",
consumer_key: "12345",
shared_secret: "secret"
)
end
it "creates an event when graded by an external tool" do
expect { assignment.grade_student(student, grader_id: -external_tool.id, score: 80) }.to change {
AnonymousOrModerationEvent.where(assignment:, submission:).count
}.by(1)
end
end
describe "quiz autograding" do
let(:quiz) do
quiz = course.quizzes.create!
quiz.workflow_state = "available"
quiz.quiz_questions.create!({ question_data: test_quiz_data.first })
quiz.save!
quiz.assignment.updating_user = teacher
quiz.assignment.update_attribute(:anonymous_grading, true)
quiz
end
let(:quiz_assignment) { quiz.assignment }
let(:quiz_submission) do
qsub = Quizzes::SubmissionManager.new(quiz).find_or_create_submission(student)
qsub.quiz_data = test_quiz_data
qsub.started_at = 1.minute.ago
qsub.attempt = 1
qsub.submission_data = [{ points: 0, text: "7051", question_id: 128, correct: false, answer_id: 7051 }]
qsub.score = 0
qsub.save!
qsub.finished_at = Time.now.utc
qsub.workflow_state = "complete"
qsub.submission = quiz.assignment.find_or_create_submission(student)
qsub
end
it "creates an event when graded by a quiz" do
real_submission = quiz_submission.submission
real_submission.audit_grade_changes = true
expect { quiz_submission.with_versioning(true) { quiz_submission.save! } }.to change {
AnonymousOrModerationEvent.where(assignment: quiz_assignment, submission: real_submission).count
}.by(1)
end
end
end
it "does not create audit events when the assignment is not auditable" do
assignment1 = course.assignments.create!(title: "ok", anonymous_grading: false)
submission1 = assignment1.submission_for_student(student)
expect { submission1.add_comment(comment_params) }.not_to change {
AnonymousOrModerationEvent.where(assignment:, submission:).count
}
end
end
describe "submission posting" do
let(:course) { Course.create! }
let(:assignment) { course.assignments.create!(title: "ok") }
let(:student) { course.enroll_student(User.create!, enrollment_state: "active").user }
let(:teacher) { course.enroll_teacher(User.create!, enrollment_state: "active").user }
let(:submission) { assignment.submissions.find_by!(user: student) }
let(:comment_params) { { comment: "oh no", author: teacher } }
context "when the submission is unposted" do
it "posts the submission if the comment is from an instructor in the course" do
submission.add_comment(comment_params)
expect(submission).to be_posted
end
it "posts the submission if the comment is from an admin" do
admin = User.create!
course.root_account.account_users.create!(user: admin)
submission.add_comment(comment_params.merge({ author: admin }))
expect(submission).to be_posted
end
it "does not post the submission if the comment is not from an instructor or admin" do
submission.add_comment(comment_params.merge({ author: student }))
expect(submission).not_to be_posted
end
it "does not post the submission if the comment is a draft" do
submission.add_comment(comment_params.merge({ draft_comment: true }))
expect(submission).not_to be_posted
end
it "does not post the submission if the comment has no author" do
comment_params.delete(:author)
submission.add_comment(comment_params)
expect(submission).not_to be_posted
end
it "does not post the submission if the comment is provisional" do
moderated_assignment = course.assignments.create!(
title: "aa",
moderated_grading: true,
final_grader: teacher,
grader_count: 2
)
moderated_submission = moderated_assignment.submission_for_student(student)
moderated_submission.add_comment(comment_params.merge({ provisional: true }))
expect(moderated_submission).not_to be_posted
end
it "does not post the submission if the assignment is manually-posted" do
assignment.ensure_post_policy(post_manually: true)
submission.add_comment(comment_params)
expect(submission).not_to be_posted
end
it "does not post the submission if post policies are not enabled and the assignment is muted" do
assignment.mute!
expect(submission).not_to be_posted
end
end
it "does not update the posted_at date if a submission is already posted" do
submission.update!(posted_at: 1.day.ago)
expect do
submission.add_comment(comment_params)
end.not_to change {
assignment.submission_for_student(student).posted_at
}
end
end
end
describe "#last_teacher_comment" do
before(:once) do
submission_spec_model
end
it "returns the last published comment made by the teacher" do
@submission.add_comment(author: @teacher, comment: "a comment")
expect(@submission.last_teacher_comment).to be_present
end
it "does not include draft comments" do
@submission.add_comment(author: @teacher, comment: "a comment", draft_comment: true)
expect(@submission.last_teacher_comment).to be_nil
end
end
describe "#ensure_grader_can_grade" do
before do
@submission = Submission.new
end
context "when #grader_can_grade? returns true" do
before do
expect(@submission).to receive(:grader_can_grade?).and_return(true)
end
it "returns true" do
expect(@submission.ensure_grader_can_grade).to be_truthy
end
it "does not add any errors to @submission" do
@submission.ensure_grader_can_grade
expect(@submission.errors.full_messages).to be_empty
end
end
context "when #grader_can_grade? returns false" do
before do
expect(@submission).to receive(:grader_can_grade?).and_return(false)
end
it "returns false" do
expect(@submission.ensure_grader_can_grade).to be_falsey
end
it "adds an error to the :grade field" do
@submission.ensure_grader_can_grade
expect(@submission.errors[:grade]).not_to be_empty
end
describe "skip_grader_check" do
it "does not add an error to the :grade field if skip_grader_check is true" do
@submission.skip_grader_check = true
@submission.ensure_grader_can_grade
expect(@submission.errors[:grade]).to be_empty
end
it "adds an error to the :grade field if skip_grader_check is false" do
@submission.skip_grader_check = false
@submission.ensure_grader_can_grade
expect(@submission.errors[:grade]).not_to be_empty
end
it "adds an error to the :grade field if skip_grader_check is not set" do
@submission.ensure_grader_can_grade
expect(@submission.errors[:grade]).not_to be_empty
end
end
end
end
describe "#grader_can_grade?" do
before do
@submission = Submission.new
end
it "returns true if grade hasn't been changed" do
expect(@submission).to receive(:grade_changed?).and_return(false)
expect(@submission.grader_can_grade?).to be_truthy
end
it "returns true if the submission is autograded and the submission can be autograded" do
expect(@submission).to receive(:grade_changed?).and_return(true)
expect(@submission).to receive(:autograded?).and_return(true)
expect(@submission).to receive(:can_autograde?).and_return(true)
expect(@submission.grader_can_grade?).to be_truthy
end
it "returns true if the submission isn't autograded but can still be graded" do
expect(@submission).to receive(:grade_changed?).and_return(true)
expect(@submission).to receive(:autograded?).and_return(false)
@submission.grader = @grader = User.new
expect(@submission).to receive(:grants_right?).with(@grader, :grade).and_return(true)
expect(@submission.grader_can_grade?).to be_truthy
end
it "returns false if the grade changed but the submission can't be graded at all" do
@submission.grader = @grader = User.new
expect(@submission).to receive(:grade_changed?).and_return(true)
expect(@submission).to receive(:autograded?).and_return(false)
expect(@submission).to receive(:grants_right?).with(@grader, :grade).and_return(false)
expect(@submission.grader_can_grade?).to be_falsey
end
end
describe "#submission_history" do
let!(:student) { student_in_course(active_all: true).user }
let(:attachment) { attachment_model(filename: "submission-a.doc", context: student) }
let(:submission) { @assignment.submit_homework(student, submission_type: "online_upload", attachments: [attachment]) }
it "includes originality data" do
OriginalityReport.create!(submission:, attachment:, originality_score: 1.0, workflow_state: "pending")
submission.originality_reports.load_target
expect(submission.submission_history.first.turnitin_data[attachment.asset_string][:similarity_score]).to eq 1.0
end
it "doesn't include the originality_data if originality_report isn't pre loaded" do
OriginalityReport.create!(submission:, attachment:, originality_score: 1.0, workflow_state: "pending")
expect(submission.submission_history.first.turnitin_data[attachment.asset_string]).to be_nil
end
it "returns self as complete history when no history record is present" do
student.submissions.destroy_all
create_sql = "INSERT INTO #{Submission.quoted_table_name}
(assignment_id, user_id, workflow_state, created_at, updated_at, course_id)
values
(#{@assignment.id}, #{student.id}, 'unsubmitted', now(), now(), #{@assignment.context_id})"
sub = Submission.find(Submission.connection.create(create_sql))
expect(sub.submission_history).to eq([sub])
end
end
context "draft comments" do
before do
@teacher = course_with_user("TeacherEnrollment", course: @course, name: "Teacher", active_all: true).user
ta = course_with_user("TaEnrollment", course: @course, name: "First Ta", active_all: true).user
@student = course_with_user("StudentEnrollment", course: @course, name: "Student", active_all: true).user
@assignment = @course.assignments.create!(name: "plain assignment")
@assignment.ensure_post_policy(post_manually: true)
@submission = @assignment.submissions.find_by(user: @student)
@student_comment = @submission.add_comment(author: @student, comment: "Student comment")
@teacher_comment = @submission.add_comment(author: @teacher, comment: "Teacher comment", draft_comment: true)
@ta_comment = @submission.add_comment(author: ta, comment: "Ta comment")
end
describe "#comments_excluding_drafts_for" do
it "doesn't blow up when the submission has no course" do
@submission.course = nil
expect { @submission.comments_excluding_drafts_for(@teacher) }.not_to raise_error
end
it "returns non-draft comments, filtering out draft comments" do
comments = @submission.comments_excluding_drafts_for(@teacher)
expect(comments).to include @student_comment, @ta_comment
expect(comments).not_to include @teacher_comment
end
it "does not return hidden comments if the user is a student and the comment has not been posted" do
hidden_comment = @submission.add_comment(author: @teacher, comment: "Hidden comment", hidden: true)
@assignment.ensure_post_policy(post_manually: false)
comments = @submission.comments_excluding_drafts_for(@student)
expect(comments).not_to include hidden_comment
end
context "when comments are preloaded" do
it "returns non-draft comments, filtering out draft comments" do
preloaded_submission = Submission.where(id: @submission.id).preload(:submission_comments).first
comments = preloaded_submission.comments_excluding_drafts_for(@teacher)
expect(comments).to include @student_comment, @ta_comment
expect(comments).not_to include @teacher_comment
end
end
end
describe "#comments_including_drafts_for" do
it "returns draft comments, filtering out draft comments" do
comments = @submission.comments_including_drafts_for(@teacher)
expect(comments).to include @student_comment, @ta_comment
expect(comments).to include @teacher_comment
end
context "when comments are preloaded" do
it "returns non-draft comments, filtering out draft comments" do
preloaded_submission = Submission.where(id: @submission.id).preload(:submission_comments).first
comments = preloaded_submission.comments_including_drafts_for(@teacher)
expect(comments).to include @student_comment, @ta_comment
expect(comments).to include @teacher_comment
end
end
end
end
describe "#feedback_for_current_attempt?" do
before(:once) do
@teacher = course_with_user("TeacherEnrollment", course: @course, name: "Teacher", active_all: true).user
@student = course_with_user("StudentEnrollment", course: @course, name: "Student", active_all: true).user
@peer = course_with_user("StudentEnrollment", course: @course, name: "Peer", active_all: true).user
@assignment = @course.assignments.create!(name: "HasFeedback Assignment")
@submission = @assignment.submissions.find_by(user: @student)
end
it "is true when a teacher leaves a comment" do
@submission.attempt = 1
@submission.add_comment(author: @teacher, comment: "Teacher comment", attempt: 1)
expect(@submission).to be_feedback_for_current_attempt
end
it "is true when a peer leaves a comment" do
@submission.attempt = 1
@submission.add_comment(author: @peer, comment: "Peer comment", attempt: 1)
expect(@submission).to be_feedback_for_current_attempt
end
it "is true when a teacher leaves a comment on the latest attempt" do
@submission.attempt = 3
@submission.add_comment(author: @teacher, comment: "Teacher comment", attempt: 3)
expect(@submission).to be_feedback_for_current_attempt
end
it "is true when a teacher has left a comment prior to the first attempt (nil)" do
@submission.add_comment(author: @teacher, comment: "Teacher comment", attempt: nil)
expect(@submission).to be_feedback_for_current_attempt
end
it "is true when a teacher has left a comment prior to the first attempt (zero)" do
@submission.add_comment(author: @teacher, comment: "Teacher comment", attempt: 0)
expect(@submission).to be_feedback_for_current_attempt
end
it "is true when a teacher leaves a comment prior to the first attempt and it has been submitted" do
@submission.attempt = 1
@submission.add_comment(author: @teacher, comment: "Teacher comment", attempt: nil)
expect(@submission).to be_feedback_for_current_attempt
end
it "is false when no comments exist" do
expect(@submission).not_to be_feedback_for_current_attempt
end
it "is false when a teacher leaves comment on prior attempt" do
@submission.attempt = 2
@submission.add_comment(author: @teacher, comment: "Teacher comment", attempt: 1)
expect(@submission).not_to be_feedback_for_current_attempt
end
it "is false when a teacher leaves a comment prior to first attempt and a second is started" do
@submission.attempt = 2
@submission.add_comment(author: @teacher, comment: "Teacher comment", attempt: nil)
expect(@submission).not_to be_feedback_for_current_attempt
end
it "is false when a peer leaves comment on prior attempt" do
@submission.attempt = 2
@submission.add_comment(author: @peer, comment: "Peer comment", attempt: 1)
expect(@submission).not_to be_feedback_for_current_attempt
end
it "is false when only submitter has commented on the current attempt" do
@submission.attempt = 1
@submission.add_comment(author: @student, comment: "Student comment", attempt: 1)
expect(@submission).not_to be_feedback_for_current_attempt
end
end
describe "#visible_submission_comments_for" do
before(:once) do
@teacher = course_with_user("TeacherEnrollment", course: @course, name: "Teacher", active_all: true).user
@first_ta = course_with_user("TaEnrollment", course: @course, name: "First Ta", active_all: true).user
@second_ta = course_with_user("TaEnrollment", course: @course, name: "Second Ta", active_all: true).user
@third_ta = course_with_user("TaEnrollment", course: @course, name: "Third Ta", active_all: true).user
@student = course_with_user("StudentEnrollment", course: @course, name: "Student", active_all: true).user
@admin = account_admin_user(account: @course.account)
@assignment = @course.assignments.create!(name: "plain assignment")
@assignment.ensure_post_policy(post_manually: true)
@submission = @assignment.submissions.find_by(user: @student)
@student_comment = @submission.add_comment(author: @student, comment: "Student comment")
@teacher_comment = @submission.add_comment(author: @teacher, comment: "Teacher comment")
@first_ta_comment = @submission.add_comment(author: @first_ta, comment: "First Ta comment")
end
it "shows teacher all comments" do
comments = @submission.visible_submission_comments_for(@teacher)
expect(comments).to match_array([@student_comment, @teacher_comment, @first_ta_comment])
end
it "shows ta all comments" do
comments = @submission.visible_submission_comments_for(@first_ta)
expect(comments).to match_array([@student_comment, @teacher_comment, @first_ta_comment])
end
it "shows student all comments, when submission is posted" do
@submission.update!(posted_at: Time.zone.now)
comments = @submission.visible_submission_comments_for(@student)
expect(comments).to match_array([@student_comment, @teacher_comment, @first_ta_comment])
end
it "shows student only their own comment, when submission is unposted" do
comments = @submission.visible_submission_comments_for(@student)
expect(comments).to match_array([@student_comment])
end
context "for an assignment with peer reviews" do
let_once(:assignment) do
@course.assignments.create!(name: "peer review assignment", peer_reviews: true, muted: true)
end
before(:once) do
assignment.ensure_post_policy(post_manually: true)
@submission = assignment.submissions.find_by(user: @student)
@student2 = course_with_user("StudentEnrollment", course: @course, name: "Student2", active_all: true).user
student2_sub = assignment.submissions.find_by(user: @student2)
student2_request = AssessmentRequest.create!(assessor: @student2, assessor_asset: student2_sub, asset: @submission, user: @student)
@teacher_comment = @submission.add_comment(author: @teacher, comment: "A teacher comment")
@peer_review_comment = @submission.add_comment(author: @student2, comment: "A peer reviewer's comment", assessment_request: student2_request)
@student_comment = @submission.add_comment(author: @student, comment: "A comment by the submitter")
end
context "when grades are hidden" do
before(:once) do
other_assessor = @course.enroll_student(User.create!(name: "Student3")).user
other_request = AssessmentRequest.create!(
assessor: other_assessor,
assessor_asset: @assignment.submission_for_student(other_assessor),
asset: @submission,
user: @student
)
@alternate_assessment_comment = @submission.add_comment(author: other_assessor, comment: "Other assessment", assessment_request: other_request)
end
it "shows the submitting student their own comments and any peer review comments" do
comments = @submission.visible_submission_comments_for(@student)
expect(comments).to match_array([@peer_review_comment, @student_comment, @alternate_assessment_comment])
end
it "shows a peer-reviewing student only their own comments" do
comments = @submission.visible_submission_comments_for(@student2)
expect(comments).to match_array([@peer_review_comment])
end
end
context "when grades have been posted" do
before(:once) do
assignment.post_submissions
end
it "shows the submitting student comments from all users" do
comments = @submission.visible_submission_comments_for(@student)
expect(comments).to match_array([@peer_review_comment, @student_comment, @teacher_comment])
end
it "shows a peer-reviewing student only their own comments" do
comments = @submission.visible_submission_comments_for(@student2)
expect(comments).to match_array([@peer_review_comment])
end
end
end
context "when assignment is graded as a group" do
let_once(:all_groups) { @course.group_categories.create!(name: "all groups") }
before(:once) do
student2 = course_with_user("StudentEnrollment", course: @course, name: "Student2", active_all: true).user
group1 = all_groups.groups.create!(context: @course)
group1.add_user(@student)
group1.add_user(student2)
assignment = @course.assignments.create!(
grade_group_students_individually: false,
group_category: all_groups,
name: "group assignment"
)
@submission = assignment.submissions.find_by(user: @student)
@student_comment = @submission.add_comment(author: @student, comment: "Student comment", group_comment_id: group1.id)
@student2_comment = @submission.add_comment(author: student2, comment: "Student2 comment", group_comment_id: group1.id)
end
it "returns comments scoped to that group" do
comments = @submission.visible_submission_comments_for(@teacher)
expect(comments).to match_array([@student_comment, @student2_comment])
end
context "when peer reviews are enabled" do
before(:once) do
@student = @course.enroll_student(User.create!, enrollment_state: "active").user
@student2 = @course.enroll_student(User.create!, enrollment_state: "active").user
all_groups.groups.create!(context: @course).add_user(@student)
all_groups.groups.create!(context: @course).add_user(@student2)
assignment = @course.assignments.create!(
grade_group_students_individually: false,
group_category: all_groups,
name: "group assignment",
peer_reviews: true
)
@submission = assignment.submissions.find_by(user: @student)
student2_sub = assignment.submissions.find_by(user: @student2)
AssessmentRequest.create!(
assessor: @student2,
assessor_asset: student2_sub,
asset: @submission,
user: @student
)
@peer_review_comment = @submission.add_comment(author: @student2, comment: "Student2", group_comment_id: "ab")
@student_comment = @submission.add_comment(author: @student, comment: "Student", group_comment_id: "ac")
@teacher_comment = @submission.add_comment(author: @teacher, comment: "Teacher", group_comment_id: "ad")
end
it "shows a peer reviewer only their own comments" do
comments = @submission.visible_submission_comments_for(@student2)
expect(comments).to match_array([@peer_review_comment])
end
it "shows all comments to the submitting student" do
comments = @submission.visible_submission_comments_for(@student)
expect(comments).to match_array([@peer_review_comment, @student_comment, @teacher_comment])
end
it "shows all comments to a teacher" do
comments = @submission.visible_submission_comments_for(@teacher)
expect(comments).to match_array([@peer_review_comment, @student_comment, @teacher_comment])
end
end
end
context "when the assignment is a group peer-reviewed assignment" do
let_once(:student1) { @course.enroll_student(User.create!, active_all: true).user }
let_once(:student2) { @course.enroll_student(User.create!, active_all: true).user }
let_once(:student3) { @course.enroll_student(User.create!, active_all: true).user }
let_once(:student4) { @course.enroll_student(User.create!, active_all: true).user }
let_once(:group_category) do
group_category = @course.group_categories.create!(name: "a group")
group_category.create_groups(3)
group_category.groups.first.add_user(student1)
group_category.groups.second.add_user(student2)
group_category.groups.second.add_user(student3)
group_category.groups.third.add_user(student4)
group_category
end
let_once(:assignment) do
@course.assignments.create!(group_category:, peer_reviews: true)
end
before(:once) do
assignment.submit_homework(student1, body: "I am student 1")
assignment.submit_homework(student2, body: "I am student 2")
assignment.submit_homework(student3, body: "I am student 3")
assignment.submit_homework(student4, body: "I am student 4")
assignment.assign_peer_review(student1, student2)
assignment.assign_peer_review(student1, student4)
end
context "when the assignment is manually posted" do
before(:once) do
assignment.post_policy.update!(post_manually: true)
# Call update_submission to post the comment (rather than add_comment)
# so that it gets propagated to other group members
student2_submission_params = {
assessment_request: AssessmentRequest.find_by(assessor: student1, user: student2),
author: student1,
comment: "good job",
group_comment: true
}
assignment.update_submission(student2, student2_submission_params)
student4_submission_params = {
assessment_request: AssessmentRequest.find_by(assessor: student1, user: student4),
author: student1,
comment: "bad job",
group_comment: true
}
assignment.update_submission(student4, student4_submission_params)
end
it "allows the specific recipient of the comment to view it" do
comment = SubmissionComment.find_by(submission: assignment.submission_for_student(student2), author: student1)
expect(comment).to be_grants_right(student2, :read)
end
it "allows other students in the recipient's group to view their respective comment" do
comment = SubmissionComment.find_by(submission: assignment.submission_for_student(student3), author: student1)
expect(comment).to be_grants_right(student3, :read)
end
it "does not allow assessed students in a different group to view the comment" do
comment = SubmissionComment.find_by(submission: assignment.submission_for_student(student2), author: student1)
expect(comment).not_to be_grants_right(student4, :read)
end
end
end
context "for a moderated assignment" do
before(:once) do
@assignment = @course.assignments.create!(
name: "moderated assignment",
moderated_grading: true,
grader_count: 10,
final_grader: @teacher
)
@assignment.grade_student(@student, grade: 1, grader: @first_ta, provisional: true)
@assignment.grade_student(@student, grade: 1, grader: @second_ta, provisional: true)
@assignment.grade_student(@student, grade: 1, grader: @teacher, provisional: true)
@submission = @assignment.submissions.find_by(user: @student)
@student_comment = @submission.add_comment(author: @student, comment: "Student comment")
@first_ta_comment = @submission.add_comment(author: @first_ta, comment: "First Ta comment", provisional: true)
@second_ta_comment = @submission.add_comment(author: @second_ta, comment: "Second Ta comment", provisional: true)
@third_ta_comment = @submission.add_comment(author: @third_ta, comment: "Third Ta comment", provisional: true)
@final_grader_comment = @submission.add_comment(author: @teacher, comment: "Final Grader comment", provisional: true)
end
context "when graders can view other graders' comments" do
context "when grades are unpublished" do
it "shows final grader all submission comments" do
comments = @submission.visible_submission_comments_for(@teacher)
expect(comments).to match_array([
@student_comment,
@first_ta_comment,
@second_ta_comment,
@third_ta_comment,
@final_grader_comment
])
end
it "shows provisional grader all submission comments" do
comments = @submission.visible_submission_comments_for(@first_ta)
expect(comments).to match_array([
@student_comment,
@first_ta_comment,
@second_ta_comment,
@third_ta_comment,
@final_grader_comment
])
end
it "shows student only their own comments" do
comments = @submission.visible_submission_comments_for(@student)
expect(comments).to match_array([@student_comment])
end
it "shows admins all submission comments" do
comments = @submission.visible_submission_comments_for(@admin)
expect(comments).to match_array([
@student_comment,
@first_ta_comment,
@second_ta_comment,
@third_ta_comment,
@final_grader_comment
])
end
end
context "when grades are published" do
before(:once) do
ModeratedGrading::ProvisionalGrade.find_by(submission: @submission, scorer: @first_ta).publish!
@assignment.update!(grades_published_at: Time.zone.now)
@submission.reload
end
it "shows final grader all submission comments" do
comments = @submission.visible_submission_comments_for(@teacher)
expect(comments.pluck(:comment)).to match_array([
"Student comment",
"First Ta comment",
"Second Ta comment",
"Third Ta comment",
"Final Grader comment"
])
end
it "shows provisional grader all submission comments" do
comments = @submission.visible_submission_comments_for(@first_ta)
expect(comments.pluck(:comment)).to match_array([
"Student comment",
"First Ta comment",
"Second Ta comment",
"Third Ta comment",
"Final Grader comment"
])
end
it "shows student only their own comments" do
comments = @submission.visible_submission_comments_for(@student)
expect(comments).to match_array([@student_comment])
end
it "when grades are posted, shows student their own, chosen grader's, and final grader's comments" do
@assignment.post_submissions
comments = @submission.visible_submission_comments_for(@student)
expect(comments.pluck(:comment)).to match_array([
"Student comment",
"First Ta comment",
"Final Grader comment"
])
end
it "shows admins all submission comments" do
comments = @submission.visible_submission_comments_for(@admin)
expect(comments.pluck(:comment)).to match_array([
"Student comment",
"First Ta comment",
"Second Ta comment",
"Third Ta comment",
"Final Grader comment"
])
end
end
end
context "when graders cannot view other graders' comments" do
before(:once) do
@assignment.update!(grader_comments_visible_to_graders: false)
end
context "when grades are unpublished" do
it "shows final grader all submission comments" do
comments = @submission.visible_submission_comments_for(@teacher)
expect(comments).to match_array([
@student_comment,
@first_ta_comment,
@second_ta_comment,
@third_ta_comment,
@final_grader_comment
])
end
it "shows provisional grader their own and student's" do
comments = @submission.visible_submission_comments_for(@second_ta)
expect(comments.pluck(:comment)).to match_array(["Student comment", "Second Ta comment"])
end
it "shows student only their own comments" do
comments = @submission.visible_submission_comments_for(@student)
expect(comments).to match_array([@student_comment])
end
it "shows admins all submission comments" do
comments = @submission.visible_submission_comments_for(@admin)
expect(comments).to match_array([
@student_comment,
@first_ta_comment,
@second_ta_comment,
@third_ta_comment,
@final_grader_comment
])
end
end
context "when grades are published" do
before(:once) do
ModeratedGrading::ProvisionalGrade.find_by(submission: @submission, scorer: @first_ta).publish!
@assignment.update!(grades_published_at: Time.zone.now)
@submission.reload
end
it "shows final grader all submission comments" do
comments = @submission.visible_submission_comments_for(@teacher)
expect(comments.pluck(:comment)).to match_array([
"Student comment",
"First Ta comment",
"Second Ta comment",
"Third Ta comment",
"Final Grader comment"
])
end
it "shows provisional grader their own, student's, chosen grader's, and final grader's comments" do
comments = @submission.visible_submission_comments_for(@second_ta)
expect(comments.pluck(:comment)).to match_array([
"Student comment",
"First Ta comment",
"Second Ta comment",
"Final Grader comment"
])
end
it "shows student only their own comments" do
comments = @submission.visible_submission_comments_for(@student)
expect(comments).to match_array([@student_comment])
end
it "when grades are posted, shows student own, chosen grader's, and final grader's comments" do
@assignment.post_submissions
comments = @submission.visible_submission_comments_for(@student)
expect(comments.pluck(:comment)).to match_array([
"Student comment",
"First Ta comment",
"Final Grader comment"
])
end
it "shows admins all submission comments" do
comments = @submission.visible_submission_comments_for(@admin)
expect(comments.pluck(:comment)).to match_array([
"Student comment",
"First Ta comment",
"Second Ta comment",
"Third Ta comment",
"Final Grader comment"
])
end
end
end
end
end
describe ".needs_grading" do
before :once do
@submission = @assignment.submit_homework(@student, submission_type: "online_text_entry", body: "a body")
end
it "includes submission that has not been graded" do
expect(Submission.needs_grading.count).to eq(1)
end
it "includes submission by enrolled student" do
@student.enrollments.take!.complete
expect(Submission.needs_grading.count).to eq(0)
@course.enroll_student(@student).accept
expect(Submission.needs_grading.count).to eq(1)
end
it "includes submission by user with multiple enrollments in the course only once" do
another_section = @course.course_sections.create(name: "two")
@course.enroll_student(@student, section: another_section, allow_multiple_enrollments: true).accept
expect(Submission.needs_grading.count).to eq(1)
end
it "does not include submission that has been graded" do
@assignment.grade_student(@student, grade: "100", grader: @teacher)
expect(Submission.needs_grading.count).to eq(0)
end
it "does include submissions that have been graded but the score was reset to nil" do
@assignment.grade_student(@student, grade: "100", grader: @teacher)
@assignment.grade_student(@student, grade: nil, grader: @teacher)
expect(Submission.needs_grading.count).to eq(1)
end
it "does not include submission by non-student user" do
@student.enrollments.take!.complete
@course.enroll_user(@student, "TaEnrollment").accept
expect(Submission.needs_grading.count).to eq(0)
end
it "does not include excused submissions" do
@assignment.grade_student(@student, excused: true, grader: @teacher)
expect(Submission.needs_grading.count).to eq(0)
end
it "does not include submissions for inactive/concluded students who have other active enrollments somewhere" do
@course.enroll_student(@student).update_attribute(:workflow_state, "inactive")
course_with_student(user: @student, active_all: true)
expect(Submission.needs_grading).not_to include @assignment.submissions.first
end
context "sharding" do
specs_require_sharding
it "serializes relative to current scope's shard" do
@shard1.activate do
expect(Submission.shard(Shard.default).needs_grading.count).to eq(1)
end
end
it "works with cross shard attachments" do
@shard1.activate do
@student = user_factory(active_user: true)
@attachment = Attachment.create! uploaded_data: StringIO.new("blah"), context: @student, filename: "blah.txt"
end
course_factory(active_all: true)
@course.enroll_user(@student, "StudentEnrollment").accept!
@assignment = @course.assignments.create!
sub = @assignment.submit_homework(@user, attachments: [@attachment])
expect(sub.attachments).to eq [@attachment]
end
it "bulk_load_versioned_attachments works with attachments in a different shard" do
course_factory(active_all: true)
student = user_factory(active_user: true)
attachment = attachment_model(filename: "submission.doc", context: student)
@course.enroll_user(student, "StudentEnrollment").accept!
assignment = @course.assignments.create!
submission = assignment.submit_homework(student, attachments: [attachment])
submission.update_attribute(:attachment_ids, attachment.id.to_s)
@shard1.activate do
submission_with_attachments = Submission.bulk_load_versioned_attachments([submission]).first
expect(submission_with_attachments.versioned_attachments).to eq [attachment]
end
end
end
end
describe "#can_view_details?" do
before do
@assignment.update!(anonymous_grading: true)
@submission = @assignment.submit_homework(@student, submission_type: "online_text_entry", body: "a body")
end
context "for observers" do
let(:observer) do
course_with_observer(
course: @assignment.course,
associated_user_id: @submission.user_id,
active_all: true
).user
end
it "allows observers of the submission's owner to view details" do
expect(@submission).to be_can_view_details(observer)
end
it "does not allow observers to view details if they're not observing the submission's owner" do
new_student = User.create!
@context.enroll_student(new_student, enrollment_state: "active")
new_student_submission = @assignment.submissions.find_by(user: new_student)
expect(new_student_submission).not_to be_can_view_details(observer)
end
end
context "for peer reviewers" do
let(:reviewer) { @context.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user }
let(:reviewer_sub) { @assignment.submissions.find_by!(user: reviewer) }
before do
@assignment.update!(submission_types: "online_text_entry", peer_reviews: true)
end
it "returns false for peer reviewer of student under view that has not submitted" do
AssessmentRequest.create!(assessor: reviewer, assessor_asset: reviewer_sub, asset: @submission, user: @student)
expect(@submission.can_view_details?(reviewer)).to be false
end
it "returns true for peer reviewer of student under view that has submitted" do
AssessmentRequest.create!(assessor: reviewer, assessor_asset: reviewer_sub, asset: @submission, user: @student)
@assignment.submit_homework(reviewer, body: "hi")
expect(@submission.can_view_details?(reviewer)).to be true
end
it "returns false for peer reviewer of student not under view" do
new_student = @context.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user
new_student_sub = @assignment.submissions.find_by!(user: new_student)
expect(new_student_sub.can_view_details?(reviewer)).to be false
end
end
context "when the assignment is muted" do
it "returns false if user isn't present" do
expect(@submission).not_to be_can_view_details(nil)
end
it "returns true for submitting student if assignment anonymous grading" do
expect(@submission.can_view_details?(@student)).to be true
end
it "returns false for non-submitting student if assignment anonymous grading" do
new_student = User.create!
@context.enroll_student(new_student, enrollment_state: "active")
expect(@submission.can_view_details?(@new_student)).to be false
end
it "returns false for teacher if assignment anonymous grading" do
expect(@submission.can_view_details?(@teacher)).to be false
end
it "returns false for admin if assignment anonymous grading" do
expect(@submission.can_view_details?(account_admin_user)).to be false
end
it "returns true for site admin if assignment anonymous grading" do
expect(@submission.can_view_details?(site_admin_user)).to be true
end
end
context "when the assignment is unmuted" do
before do
@assignment.unmute!
end
it "returns false if user isn't present" do
expect(@submission).not_to be_can_view_details(nil)
end
it "returns true for submitting student if assignment anonymous grading" do
expect(@submission.can_view_details?(@student)).to be true
end
it "returns false for non-submitting student if assignment anonymous grading" do
new_student = User.create!
@context.enroll_student(new_student, enrollment_state: "active")
expect(@submission.can_view_details?(@new_student)).to be false
end
it "returns true for teacher if assignment anonymous grading" do
expect(@submission.can_view_details?(@teacher)).to be true
end
it "returns true for admin if assignment anonymous grading" do
expect(@submission.can_view_details?(account_admin_user)).to be true
end
it "returns true for site admin if assignment anonymous grading" do
expect(@submission.can_view_details?(site_admin_user)).to be true
end
end
end
describe "#needs_grading?" do
before :once do
@submission = @assignment.submit_homework(@student, submission_type: "online_text_entry", body: "a body")
end
it "returns true for submission that has not been graded" do
expect(@submission.needs_grading?).to be true
end
it "returns false for submission that has been graded" do
@assignment.grade_student(@student, grade: "100", grader: @teacher)
@submission.reload
expect(@submission.needs_grading?).to be false
end
it "returns true for submission that has been graded but the score was reset to nil" do
@assignment.grade_student(@student, grade: "100", grader: @teacher)
@assignment.grade_student(@student, grade: nil, grader: @teacher)
@submission.reload
expect(@submission.needs_grading?).to be true
end
it "returns true for submission that is pending review" do
@submission.workflow_state = "pending_review"
expect(@submission.needs_grading?).to be true
end
it "returns false for submission with nil submission_type" do
@submission.submission_type = nil
expect(@submission.needs_grading?).to be false
end
end
describe "#plagiarism_service_to_use" do
it "returns nil when no service is configured" do
submission = @assignment.submit_homework(@student,
submission_type: "online_text_entry",
body: "whee")
expect(submission.plagiarism_service_to_use).to be_nil
end
it "returns :turnitin when only turnitin is configured" do
setup_account_for_turnitin(@context.account)
submission = @assignment.submit_homework(@student,
submission_type: "online_text_entry",
body: "whee")
expect(submission.plagiarism_service_to_use).to eq(:turnitin)
end
it "returns :vericite when only vericite is configured" do
plugin = Canvas::Plugin.find(:vericite)
PluginSetting.create!(name: plugin.id, settings: plugin.default_settings, disabled: false)
submission = @assignment.submit_homework(@student,
submission_type: "online_text_entry",
body: "whee")
expect(submission.plagiarism_service_to_use).to eq(:vericite)
end
it "returns :vericite when both vericite and turnitin are enabled" do
setup_account_for_turnitin(@context.account)
plugin = Canvas::Plugin.find(:vericite)
PluginSetting.create!(name: plugin.id, settings: plugin.default_settings, disabled: false)
submission = @assignment.submit_homework(@student,
submission_type: "online_text_entry",
body: "whee")
expect(submission.plagiarism_service_to_use).to eq(:vericite)
end
end
describe "#resubmit_to_vericite" do
it "calls resubmit_to_plagiarism_later" do
plugin = Canvas::Plugin.find(:vericite)
PluginSetting.create!(name: plugin.id, settings: plugin.default_settings, disabled: false)
submission = @assignment.submit_homework(@student,
submission_type: "online_text_entry",
body: "whee")
expect(submission).to receive(:submit_to_plagiarism_later).once
submission.resubmit_to_vericite
end
end
describe "scope: late" do
before :once do
@now = Time.zone.now
### Quizzes
@quiz = generate_quiz(@course)
@quiz_assignment = @quiz.assignment
@unsubmitted_quiz_submission = @assignment.submissions.create(user: User.create, submission_type: "online_quiz")
Submission.where(id: @unsubmitted_quiz_submission.id).update_all(submitted_at: nil, cached_due_date: nil)
@ongoing_unsubmitted_quiz = generate_quiz_submission(@quiz, student: User.create)
@ongoing_unsubmitted_quiz_submission = @ongoing_unsubmitted_quiz.submission
@ongoing_unsubmitted_quiz_submission.save!
Submission.where(id: @ongoing_unsubmitted_quiz_submission.id).update_all(submitted_at: nil)
@timely_quiz1 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
@timely_quiz1_submission = @timely_quiz1.submission
Submission.where(id: @timely_quiz1_submission.id).update_all(submitted_at: @now, cached_due_date: nil)
@timely_quiz2 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
@timely_quiz2_submission = @timely_quiz2.submission
Submission.where(id: @timely_quiz2_submission.id).update_all(submitted_at: @now, cached_due_date: @now + 1.hour)
@timely_quiz3 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
@timely_quiz3_submission = @timely_quiz3.submission
Submission.where(id: @timely_quiz3_submission.id)
.update_all(submitted_at: @now, cached_due_date: @now - 45.seconds)
@late_quiz1 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
@late_quiz1_submission = @late_quiz1.submission
Submission.where(id: @late_quiz1_submission).update_all(submitted_at: @now, cached_due_date: @now - 61.seconds)
@late_quiz2 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
@late_quiz2_submission = @late_quiz2.submission
Submission.where(id: @late_quiz2_submission).update_all(submitted_at: @now, cached_due_date: @now - 1.hour)
@late_quiz_extended = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
@late_quiz_extended_submission = @late_quiz_extended.submission
Submission.where(id: @late_quiz_extended_submission).update_all(submitted_at: @now, cached_due_date: @now - 1.hour, late_policy_status: "extended")
@timely_quiz_marked_late = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
@timely_quiz_marked_late_submission = @timely_quiz_marked_late.submission
Submission.where(id: @timely_quiz_marked_late_submission).update_all(submitted_at: @now, cached_due_date: nil)
Submission.where(id: @timely_quiz_marked_late_submission).update_all(late_policy_status: "late")
@ongoing_late_quiz1 = generate_quiz_submission(@quiz, student: User.create)
@ongoing_late_quiz1_submission = @ongoing_late_quiz1.submission
@ongoing_late_quiz1_submission.save!
Submission.where(id: @ongoing_late_quiz1_submission)
.update_all(submitted_at: @now, cached_due_date: @now - 61.seconds)
@ongoing_late_quiz2 = generate_quiz_submission(@quiz, student: User.create)
@ongoing_late_quiz2_submission = @ongoing_late_quiz2.submission
@ongoing_late_quiz2_submission.save!
Submission.where(id: @ongoing_late_quiz2_submission)
.update_all(submitted_at: @now, cached_due_date: @now - 1.hour)
@ongoing_timely_quiz_marked_late = generate_quiz_submission(@quiz, student: User.create)
@ongoing_timely_quiz_marked_late_submission = @ongoing_timely_quiz_marked_late.submission
@ongoing_timely_quiz_marked_late_submission.save!
Submission.where(id: @ongoing_timely_quiz_marked_late_submission)
.update_all(submitted_at: @now, cached_due_date: nil, late_policy_status: "late")
### Homeworks
@unsubmitted_hw = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
Submission.where(id: @unsubmitted_hw.id).update_all(submitted_at: nil, cached_due_date: nil)
@timely_hw1 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
Submission.where(id: @timely_hw1.id).update_all(submitted_at: @now, cached_due_date: nil)
@timely_hw2 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
Submission.where(id: @timely_hw2.id).update_all(submitted_at: @now, cached_due_date: @now + 1.hour)
@late_hw1 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
Submission.where(id: @late_hw1.id).update_all(submitted_at: @now, cached_due_date: @now - 45.seconds)
@late_hw2 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
Submission.where(id: @late_hw2.id).update_all(submitted_at: @now, cached_due_date: @now - 61.seconds)
@late_hw3 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
Submission.where(id: @late_hw3.id).update_all(submitted_at: @now, cached_due_date: @now - 1.hour)
@late_hw_excused = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
Submission.where(id: @late_hw_excused.id).update_all(submitted_at: @now, cached_due_date: @now - 1.hour, excused: true)
@timely_hw_marked_late = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
Submission.where(id: @timely_hw_marked_late.id).update_all(submitted_at: @now, cached_due_date: nil)
Submission.where(id: @timely_hw_marked_late.id).update_all(late_policy_status: "late")
@late_submission_ids = Submission.late.map(&:id)
end
### Quizzes
it "excludes unsubmitted quizzes" do
expect(@late_submission_ids).not_to include(@unsubmitted_quiz_submission.id)
end
it "excludes ongoing quizzes that have never been submitted before" do
expect(@late_submission_ids).not_to include(@ongoing_unsubmitted_quiz_submission.id)
end
it "excludes quizzes submitted with no due date" do
expect(@late_submission_ids).not_to include(@timely_quiz1_submission.id)
end
it "excludes quizzes submitted before the due date" do
expect(@late_submission_ids).not_to include(@timely_quiz2_submission.id)
end
it "excludes quizzes submitted less than 60 seconds after the due date" do
expect(@late_submission_ids).not_to include(@timely_quiz3_submission.id)
end
it "includes quizzes submitted more than 60 seconds after the due date" do
expect(@late_submission_ids).to include(@late_quiz1_submission.id)
end
it "excludes quizzes that were last submitted more than 60 seconds after the due date but are being retaken" do
expect(@late_submission_ids).not_to include(@ongoing_late_quiz1_submission.id)
end
it "includes quizzes submitted after the due date" do
expect(@late_submission_ids).to include(@late_quiz2_submission.id)
end
it "excludes quizzes that were last submitted after the due date but are being retaken" do
expect(@late_submission_ids).not_to include(@ongoing_late_quiz2_submission.id)
end
it "includes quizzes that have been manually marked as late" do
expect(@late_submission_ids).to include(@timely_quiz_marked_late_submission.id)
end
it "includes quizzes that have been manually marked as late but are being retaken" do
expect(@late_submission_ids).to include(@ongoing_timely_quiz_marked_late_submission.id)
end
it "excludes quizzes that are late but have been marked as extended" do
expect(@late_submission_ids).not_to include(@late_quiz_extended_submission.id)
end
### Homeworks
it "excludes an otherwise late submission that has been marked with a custom status" do
admin = account_admin_user(account: @course.root_account)
custom_grade_status = @course.root_account.custom_grade_statuses.create!(
name: "Custom Status",
color: "#ABC",
created_by: admin
)
expect { @late_hw1.update!(custom_grade_status:) }.to change {
Submission.late.include?(@late_hw1)
}.from(true).to(false)
end
it "excludes unsubmitted homeworks" do
expect(@late_submission_ids).not_to include(@unsubmitted_hw.id)
end
it "excludes homeworks submitted with no due date" do
expect(@late_submission_ids).not_to include(@timely_hw1.id)
end
it "excludes homeworks submitted before the due date" do
expect(@late_submission_ids).not_to include(@timely_hw2.id)
end
it "includes homeworks submitted less than 60 seconds after the due date" do
expect(@late_submission_ids).to include(@late_hw1.id)
end
it "includes homeworks submitted more than 60 seconds after the due date" do
expect(@late_submission_ids).to include(@late_hw2.id)
end
it "includes homeworks submitted after the due date" do
expect(@late_submission_ids).to include(@late_hw3.id)
end
it "excludes excused homework submitted after the due date" do
expect(@late_submission_ids).not_to include(@late_hw_excused.id)
end
it "includes homeworks that have been manually marked as late" do
expect(@late_submission_ids).to include(@timely_hw_marked_late.id)
end
end
describe "scope: not_late" do
before :once do
@now = Time.zone.now
### Quizzes
@quiz = generate_quiz(@course)
@quiz_assignment = @quiz.assignment
@unsubmitted_quiz_submission = @assignment.submissions.create(user: User.create, submission_type: "online_quiz")
Submission.where(id: @unsubmitted_quiz_submission.id).update_all(submitted_at: nil, cached_due_date: nil)
@ongoing_unsubmitted_quiz = generate_quiz_submission(@quiz, student: User.create)
@ongoing_unsubmitted_quiz_submission = @ongoing_unsubmitted_quiz.submission
@ongoing_unsubmitted_quiz_submission.save!
Submission.where(id: @ongoing_unsubmitted_quiz_submission.id).update_all(submitted_at: nil)
@timely_quiz1 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
@timely_quiz1_submission = @timely_quiz1.submission
Submission.where(id: @timely_quiz1_submission.id).update_all(submitted_at: @now, cached_due_date: nil)
@timely_quiz2 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
@timely_quiz2_submission = @timely_quiz2.submission
Submission.where(id: @timely_quiz2_submission.id).update_all(submitted_at: @now, cached_due_date: @now + 1.hour)
@timely_quiz3 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
@timely_quiz3_submission = @timely_quiz3.submission
Submission.where(id: @timely_quiz3_submission.id)
.update_all(submitted_at: @now, cached_due_date: @now - 45.seconds)
@late_quiz1 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
@late_quiz1_submission = @late_quiz1.submission
Submission.where(id: @late_quiz1_submission).update_all(submitted_at: @now, cached_due_date: @now - 61.seconds)
@late_quiz2 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
@late_quiz2_submission = @late_quiz2.submission
Submission.where(id: @late_quiz2_submission).update_all(submitted_at: @now, cached_due_date: @now - 1.hour)
@late_quiz_extended = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
@late_quiz_extended_submission = @late_quiz_extended.submission
Submission.where(id: @late_quiz_extended_submission).update_all(submitted_at: @now, cached_due_date: @now - 1.hour, late_policy_status: "extended")
@timely_quiz_marked_late = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
@timely_quiz_marked_late_submission = @timely_quiz_marked_late.submission
Submission.where(id: @timely_quiz_marked_late_submission).update_all(submitted_at: @now, cached_due_date: nil)
Submission.where(id: @timely_quiz_marked_late_submission).update_all(late_policy_status: "late")
@ongoing_late_quiz1 = generate_quiz_submission(@quiz, student: User.create)
@ongoing_late_quiz1_submission = @ongoing_late_quiz1.submission
@ongoing_late_quiz1_submission.save!
Submission.where(id: @ongoing_late_quiz1_submission)
.update_all(submitted_at: @now, cached_due_date: @now - 61.seconds)
@ongoing_late_quiz2 = generate_quiz_submission(@quiz, student: User.create)
@ongoing_late_quiz2_submission = @ongoing_late_quiz2.submission
@ongoing_late_quiz2_submission.save!
Submission.where(id: @ongoing_late_quiz2_submission)
.update_all(submitted_at: @now, cached_due_date: @now - 1.hour)
@ongoing_timely_quiz_marked_late = generate_quiz_submission(@quiz, student: User.create)
@ongoing_timely_quiz_marked_late_submission = @ongoing_timely_quiz_marked_late.submission
@ongoing_timely_quiz_marked_late_submission.save!
Submission.where(id: @ongoing_timely_quiz_marked_late_submission)
.update_all(submitted_at: @now, cached_due_date: nil, late_policy_status: "late")
### Homeworks
@unsubmitted_hw = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
Submission.where(id: @unsubmitted_hw.id).update_all(submitted_at: nil, cached_due_date: nil)
@timely_hw1 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
Submission.where(id: @timely_hw1.id).update_all(submitted_at: @now, cached_due_date: nil)
@timely_hw2 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
Submission.where(id: @timely_hw2.id).update_all(submitted_at: @now, cached_due_date: @now + 1.hour)
@late_hw1 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
Submission.where(id: @late_hw1.id).update_all(submitted_at: @now, cached_due_date: @now - 45.seconds)
@late_hw2 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
Submission.where(id: @late_hw2.id).update_all(submitted_at: @now, cached_due_date: @now - 61.seconds)
@late_hw3 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
Submission.where(id: @late_hw3.id).update_all(submitted_at: @now, cached_due_date: @now - 1.hour)
@late_hw_excused = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
Submission.where(id: @late_hw_excused.id).update_all(submitted_at: @now, cached_due_date: @now - 1.hour, excused: true)
@timely_hw_marked_late = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
Submission.where(id: @timely_hw_marked_late.id).update_all(submitted_at: @now, cached_due_date: nil)
Submission.where(id: @timely_hw_marked_late.id).update_all(late_policy_status: "late")
@not_late_submission_ids = Submission.not_late.map(&:id)
end
### Quizzes
it "includes unsubmitted quizzes" do
expect(@not_late_submission_ids).to include(@unsubmitted_quiz_submission.id)
end
it "includes ongoing quizzes that have never been submitted before" do
expect(@not_late_submission_ids).to include(@ongoing_unsubmitted_quiz_submission.id)
end
it "includes quizzes submitted with no due date" do
expect(@not_late_submission_ids).to include(@timely_quiz1_submission.id)
end
it "includes quizzes submitted before the due date" do
expect(@not_late_submission_ids).to include(@timely_quiz2_submission.id)
end
it "includes quizzes submitted less than 60 seconds after the due date" do
expect(@not_late_submission_ids).to include(@timely_quiz3_submission.id)
end
it "excludes quizzes submitted more than 60 seconds after the due date" do
expect(@not_late_submission_ids).not_to include(@late_quiz1_submission.id)
end
it "includes quizzes that were last submitted more than 60 seconds after the due date but are being retaken" do
expect(@not_late_submission_ids).to include(@ongoing_late_quiz1_submission.id)
end
it "excludes quizzes submitted after the due date" do
expect(@not_late_submission_ids).not_to include(@late_quiz2_submission.id)
end
it "includes quizzes that were last submitted after the due date but are being retaken" do
expect(@not_late_submission_ids).to include(@ongoing_late_quiz2_submission.id)
end
it "excludes quizzes that have been manually marked as late" do
expect(@not_late_submission_ids).not_to include(@timely_quiz_marked_late_submission.id)
end
it "excludes quizzes that have been manually marked as late but are being retaken" do
expect(@not_late_submission_ids).not_to include(@ongoing_timely_quiz_marked_late_submission.id)
end
it "includes quizzes that are late but have been marked as extended" do
expect(@not_late_submission_ids).to include(@late_quiz_extended_submission.id)
end
### Homeworks
it "includes an otherwise late submission that has been marked with a custom status" do
admin = account_admin_user(account: @course.root_account)
custom_grade_status = @course.root_account.custom_grade_statuses.create!(
name: "Custom Status",
color: "#ABC",
created_by: admin
)
expect { @late_hw1.update!(custom_grade_status:) }.to change {
Submission.not_late.include?(@late_hw1)
}.from(false).to(true)
end
it "includes unsubmitted homeworks" do
expect(@not_late_submission_ids).to include(@unsubmitted_hw.id)
end
it "includes homeworks submitted with no due date" do
expect(@not_late_submission_ids).to include(@timely_hw1.id)
end
it "includes homeworks submitted before the due date" do
expect(@not_late_submission_ids).to include(@timely_hw2.id)
end
it "excludes homeworks submitted less than 60 seconds after the due date" do
expect(@not_late_submission_ids).not_to include(@late_hw1.id)
end
it "excludes homeworks submitted more than 60 seconds after the due date" do
expect(@not_late_submission_ids).not_to include(@late_hw2.id)
end
it "excludes homeworks submitted after the due date" do
expect(@not_late_submission_ids).not_to include(@late_hw3.id)
end
it "includes excused homework submitted after the due date" do
expect(@not_late_submission_ids).to include(@late_hw_excused.id)
end
it "excludes homeworks that have been manually marked as late" do
expect(@not_late_submission_ids).not_to include(@timely_hw_marked_late.id)
end
end
describe "scope: with_assignment" do
it "excludes submissions to deleted assignments" do
expect { @assignment.destroy }.to change { @student.submissions.with_assignment.count }.by(-1)
end
end
describe "scope: for_assignment" do
it "includes all submissions for a given assignment" do
first_assignment = @assignment
@course.assignments.create!
submissions = Submission.for_assignment(@assignment)
expect(submissions).to match_array(first_assignment.submissions)
end
end
describe "#filter_attributes_for_user" do
let(:user) { instance_double("User", id: 1) }
let(:session) { {} }
let(:submission) { @assignment.submissions.build(user_id: 2) }
context "assignment is set to manually post grades" do
before do
@assignment.ensure_post_policy(post_manually: true)
@assignment.grade_student(@student, grader: @teacher, score: 5)
end
it "filters score" do
expect(submission.assignment).to receive(:user_can_read_grades?).and_return(false)
hash = { "score" => 10 }
expect(submission.filter_attributes_for_user(hash, user, session)).not_to have_key("score")
end
it "filters grade" do
expect(submission.assignment).to receive(:user_can_read_grades?).and_return(false)
hash = { "grade" => 10 }
expect(submission.filter_attributes_for_user(hash, user, session)).not_to have_key("grade")
end
it "filters published_score" do
expect(submission.assignment).to receive(:user_can_read_grades?).and_return(false)
hash = { "published_score" => 10 }
expect(submission.filter_attributes_for_user(hash, user, session)).not_to have_key("published_score")
end
it "filters published_grade" do
expect(submission.assignment).to receive(:user_can_read_grades?).and_return(false)
hash = { "published_grade" => 10 }
expect(submission.filter_attributes_for_user(hash, user, session)).not_to have_key("published_grade")
end
it "filters entered_score" do
expect(submission.assignment).to receive(:user_can_read_grades?).and_return(false)
hash = { "entered_score" => 10 }
expect(submission.filter_attributes_for_user(hash, user, session)).not_to have_key("entered_score")
end
it "filters entered_grade" do
expect(submission.assignment).to receive(:user_can_read_grades?).and_return(false)
hash = { "entered_grade" => 10 }
expect(submission.filter_attributes_for_user(hash, user, session)).not_to have_key("entered_grade")
end
end
end
describe "#provisional_grade" do
before(:once) do
@assignment.update!(moderated_grading: true, grader_count: 2, final_grader: @teacher)
@assignment.grade_student(@student, score: 10, grader: @teacher, provisional: true)
@assignment.grade_student(@student, score: 50, grader: @teacher, provisional: true, final: true)
end
let(:submission) { @assignment.submissions.first }
it "returns the provisional grade matching the passed-in scorer if provided" do
expect(submission.provisional_grade(@teacher).score).to eq 10
end
it "returns the final provisional grade if final is true" do
expect(submission.provisional_grade(@teacher, final: true).score).to eq 50
end
context "when no matching grade is found" do
let(:non_scorer) { User.new }
it "returns a null provisional grade by default" do
provisional_grade = submission.provisional_grade(non_scorer)
expect(provisional_grade).to be_a(ModeratedGrading::NullProvisionalGrade)
end
it "returns nil if default_to_null_grade is false" do
provisional_grade = submission.provisional_grade(non_scorer, default_to_null_grade: false)
expect(provisional_grade).to be_nil
end
end
end
describe "#update_line_item_result" do
let(:submission) { submission_model(assignment: @assignment) }
context "when lti_result does not exist" do
it "does nothing when there is no line item" do
expect do
submission.update!(score: 1.3)
end.to_not change { submission.lti_result }.from(nil)
end
context "when there is a line item" do
before { line_item_model(assignment: @assignment) }
it "does nothing if score has not changed" do
expect do
submission.update!(body: "hello abc")
end.to_not change { submission.lti_result }.from(nil)
end
it "creates an the lti_result with the correct score_given if the score has changed" do
expect do
submission.update!(score: 1.3)
end.to change { submission.lti_result&.reload&.result_score }.from(nil).to(1.3)
end
it "does nothing if the lti_result was updated by a tool" do
expect do
submission.update!(score: 1.3, grader_id: -123)
end.to_not change { submission.lti_result }.from(nil)
end
end
end
context "with lti_result" do
let(:lti_result) { lti_result_model(assignment: @assignment) }
let(:submission) { lti_result.submission }
it "does nothing if score has not changed" do
expect do
submission.save!
end.to_not change { lti_result.result_score }
end
it "updates the lti_result score_given if the score has changed" do
expect do
submission.update!(score: 1.3)
end.to change { lti_result.reload.result_score }.from(nil).to(1.3)
end
it "does nothing if the lti_result was updated by a tool" do
expect do
submission.update!(score: 1.3, grader_id: -123)
end.to_not change { lti_result.reload.result_score }
end
end
end
describe "#delete_ignores" do
context "for submission ignores" do
before :once do
@submission = @assignment.submissions.find_by!(user_id: @student)
@ignore = Ignore.create!(asset: @assignment, user: @student, purpose: "submitting")
end
it "deletes submission ignores when asset is submitted" do
@assignment.submit_homework(@student, { submission_type: "online_text_entry", body: "Hi" })
expect { @ignore.reload }.to raise_error ActiveRecord::RecordNotFound
end
it "does not delete submission ignores when asset is not submitted" do
@submission.student_entered_score = 5
@submission.save!
expect(@ignore.reload).to eq @ignore
end
it "deletes submission ignores when asset is excused" do
@submission.excused = true
@submission.save!
expect { @ignore.reload }.to raise_error ActiveRecord::RecordNotFound
end
end
context "for grading ignores" do
before :once do
@student1 = @student
@student2 = student_in_course(course: @course, active_all: true).user
@sub1 = @assignment.submit_homework(@student1, { submission_type: "online_text_entry", body: "Hi" })
@sub2 = @assignment.submit_homework(@student2, { submission_type: "online_text_entry", body: "Hi" })
@ignore = Ignore.create!(asset: @assignment, user: @teacher, purpose: "grading")
end
it "deletes grading ignores if every submission is graded or excused" do
@sub1.score = 5
@sub1.save!
@sub2.excused = true
@sub2.save!
expect { @ignore.reload }.to raise_error ActiveRecord::RecordNotFound
end
it "does not delete grading ignores if some submissions are ungraded" do
@sub1.score = 5
@sub1.save!
expect(@ignore.reload).to eq @ignore
end
end
end
def submission_spec_model(opts = {})
submit_homework = opts.delete(:submit_homework)
opts = submit_homework ? @valid_attributes.merge(opts) : @valid_attributes.except(:workflow_state, :url).merge(opts)
assignment = opts.delete(:assignment) || Assignment.find(opts.delete(:assignment_id))
user = opts.delete(:user) || User.find(opts.delete(:user_id))
@submission = if submit_homework
assignment.submit_homework(user)
else
assignment.submissions.find_by!(user:)
end
unless assignment.grades_published? || @submission.grade_posting_in_progress || assignment.permits_moderation?(user)
opts.delete(:grade)
end
@submission.update!(opts)
@submission
end
def setup_account_for_turnitin(account)
account.update(turnitin_account_id: "test_account",
turnitin_shared_secret: "skeret",
settings: account.settings.merge(enable_turnitin: true))
end
context "generated observer alerts" do
before :once do
course_with_teacher
@threshold = observer_alert_threshold_model(alert_type: "assignment_grade_high", threshold: "80", course: @course)
@assignment = assignment_model(context: @course, points_possible: 10)
end
it "doesn't create an alert if the observer has been deleted" do
# This sets up the environment for an error we've seen in the wild
observer_user_ids = @course.observer_enrollments.pluck(:user_id).uniq
User.where(id: observer_user_ids).destroy_all
expect do
@assignment.grade_student(@threshold.student, score: 10, grader: @teacher)
end.not_to change {
ObserverAlert.where(context: @assignment, alert_type: :assignment_grade_high).count
}
end
it "logs if it can't create an observer alert" do
allow(ObserverAlert).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new)
submission_id = @assignment.submissions.find_by(user: @threshold.student).id
expect(Rails.logger).to receive(:error)
.with("Couldn't create ObserverAlert for submission #{submission_id} observer #{@threshold.observer_id}")
@assignment.grade_student(@threshold.student, score: 10, grader: @teacher)
end
end
describe "#grade_posting_in_progress" do
subject { submission.grade_posting_in_progress }
it { is_expected.to be_nil }
it "reports its value" do
submission.grade_posting_in_progress = true
expect(submission.grade_posting_in_progress).to be true
end
end
describe "#grade_posting_in_progress=" do
it "can set a value" do
expect { submission.grade_posting_in_progress = true }.to change {
submission.grade_posting_in_progress
}.from(nil).to(true)
end
end
describe "sticker validations" do
it "allows a nil sticker" do
submission = @assignment.submissions.first
submission.sticker = nil
expect(submission).to be_valid
end
it "does not allow a sticker that is not in the approved list" do
submission = @assignment.submissions.first
submission.sticker = "my_custom_sticker"
expect(submission).not_to be_valid
end
it "allows a sticker that is in the approved list" do
submission = @assignment.submissions.first
submission.sticker = "basketball"
expect(submission).to be_valid
end
end
describe "extra_attempts validations" do
it { is_expected.to validate_numericality_of(:extra_attempts).is_greater_than_or_equal_to(0).allow_nil }
describe "#extra_attempts_can_only_be_set_on_online_uploads" do
it "does not allowe extra_attempts to be set for non online upload submission types" do
submission = @assignment.submissions.first
%w[online_upload online_url online_text_entry].each do |submission_type|
submission.assignment.submission_types = submission_type
submission.assignment.save!
submission.extra_attempts = 10
expect(submission).to be_valid
end
%w[discussion_entry online_quiz].each do |submission_type|
submission.assignment.submission_types = submission_type
submission.assignment.save!
submission.extra_attempts = 10
expect(submission).to_not be_valid
end
end
end
end
describe "#ensure_attempts_are_in_range" do
let(:submission) { @assignment.submissions.first }
context "the assignment is of a type that is restricted by attempts" do
before do
@assignment.allowed_attempts = 10
@assignment.submission_types = "online_upload"
@assignment.save!
end
context "attempts_left <= 0" do
before do
submission.attempt = 10
submission.save!
end
context "the submitted_at changed" do
it "is invalid" do
submission.submitted_at = Time.zone.now
expect(submission).to_not be_valid
end
end
context "the submitted_at did not change" do
it "is valid" do
expect(submission).to be_valid
end
end
end
end
context "the assignment is of a type that is not restricted by attempts" do
before do
@assignment.allowed_attempts = 10
@assignment.submission_types = "online_discussion"
@assignment.save!
submission.attempt = 10
submission.save!
end
it "is valid" do
expect(submission).to be_valid
end
end
end
describe "#attempts_left" do
let(:submission) { @assignment.submissions.first }
context "allowed_attempts is set to a number > 0 on the assignment" do
before do
@assignment.allowed_attempts = 10
@assignment.submission_types = "online_upload"
@assignment.save!
end
context "the submission has extra_attempts set to a value > 0" do
it "returns assignment.allowed_attempts + submission.extra_attempts - submission.attempt" do
submission.extra_attempts = 12
submission.attempt = 6
submission.save!
expect(submission.attempts_left).to eq(10 + 12 - 6)
end
it "correctly recalculates when allowed_attempts and extra_attempts change" do
submission.extra_attempts = 12
submission.attempt = 22
submission.save!
expect(submission.attempts_left).to eq(0)
@assignment.allowed_attempts = 11
@assignment.save!
expect(submission.attempts_left).to eq(1)
submission.extra_attempts = 13
submission.save!
expect(submission.attempts_left).to eq(2)
end
it "will never return negative values" do
submission.attempt = 1000
submission.save!
expect(submission.attempts_left).to eq(0)
end
end
context "the submission has extra_attempts set to nil" do
it "returns allowed_attempts from the assignment" do
submission.extra_attempts = nil
submission.attempt = 6
submission.save!
expect(submission.attempts_left).to eq(10 - 6)
end
end
end
context "allowed_attempts is set to nil or -1 on the assignment" do
it "returns nil" do
@assignment.allowed_attempts = nil
@assignment.save!
expect(submission.attempts_left).to be_nil
@assignment.allowed_attempts = -1
@assignment.save!
expect(submission.attempts_left).to be_nil
end
end
end
describe "#attempt" do
it "is nil when homework has not been submitted" do
submission = Submission.find_by(user: @student)
expect(submission.attempt).to be_nil
end
it "is 1 when homework is submitted" do
submission = @assignment.submit_homework(
@student,
submission_type: "online_text_entry",
body: "body"
)
expect(submission.attempt).to eq 1
end
it "is incremented when homework is resubmitted" do
submission = @assignment.submit_homework(
@student,
submission_type: "online_text_entry",
body: "body",
submitted_at: 1.hour.ago
)
# Due to unit tests being ran in a transaction and not actually committed
# to the database, we can't call submit_homework multiple times. We are
# instead just updating the submitted_at time, which triggers the before_save
# callback.
submission.update!(submitted_at: 2.hours.ago)
submission.update!(submitted_at: 1.hour.ago)
expect(submission.attempt).to eq 3
end
end
describe "sticker removal" do
before(:once) do
@submission = Submission.find_by(user: @student)
end
it "removes the sticker when a new attempt is submitted" do
@submission.update!(sticker: "basketball")
@assignment.submit_homework(@student, submission_type: "online_text_entry", body: "foo")
expect(@submission.reload.sticker).to be_nil
end
it "does not remove the sticker when the submission is updated but there's not a new attempt" do
@submission.update!(sticker: "basketball")
@assignment.grade_student(@student, score: 5, grader: @teacher)
expect(@submission.reload.sticker).to eq "basketball"
end
it "preserves previously awarded stickers in submission history" do
submission = @assignment.submit_homework(@student, submission_type: "online_text_entry", body: "foo")
submission.update!(sticker: "basketball")
Timecop.freeze(10.minutes.from_now) do
submission = @assignment.submit_homework(@student, submission_type: "online_text_entry", body: "bar")
submission.update!(sticker: "paintbrush")
end
sticker = submission.submission_history.find { |sub| sub.attempt == 1 }.sticker
expect(sticker).to eq "basketball"
end
end
describe "#submission_drafts" do
before(:once) do
@submission = Submission.find_by(user: @student)
end
it "is empty by default" do
expect(@submission.submission_drafts).to eq []
end
describe "with drafts for multiple attempts" do
before(:once) do
@submission = @assignment.submit_homework(@student, submission_type: "online_text_entry", body: "foo")
@draft1 = SubmissionDraft.new(submission: @submission, submission_attempt: 0)
@draft2 = SubmissionDraft.new(submission: @submission, submission_attempt: 1)
@submission.submission_drafts << @draft1
@submission.submission_drafts << @draft2
end
it "can have drafts for different submission attempts" do
expect(@submission.submission_drafts.sort).to eq [@draft1, @draft2]
end
it "deletes all drafts for all submission attempts when homework is submitted" do
@assignment.submit_homework(@student, submission_type: "online_text_entry", body: "foo")
@submission.reload
expect(@submission.submission_drafts).to eq []
expect(SubmissionDraft.count).to be 0
end
end
describe "with attachments" do
before(:once) do
@attachment1 = attachment_model
@attachment2 = attachment_model
@submission_draft = SubmissionDraft.create!(
submission: @submission,
submission_attempt: 0
)
@submission_draft.attachments = [@attachment1, @attachment2]
end
it "can access the attachments" do
expect(@submission.submission_drafts.first.attachments.sort).to eq [@attachment1, @attachment2]
end
it "will cascade deletes to SubmissionDraftAttachments when homework is submitted" do
@assignment.submit_homework(@student, submission_type: "online_text_entry", body: "foo")
@submission.reload
expect(@submission.submission_drafts).to eq []
expect(SubmissionDraft.count).to be 0
expect(SubmissionDraftAttachment.count).to be 0
end
end
end
describe "#hide_grade_from_student?" do
subject(:submission) { assignment.submissions.find_by!(user: student) }
let(:course) { @course }
let(:assignment) { @assignment }
let(:teacher) { @teacher }
let(:student) { @student }
before do
course.enroll_student(student)
course.enroll_teacher(teacher)
end
it { is_expected.not_to be_hide_grade_from_student }
context "when assignment posts manually" do
before { assignment.ensure_post_policy(post_manually: true) }
it { is_expected.to be_hide_grade_from_student }
it { is_expected.not_to be_hide_grade_from_student(for_plagiarism: true) }
context "when a submission is posted" do
before { submission.update!(posted_at: Time.zone.now) }
it { is_expected.not_to be_hide_grade_from_student }
end
end
context "when assignment posts automatically" do
before { assignment.ensure_post_policy(post_manually: false) }
it { is_expected.not_to be_hide_grade_from_student }
context "when a submission is posted" do
before { submission.update!(posted_at: Time.zone.now) }
it { is_expected.not_to be_hide_grade_from_student }
end
context "when a submission is graded but not posted" do
before do
assignment.grade_student(student, score: 5, grader: teacher)
assignment.hide_submissions
end
it { is_expected.to be_hide_grade_from_student }
end
context "when homework has been submitted, but the submission is not graded or posted" do
before do
assignment.update!(submission_types: "online_text_entry")
assignment.submit_homework(student, submission_type: "online_text_entry", body: "hi")
end
it { is_expected.not_to be_hide_grade_from_student }
end
context "when a student re-submits to a previously graded and subsequently hidden submission" do
before do
assignment.update!(submission_types: "online_text_entry")
assignment.submit_homework(student, submission_type: "online_text_entry", body: "hi")
assignment.grade_student(student, score: 0, grader: teacher)
assignment.hide_submissions
assignment.submit_homework(student, submission_type: "online_text_entry", body: "I will never give up")
end
it { is_expected.to be_hide_grade_from_student }
end
end
end
describe "posting and unposting" do
subject(:submission) { @assignment.submissions.first }
describe "#posted?" do
it { is_expected.not_to be_posted }
it "returns true if the submission's posted_at date is not nil" do
submission.update!(posted_at: Time.zone.now)
expect(submission).to be_posted
end
end
describe "#handle_posted_at_changed" do
describe "when an studen that is also admin posts an submission" do
it "unmutes the assignment if all submissions are now posted" do
admin = account_admin_user(account: @account, name: "default admin")
@course.enroll_student(admin)
assignment = @course.assignments.create!(
title: "some assignment",
workflow_state: "published"
)
submission_model(user: admin, assignment:, body: "first student submission text")
expect { assignment.reload }.not_to raise_error
end
end
context "when posting an individual submission" do
context "when post policies are enabled" do
it "unmutes the assignment if all submissions are now posted" do
submission.update!(posted_at: Time.zone.now)
expect(@assignment.reload).not_to be_muted
end
it "does not unmute the assignment if some submissions remain unposted" do
@course.enroll_student(User.create!, enrollment_state: "active")
submission.update!(posted_at: Time.zone.now)
expect(@assignment.reload).to be_muted
end
end
end
context "when unposting an individual submission" do
before { submission.update!(posted_at: 1.day.ago) }
context "when post policies are enabled" do
it "mutes an unmuted assignment when a submission is hidden" do
@assignment.post_submissions
submission.update!(posted_at: nil)
expect(@assignment.reload).to be_muted
end
end
end
end
end
context "caching" do
specs_require_cache(:redis_cache_store)
def check_cache_clear
key = @student.cache_key(:submissions)
yield
expect(@student.cache_key(:submissions)).to_not eq key
end
it "clears key when submission is deleted" do
check_cache_clear do
sub = @student.submissions.first
@student.enrollments.first.destroy
expect(sub.reload).to be_deleted
end
end
it "clears key when a submission comment is made" do
check_cache_clear do
@student.submissions.first.add_comment(author: @teacher, comment: "some comment")
end
end
it "clears key when assignment is unmuted" do
@assignment.mute!
check_cache_clear do
@assignment.unmute!
end
end
end
describe "postable scope" do
specs_require_sharding
it "works cross-shard" do
@shard1.activate do
expect(@assignment.submissions.postable.to_sql).to_not include(@shard1.name)
end
end
end
describe "root account ID" do
let_once(:root_account) { Account.create! }
let_once(:subaccount) { Account.create!(root_account:) }
let_once(:course) { Course.create!(account: subaccount) }
let_once(:student) { course.enroll_student(User.create!, workflow_state: "active").user }
it "is set to the root account ID of the owning course" do
assignment = course.assignments.create!
expect(assignment.submission_for_student(student).root_account_id).to eq root_account.id
end
end
describe "redo request" do
subject(:submission) { @assignment.submissions.new user: User.create, workflow_state: "submitted", redo_request: true, attempt: 1 }
it "redo request is reset on an updated submission" do
submission.update!(attempt: 2)
expect(submission.redo_request).to be false
end
end
describe "word_count" do
it "returns the word count" do
submission.update(body: "test submission")
expect(submission.word_count).to eq 2
end
it "returns the word count if body is split up by <br> tags" do
submission.update(body: "test<br>submission")
expect(submission.word_count).to eq 2
end
it "returns nil if there is no body" do
expect(submission.body).to be_nil
expect(submission.word_count).to be_nil
end
it "returns nil if it's a quiz submission" do
submission.update(submission_type: "online_quiz", body: "test submission")
expect(submission.submission_type).to eq "online_quiz"
expect(submission.body).not_to be_nil
expect(submission.word_count).to be_nil
end
it "returns 0 if the body is empty" do
submission.update(body: "")
expect(submission.word_count).to eq 0
end
it "ignores HTML tags" do
submission.update(body: "<span>test <div></div>submission</span> <p></p>")
expect(submission.word_count).to eq 2
submission.instance_variable_set :@word_count, nil
submission.update(body: '<p>This is my submission, which has&nbsp;<strong>some bold&nbsp;<em>italic text</em> in</strong> it.</p>
<p>A couple paragraphs, and maybe super<sup>script</sup>.&nbsp;</p>')
expect(submission.word_count).to eq 18
end
it "sums word counts of attachments if there are any" do
student_in_course(active_all: true)
submission_text = "Text based submission with some words"
attachment1 = attachment_model(uploaded_data: stub_file_data("submission.txt", submission_text, "text/plain"), context: @student)
attachment2 = attachment_model(uploaded_data: stub_file_data("submission.txt", submission_text, "text/plain"), context: @student)
sub = @assignment.submit_homework(@student, attachments: [attachment1, attachment2])
run_jobs
expect(sub.word_count).to eq 12
end
end
context "Assignment Cache" do
specs_require_cache(:redis_cache_store)
describe "creating a new submission" do
subject(:submission) { @assignment.submissions.new user: User.create, workflow_state: "submitted" }
it "invalidates submited count cache if submitted" do
Rails.cache.write(["submitted_count", @assignment].cache_key, "test")
expect(Rails.cache.exist?(["submitted_count", @assignment].cache_key)).to be(true)
subject.run_callbacks :create
expect(Rails.cache.exist?(["submitted_count", @assignment].cache_key)).to be(false)
end
it "does not invalidate submitted count cache if unsubmtted" do
Rails.cache.write(["submitted_count", @assignment].cache_key, "test")
expect(Rails.cache.exist?(["submitted_count", @assignment].cache_key)).to be(true)
subject.workflow_state = "unsubmitted"
subject.run_callbacks :create
expect(Rails.cache.exist?(["submitted_count", @assignment].cache_key)).to be(true)
end
it "invalidates graded count cache if graded" do
Rails.cache.write(["graded_count", @assignment].cache_key, "test")
expect(Rails.cache.exist?(["graded_count", @assignment].cache_key)).to be(true)
subject.score = 10
subject.workflow_state = "graded"
subject.run_callbacks :create
expect(Rails.cache.exist?(["graded_count", @assignment].cache_key)).to be(false)
end
it "does not invalidate graded count cache if unsubmtted" do
Rails.cache.write(["graded_count", @assignment].cache_key, "test")
expect(Rails.cache.exist?(["graded_count", @assignment].cache_key)).to be(true)
subject.run_callbacks :create
expect(Rails.cache.exist?(["graded_count", @assignment].cache_key)).to be(true)
end
end
describe "updating a submission" do
subject(:submission) { @assignment.submissions.first }
it "invalidates submited count cache if submitted" do
Rails.cache.write(["submitted_count", @assignment].cache_key, "test")
expect(Rails.cache.exist?(["submitted_count", @assignment].cache_key)).to be(true)
subject.workflow_state = "submitted"
subject.run_callbacks :update
expect(Rails.cache.exist?(["submitted_count", @assignment].cache_key)).to be(false)
end
it "does not invalidate submitted count cache if unsubmtted" do
Rails.cache.write(["submitted_count", @assignment].cache_key, "test")
expect(Rails.cache.exist?(["submitted_count", @assignment].cache_key)).to be(true)
subject.workflow_state = "unsubmitted"
subject.run_callbacks :update
expect(Rails.cache.exist?(["submitted_count", @assignment].cache_key)).to be(true)
end
it "invalidates graded count cache if graded" do
Rails.cache.write(["graded_count", @assignment].cache_key, "test")
expect(Rails.cache.exist?(["graded_count", @assignment].cache_key)).to be(true)
subject.score = 10
subject.workflow_state = "graded"
subject.run_callbacks :update
expect(Rails.cache.exist?(["graded_count", @assignment].cache_key)).to be(false)
end
it "does not invalidate graded count cache if unsubmtted" do
Rails.cache.write(["graded_count", @assignment].cache_key, "test")
expect(Rails.cache.exist?(["graded_count", @assignment].cache_key)).to be(true)
subject.workflow_state = "submitted"
subject.run_callbacks :create
expect(Rails.cache.exist?(["graded_count", @assignment].cache_key)).to be(true)
end
end
end
describe "#observer?" do
before do
@student = user_factory
course_with_observer(
course: @course,
associated_user_id: @student.id,
active_all: true,
active_cc: true
)
@submission = @assignment.submission_for_student(@student)
end
it "is true for observer" do
expect(@submission.observer?(@observer)).to be true
end
it "is false for student" do
expect(@submission.observer?(@student)).to be false
end
it "is false for teacher" do
expect(@submission.observer?(@teacher)).to be false
end
it "is false for others" do
expect(@submission.observer?(user_factory)).to be false
end
end
describe "#peer_reviewer?" do
before do
student_in_course(active_all: true)
@peer_reviewer = user_factory
@course.enroll_student(@peer_reviewer).accept!
@assignment = @course.assignments.build(
title: "Peer Reviews",
submission_types: "online_text_entry",
peer_reviews: true
)
@assignment.save!
@submission = @assignment.submission_for_student(@student)
@submission.assessment_requests.create!(
user: @student,
assessor: @peer_reviewer,
assessor_asset: @submission
)
end
it "is true for reviewer" do
expect(@submission.peer_reviewer?(@peer_reviewer)).to be true
end
it "is false for student" do
expect(@submission.peer_reviewer?(@student)).to be false
end
it "is false for teacher" do
expect(@submission.peer_reviewer?(@teacher)).to be false
end
it "is false for others" do
expect(@submission.peer_reviewer?(user_factory)).to be false
end
end
describe "send_timing_data_if_needed" do
it "calls Statsd when a classic quiz is manually graded" do
expect(InstStatsd::Statsd).to receive(:gauge).once.with("submission.manually_graded.grading_time", 600.0, 1.0, tags: { quiz_type: "classic_quiz" })
now = Time.now
Timecop.freeze(now) do
quiz_with_graded_submission([{ question_data: { :name => "question 1", :points_possible => 10, "question_type" => "essay_question" } }])
end
Timecop.freeze(10.minutes.from_now(now)) do
@quiz_submission.set_final_score(7)
@quiz_submission.save!
end
end
it "calls Statsd when a new quiz is manually graded" do
expect(InstStatsd::Statsd).to receive(:gauge).once.with("submission.manually_graded.grading_time", 300.0, 1.0, tags: { quiz_type: "new_quiz" })
now = Time.now
Timecop.freeze(now) do
quiz_with_graded_submission([{ question_data: { :name => "question 1", :points_possible => 10, "question_type" => "essay_question" } }])
end
allow(@quiz_submission.submission).to receive_messages(submission_type: "basic_lti_launch", url: "https://quiz-lti-iad-prod.instructure.com/lti/launch")
Timecop.freeze(5.minutes.from_now(now)) do
@quiz_submission.set_final_score(7)
@quiz_submission.save!
end
end
it "does not call Statsd when a quiz is automatically graded" do
expect(InstStatsd::Statsd).not_to receive(:gauge)
quiz_with_graded_submission([{ question_data: { :name => "question 1", :points_possible => 10, "question_type" => "multiple_choice_question" } }])
end
it "does not call Statsd when a submission is updated" do
expect(InstStatsd::Statsd).not_to receive(:gauge)
now = Time.now
Timecop.freeze(now) do
quiz_with_graded_submission([{ question_data: { :name => "question 1", :points_possible => 10, "question_type" => "essay_question" } }])
end
Timecop.freeze(10.minutes.from_now(now)) do
submission = @quiz.submissions.first
submission.excused = false
submission.save!
end
end
it "does not call Statsd when the time between submission and grading is less than 30 seconds" do
expect(InstStatsd::Statsd).not_to receive(:gauge)
now = Time.now
Timecop.freeze(now) do
quiz_with_graded_submission([{ question_data: { :name => "question 1", :points_possible => 10, "question_type" => "essay_question" } }])
end
Timecop.freeze(29.seconds.from_now(now)) do
@quiz_submission.set_final_score(7)
@quiz_submission.save!
end
end
end
describe "checkpoint submissions" do
before(:once) do
course = course_model
student = student_in_course(course:, active_all: true).user
course.root_account.enable_feature!(:discussion_checkpoints)
topic = DiscussionTopic.create_graded_topic!(course:, title: "graded topic")
topic.create_checkpoints(reply_to_topic_points: 3, reply_to_entry_points: 7)
@checkpoint_submission = topic.reply_to_topic_checkpoint.submissions.find_by(user: student)
@parent_submission = topic.assignment.submissions.find_by(user: student)
end
it "updates the parent submission when tracked attrs change on a checkpoint submission" do
expect { @checkpoint_submission.update!(score: 3) }.to change { @parent_submission.reload.score }.from(nil).to(3)
end
it "does not update the parent submission when attrs that changed are not tracked" do
expect { @checkpoint_submission.update!(lti_user_id: "some-id") }.not_to change { @parent_submission.reload.updated_at }
end
it "does not update the parent submission when the checkpoints flag is disabled" do
@checkpoint_submission.root_account.disable_feature!(:discussion_checkpoints)
expect { @checkpoint_submission.update!(score: 3) }.not_to change { @parent_submission.reload.score }
end
end
end