canvas-lms/spec/models/discussion_topic_spec.rb

2057 lines
83 KiB
Ruby

#
# Copyright (C) 2011 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
describe DiscussionTopic do
before :once do
course_with_teacher(:active_all => true)
student_in_course(:active_all => true)
end
describe "default values for boolean attributes" do
before(:once) do
@topic = @course.discussion_topics.create!
end
let(:values) do
DiscussionTopic.where(id: @topic).pluck(
:could_be_locked,
:podcast_enabled,
:podcast_has_student_posts,
:require_initial_post,
:pinned,
:locked,
:allow_rating,
:only_graders_can_rate,
:sort_by_rating
).first
end
it "saves boolean attributes as false if they are set to nil" do
@topic.update!(
could_be_locked: nil,
podcast_enabled: nil,
podcast_has_student_posts: nil,
require_initial_post: nil,
pinned: nil,
locked: nil,
allow_rating: nil,
only_graders_can_rate: nil,
sort_by_rating: nil
)
expect(values).to eq([false] * values.length)
end
it "saves boolean attributes as false if they are set to false" do
@topic.update!(
could_be_locked: false,
podcast_enabled: false,
podcast_has_student_posts: false,
require_initial_post: false,
pinned: false,
locked: false,
allow_rating: false,
only_graders_can_rate: false,
sort_by_rating: false
)
expect(values).to eq([false] * values.length)
end
it "saves boolean attributes as true if they are set to true" do
@topic.update!(
could_be_locked: true,
podcast_enabled: true,
podcast_has_student_posts: true,
require_initial_post: true,
pinned: true,
locked: true,
allow_rating: true,
only_graders_can_rate: true,
sort_by_rating: true
)
expect(values).to eq([true] * values.length)
end
end
it "should santize message" do
@course.discussion_topics.create!(:message => "<a href='#' onclick='alert(12);'>only this should stay</a>")
expect(@course.discussion_topics.first.message).to eql("<a href=\"#\">only this should stay</a>")
end
it "should default to side_comment type" do
d = DiscussionTopic.new
expect(d.discussion_type).to eq 'side_comment'
d.threaded = '1'
expect(d.discussion_type).to eq 'threaded'
d.threaded = ''
expect(d.discussion_type).to eq 'side_comment'
end
it "should require a valid discussion_type" do
@topic = @course.discussion_topics.build(:message => 'test', :discussion_type => "gesundheit")
expect(@topic.save).to eq false
expect(@topic.errors.detect { |e| e.first.to_s == 'discussion_type' }).to be_present
end
it "should update the assignment it is associated with" do
a = @course.assignments.create!(:title => "some assignment", :points_possible => 5)
expect(a.points_possible).to eql(5.0)
expect(a.submission_types).not_to eql("online_quiz")
t = @course.discussion_topics.build(:assignment => a, :title => "some topic", :message => "a little bit of content")
t.save
expect(t.assignment_id).to eql(a.id)
expect(t.assignment).to eql(a)
a.reload
expect(a.discussion_topic).to eql(t)
expect(a.submission_types).to eql("discussion_topic")
end
it "should delete the assignment if the topic is no longer graded" do
a = @course.assignments.create!(:title => "some assignment", :points_possible => 5)
expect(a.points_possible).to eql(5.0)
expect(a.submission_types).not_to eql("online_quiz")
t = @course.discussion_topics.build(:assignment => a, :title => "some topic", :message => "a little bit of content")
t.save
expect(t.assignment_id).to eql(a.id)
expect(t.assignment).to eql(a)
a.reload
expect(a.discussion_topic).to eql(t)
t.assignment = nil
t.save
t.reload
expect(t.assignment_id).to eql(nil)
expect(t.assignment).to eql(nil)
a.reload
expect(a).to be_deleted
end
context "permissions" do
before :each do
@teacher1 = @teacher
@teacher2 = user_factory
teacher_in_course(:course => @course, :user => @teacher2, :active_all => true)
@topic = @course.discussion_topics.create!(:user => @teacher1)
@topic.unpublish!
@entry = @topic.discussion_entries.create!(:user => @teacher1)
@entry.discussion_topic = @topic
@relevant_permissions = [:read, :reply, :update, :delete]
end
it "should not grant moderate permissions without read permissions" do
@course.account.role_overrides.create!(:role => teacher_role, :permission => 'read_forum', :enabled => false)
expect((@topic.check_policy(@teacher2) & @relevant_permissions)).to be_empty
end
it "should grant permissions if it not locked" do
@topic.publish!
expect((@topic.check_policy(@teacher1) & @relevant_permissions).map(&:to_s).sort).to eq ['read', 'reply', 'update', 'delete'].sort
expect((@topic.check_policy(@teacher2) & @relevant_permissions).map(&:to_s).sort).to eq ['read', 'reply', 'update', 'delete'].sort
expect((@topic.check_policy(@student) & @relevant_permissions).map(&:to_s).sort).to eq ['read', 'reply'].sort
expect((@entry.check_policy(@teacher1) & @relevant_permissions).map(&:to_s).sort).to eq ['read', 'reply', 'update', 'delete'].sort
expect((@entry.check_policy(@teacher2) & @relevant_permissions).map(&:to_s).sort).to eq ['read', 'reply', 'update', 'delete'].sort
expect((@entry.check_policy(@student) & @relevant_permissions).map(&:to_s).sort).to eq ['read', 'reply'].sort
end
it "should not grant reply permissions to students if it is locked" do
@topic.publish!
@topic.lock!
expect((@topic.check_policy(@teacher1) & @relevant_permissions).map(&:to_s).sort).to eq ['read', 'reply', 'update', 'delete'].sort
expect((@topic.check_policy(@teacher2) & @relevant_permissions).map(&:to_s).sort).to eq ['read', 'reply', 'update', 'delete'].sort
expect((@topic.check_policy(@student) & @relevant_permissions).map(&:to_s)).to eq ['read']
expect((@entry.check_policy(@teacher1) & @relevant_permissions).map(&:to_s).sort).to eq ['read', 'reply', 'update', 'delete'].sort
expect((@entry.check_policy(@teacher2) & @relevant_permissions).map(&:to_s).sort).to eq ['read', 'reply', 'update', 'delete'].sort
expect((@entry.check_policy(@student) & @relevant_permissions).map(&:to_s)).to eq ['read']
end
it "should not grant any permissions to students if it is unpublished" do
expect((@topic.check_policy(@teacher1) & @relevant_permissions).map(&:to_s).sort).to eq ['read', 'reply', 'update', 'delete'].sort
expect((@topic.check_policy(@teacher2) & @relevant_permissions).map(&:to_s).sort).to eq ['read', 'reply', 'update', 'delete'].sort
expect((@topic.check_policy(@student) & @relevant_permissions).map(&:to_s).sort).to eq []
expect((@entry.check_policy(@teacher1) & @relevant_permissions).map(&:to_s).sort).to eq ['read', 'reply', 'update', 'delete'].sort
expect((@entry.check_policy(@teacher2) & @relevant_permissions).map(&:to_s).sort).to eq ['read', 'reply', 'update', 'delete'].sort
expect((@entry.check_policy(@student) & @relevant_permissions).map(&:to_s).sort).to eq []
end
end
describe "visibility" do
before(:once) do
#student_in_course(:active_all => 1)
@topic = @course.discussion_topics.create!(:user => @teacher)
end
it "should be visible to author when unpublished" do
@topic.unpublish!
expect(@topic.visible_for?(@teacher)).to be_truthy
end
it "should be visible when published even when for delayed posting" do
@topic.delayed_post_at = 5.days.from_now
@topic.workflow_state = 'post_delayed'
@topic.save!
expect(@topic.visible_for?(@student)).to be_truthy
end
it "should not be visible when unpublished even when it is active" do
@topic.unpublish!
expect(@topic.visible_for?(@student)).to be_falsey
end
it "should be visible to students when topic is not locked" do
expect(@topic.visible_for?(@student)).to be_truthy
end
it "should be visible to students when topic delayed_post_at is in the future" do
@topic.delayed_post_at = 5.days.from_now
@topic.save!
expect(@topic.visible_for?(@student)).to be_truthy
end
it "should be visible to students when topic is for delayed posting" do
@topic.workflow_state = 'post_delayed'
@topic.save!
expect(@topic.visible_for?(@student)).to be_truthy
end
it "should be visible to students when topic delayed_post_at is in the past" do
@topic.delayed_post_at = 5.days.ago
@topic.save!
expect(@topic.visible_for?(@student)).to be_truthy
end
it "should be visible to students when topic delayed_post_at is nil" do
@topic.delayed_post_at = nil
@topic.save!
expect(@topic.visible_for?(@student)).to be_truthy
end
it "should not be visible to unauthenticated users in a public course" do
@course.update_attribute(:is_public, true)
expect(@topic.visible_for?(nil)).to be_falsey
end
it "should be visible when no delayed_post but assignment unlock date in future" do
@topic.delayed_post_at = nil
group_category = @course.group_categories.create(:name => "category")
@topic.group_category = group_category
@topic.assignment = @course.assignments.build(:submission_types => 'discussion_topic',
:title => @topic.title,
:unlock_at => 10.days.from_now,
:lock_at => 30.days.from_now)
@topic.assignment.infer_times
@topic.assignment.saved_by = :discussion_topic
@topic.save
expect(@topic.visible_for?(@student)).to be_truthy
end
it "should be visible to all teachers in the course" do
@topic.update_attribute(:delayed_post_at, Time.now + 1.day)
new_teacher = user_factory
@course.enroll_teacher(new_teacher).accept!
expect(@topic.visible_for?(new_teacher)).to be_truthy
end
it "unpublished topics should not be visible to custom account admins by default" do
@topic.unpublish
account = @course.root_account
nobody_role = custom_account_role('NobodyAdmin', account: account)
admin = account_admin_user(account: account, role: nobody_role, active_user: true)
expect(@topic.visible_for?(admin)).to be_falsey
end
it "unpublished topics should be visible to account admins with :read_course_content permission" do
@topic.unpublish
account = @course.root_account
nobody_role = custom_account_role('NobodyAdmin', account: account)
account_with_role_changes(account: account, role: nobody_role, role_changes: { read_course_content: true, read_forum: true })
admin = account_admin_user(account: account, role: nobody_role, active_user: true)
expect(@topic.visible_for?(admin)).to be_truthy
end
context "participants with teachers and tas" do
before(:once) do
group_course = course_factory(active_course: true)
@group_student, @group_ta, @group_teacher = create_users(3, return_type: :record)
@not_group_student, @group_designer = create_users(2, return_type: :record)
group_course.enroll_teacher(@group_teacher).accept!
group_course.enroll_ta(@group_ta).accept!
group_course.enroll_designer(@group_designer).accept!
group_category = group_course.group_categories.create(:name => "new cat")
group = group_course.groups.create(:name => "group", :group_category => group_category)
group.add_user(@group_student)
@announcement = group.announcements.build(:title => "group topic", :message => "group message")
@announcement.save!
end
it "should be visible to instructors and tas" do
[@group_student, @group_ta, @group_teacher].each do |user|
expect(@announcement.active_participants_include_tas_and_teachers.include?(user)).to be_truthy
end
end
it "should not include people out of the group or non-instructors" do
[@not_group_student, @group_designer].each do |user|
expect(@announcement.active_participants_include_tas_and_teachers.include?(user)).to be_falsey
end
end
end
context "differentiated assignements" do
before do
@course = course_factory(active_course: true)
discussion_topic_model(:user => @teacher, :context => @course)
@course.enroll_teacher(@teacher).accept!
@course_section = @course.course_sections.create
@student1, @student2, @student3 = create_users(3, return_type: :record)
@assignment = @course.assignments.create!(:title => "some discussion assignment", only_visible_to_overrides: true)
@assignment.submission_types = 'discussion_topic'
@assignment.save!
@topic.assignment_id = @assignment.id
@topic.save!
@course.enroll_student(@student2, :enrollment_state => 'active')
@section = @course.course_sections.create!(name: "test section")
student_in_section(@section, user: @student1)
create_section_override_for_assignment(@assignment, {course_section: @section})
@course.reload
end
it "should be visible to a student with an override" do
expect(@topic.visible_for?(@student1)).to be_truthy
end
it "should not be visible to a student without an override" do
expect(@topic.visible_for?(@student2)).to be_falsey
end
it "should be visible to a teacher" do
expect(@topic.visible_for?(@teacher)).to be_truthy
end
it "should not grant reply permissions to a student without an override" do
expect(@topic.check_policy(@student1)).to include :reply
expect(@topic.check_policy(@student2)).not_to include :reply
end
context "active_participants_with_visibility" do
it "should filter participants by visibility" do
[@student1, @teacher].each do |user|
expect(@topic.active_participants_with_visibility.include?(user)).to be_truthy
end
expect(@topic.active_participants_with_visibility.include?(@student2)).to be_falsey
end
it "should work when ungraded and context is a course" do
group_category = @course.group_categories.create(:name => "new cat")
@topic = @course.discussion_topics.create(:title => "group topic")
@topic.save!
expect(@topic.context).to eq(@course)
expect(@topic.active_participants_with_visibility.include?(@student1)).to be_truthy
expect(@topic.active_participants_with_visibility.include?(@student2)).to be_truthy
end
it "should work when ungraded and context is a group" do
group_category = @course.group_categories.create(:name => "new cat")
@group = @course.groups.create(:name => "group", :group_category => group_category)
@group.add_user(@student1)
@topic = @group.discussion_topics.create(:title => "group topic")
@topic.save!
expect(@topic.context).to eq(@group)
expect(@topic.active_participants_with_visibility.include?(@student1)).to be_truthy
expect(@topic.active_participants_with_visibility.include?(@student2)).to be_falsey
end
it "should not grant reply permissions to group if course is concluded" do
@relevant_permissions = [:read, :reply, :update, :delete, :read_replies]
group_category = @course.group_categories.create(:name => "new cat")
@group = @course.groups.create(:name => "group", :group_category => group_category)
@group.add_user(@student1)
@course.complete!
@topic = @group.discussion_topics.create(:title => "group topic")
@topic.save!
expect(@topic.context).to eq(@group)
expect((@topic.check_policy(@student1) & @relevant_permissions).sort).to eq [:read, :read_replies].sort
end
it "should not grant reply permissions to group if course is soft-concluded" do
@relevant_permissions = [:read, :reply, :update, :delete, :read_replies]
group_category = @course.group_categories.create(:name => "new cat")
@group = @course.groups.create(:name => "group", :group_category => group_category)
@group.add_user(@student1)
@course.update_attributes(:start_at => 2.days.ago, :conclude_at => 1.day.ago, :restrict_enrollments_to_course_dates => true)
@topic = @group.discussion_topics.create(:title => "group topic")
@topic.save!
expect(@topic.context).to eq(@group)
expect((@topic.check_policy(@student1) & @relevant_permissions).sort).to eq [:read, :read_replies].sort
end
it "should grant reply permissions to group members if course is concluded but their section isn't" do
@relevant_permissions = [:read, :reply, :update, :delete, :read_replies]
group_category = @course.group_categories.create(:name => "new cat")
@group = @course.groups.create(:name => "group", :group_category => group_category)
@group.add_user(@student1)
@course.update_attributes(:start_at => 2.days.ago, :conclude_at => 1.day.ago, :restrict_enrollments_to_course_dates => true)
@section.update_attributes(:start_at => 2.days.ago, :end_at => 2.days.from_now,
:restrict_enrollments_to_section_dates => true)
@topic = @group.discussion_topics.create(:title => "group topic")
@topic.save!
expect(@topic.context).to eq(@group)
expect((@topic.check_policy(@student1) & @relevant_permissions).sort).to eq [:read, :read_replies, :reply].sort
end
it "should not grant reply permissions to group if group isn't active" do
@relevant_permissions = [:read, :reply, :update, :delete, :read_replies]
group_category = @course.group_categories.create(:name => "new cat")
@group = @course.groups.create(:name => "group", :group_category => group_category)
@group.add_user(@student1)
@topic = @group.discussion_topics.create(:title => "group topic")
@topic.save!
@group.destroy
expect(@topic.reload.context).to eq(@group.reload)
expect((@topic.check_policy(@student1) & @relevant_permissions).sort).to eq [:read, :read_replies].sort
end
it "should grant reply permissions to teachers if course is claimed" do
course = course_factory(active_course: false)
discussion_topic_model(:user => @teacher, :context => course)
course.enroll_teacher(@teacher).accept!
course.enroll_student(@student1)
@relevant_permissions = [:read, :reply, :update, :delete, :read_replies]
group_category = course.group_categories.create(:name => "new cat")
@group = course.groups.create(:name => "group", :group_category => group_category)
@group.add_user(@student1)
@topic = @group.discussion_topics.create(:title => "group topic")
@topic.save!
expect(@topic.context).to eq(@group)
expect((@topic.check_policy(@teacher) & @relevant_permissions).sort).to eq @relevant_permissions.sort
expect((@topic.check_policy(@student1) & @relevant_permissions)).to be_empty
end
it "should work for subtopics for graded assignments" do
group_discussion_assignment
ct = @topic.child_topics.first
ct.context.add_user(@student)
@section = @course.course_sections.create!(name: "test section")
student_in_section(@section, user: @student)
create_section_override_for_assignment(@assignment, {course_section: @section})
@topic = @topic.child_topics.first
@topic.subscribe(@student)
@topic.save!
expect(@topic.context.class).to eq(Group)
expect(@topic.active_participants_with_visibility.include?(@student)).to be_truthy
end
end
end
end
describe "allow_student_discussion_topics setting" do
before(:once) do
@topic = @course.discussion_topics.create!(:user => @teacher)
end
it "should allow students to create topics by default" do
expect(@topic.check_policy(@teacher)).to include :create
expect(@topic.check_policy(@student)).to include :create
expect(@topic.check_policy(@course.student_view_student)).to include :create
end
it "should disallow students from creating topics" do
@course.allow_student_discussion_topics = false
@course.save!
@topic.reload
expect(@topic.check_policy(@teacher)).to include :create
expect(@topic.check_policy(@student)).not_to include :create
expect(@topic.check_policy(@course.student_view_student)).not_to include :create
end
end
context "observers" do
before :once do
course_with_observer(:course => @course, :active_all => true)
end
it "should grant observers read permission by default" do
@relevant_permissions = [:read, :reply, :update, :delete]
@topic = @course.discussion_topics.create!(:user => @teacher)
expect((@topic.check_policy(@observer) & @relevant_permissions).map(&:to_s).sort).to eq ['read'].sort
@entry = @topic.discussion_entries.create!(:user => @teacher)
expect((@entry.check_policy(@observer) & @relevant_permissions).map(&:to_s).sort).to eq ['read'].sort
end
it "should not grant observers read permission when read_forum override is false" do
RoleOverride.create!(:context => @course.account, :permission => 'read_forum',
:role => observer_role, :enabled => false)
@relevant_permissions = [:read, :reply, :update, :delete]
@topic = @course.discussion_topics.create!(:user => @teacher)
expect((@topic.check_policy(@observer) & @relevant_permissions).map(&:to_s)).to be_empty
@entry = @topic.discussion_entries.create!(:user => @teacher)
expect((@entry.check_policy(@observer) & @relevant_permissions).map(&:to_s)).to be_empty
end
end
context "delayed posting" do
before :once do
@student.register
end
def discussion_topic(opts = {})
workflow_state = opts.delete(:workflow_state)
@topic = @course.discussion_topics.build(opts)
@topic.workflow_state = workflow_state if workflow_state
@topic.save!
@topic
end
def delayed_discussion_topic(opts = {})
discussion_topic({:workflow_state => 'post_delayed'}.merge(opts))
end
it "shouldn't send to streams on creation or update if it's delayed" do
topic = @course.discussion_topics.create!(
title: "this should not be delayed",
message: "content here"
)
expect(topic.stream_item).not_to be_nil
topic = delayed_discussion_topic(
title: "this should be delayed",
message: "content here",
delayed_post_at: 1.day.from_now
)
expect(topic.stream_item).to be_nil
topic.message = "content changed!"
topic.save
expect(topic.stream_item).to be_nil
end
it "should send to streams on update from unpublished to active" do
topic = discussion_topic(
title: "this should be delayed",
message: "content here",
workflow_state: "unpublished"
)
expect(topic.workflow_state).to eq 'unpublished'
expect(topic.stream_item).to be_nil
topic.workflow_state = 'active'
topic.save!
expect(topic.stream_item).not_to be_nil
end
it "doesn't rely on broadcast policy when sending to stream" do
topic = discussion_topic(
title: "this should be delayed",
message: "content here",
workflow_state: "unpublished"
)
expect(topic.workflow_state).to eq 'unpublished'
expect(topic.stream_item).to be_nil
topic.workflow_state = 'active'
topic.save_without_broadcasting!
expect(topic.stream_item).not_to be_nil
end
describe "#update_based_on_date" do
it "should be active when delayed_post_at is in the past" do
topic = delayed_discussion_topic(:title => "title",
:message => "content here",
:delayed_post_at => Time.now - 1.day,
:lock_at => nil)
topic.update_based_on_date
expect(topic.workflow_state).to eql 'active'
expect(topic.locked?).to be_falsey
end
it "should be post_delayed when delayed_post_at is in the future" do
topic = delayed_discussion_topic(:title => "title",
:message => "content here",
:delayed_post_at => Time.now + 1.day,
:lock_at => nil)
topic.update_based_on_date
expect(topic.workflow_state).to eql 'post_delayed'
expect(topic.locked?).to be_falsey
end
it "should be locked when lock_at is in the past" do
topic = delayed_discussion_topic(:title => "title",
:message => "content here",
:delayed_post_at => nil,
:lock_at => Time.now - 1.day)
topic.update_based_on_date
expect(topic.locked?).to be_truthy
end
it "should be active when lock_at is in the future" do
topic = delayed_discussion_topic(:title => "title",
:message => "content here",
:delayed_post_at => nil,
:lock_at => Time.now + 1.day)
topic.update_based_on_date
expect(topic.workflow_state).to eql 'active'
expect(topic.locked?).to be_falsey
end
it "should be active when now is between delayed_post_at and lock_at" do
topic = delayed_discussion_topic(:title => "title",
:message => "content here",
:delayed_post_at => Time.now - 1.day,
:lock_at => Time.now + 1.day)
topic.update_based_on_date
expect(topic.workflow_state).to eql 'active'
expect(topic.locked?).to be_falsey
end
it "should be post_delayed when delayed_post_at and lock_at are in the future" do
topic = delayed_discussion_topic(:title => "title",
:message => "content here",
:delayed_post_at => Time.now + 1.day,
:lock_at => Time.now + 3.days)
topic.update_based_on_date
expect(topic.workflow_state).to eql 'post_delayed'
expect(topic.locked?).to be_falsey
end
it "should be locked when delayed_post_at and lock_at are in the past" do
topic = delayed_discussion_topic(:title => "title",
:message => "content here",
:delayed_post_at => Time.now - 3.days,
:lock_at => Time.now - 1.day)
topic.update_based_on_date
expect(topic.workflow_state).to eql 'active'
expect(topic.locked?).to be_truthy
end
it "should not unlock a topic even if the lock date is in the future" do
topic = discussion_topic(:title => "title",
:message => "content here",
:workflow_state => 'locked',
:locked => true,
:delayed_post_at => nil,
:lock_at => Time.now + 1.day)
topic.update_based_on_date
expect(topic.locked?).to be_truthy
end
it "should not mark a topic with post_delayed even if delayed_post_at even is in the future" do
topic = discussion_topic(:title => "title",
:message => "content here",
:workflow_state => 'active',
:delayed_post_at => Time.now + 1.day,
:lock_at => nil)
topic.update_based_on_date
expect(topic.workflow_state).to eql 'active'
expect(topic.locked?).to be_falsey
end
end
end
context "sub-topics" do
it "should default subtopics_refreshed_at on save if a group discussion" do
group_category = @course.group_categories.create(:name => "category")
@group = @course.groups.create(:name => "group", :group_category => group_category)
@topic = @course.discussion_topics.create(:title => "topic")
expect(@topic.subtopics_refreshed_at).to be_nil
@topic.group_category = group_category
@topic.save
expect(@topic.subtopics_refreshed_at).not_to be_nil
end
it "should not allow students to edit sub-topics" do
@first_user = @student
@second_user = user_model
@course.enroll_student(@second_user).accept
@parent_topic = @course.discussion_topics.create!(:title => "parent topic", :message => "msg")
@group = @course.groups.create!(:name => "course group")
@group.add_user(@first_user)
@group.add_user(@second_user)
@group_topic = @group.discussion_topics.create!(:title => "group topic", :message => "ok to be edited", :user => @first_user)
@sub_topic = @group.discussion_topics.build(:title => "sub topic", :message => "not ok to be edited", :user => @first_user)
@sub_topic.root_topic_id = @parent_topic.id
@sub_topic.save!
expect(@group_topic.grants_right?(@second_user, :update)).to eql(false)
expect(@sub_topic.grants_right?(@second_user, :update)).to eql(false)
end
end
context "refresh_subtopics" do
it "should be a no-op unless it has a group_category" do
@topic = @course.discussion_topics.create(:title => "topic")
@topic.refresh_subtopics
expect(@topic.reload.child_topics).to be_empty
@topic.assignment = @course.assignments.build(:submission_types => 'discussion_topic', :title => @topic.title)
@topic.assignment.saved_by = :discussion_topic
@topic.save
@topic.refresh_subtopics
expect(@topic.reload.child_topics).to be_empty
end
it "should refresh when groups are added to a group_category" do
group_category = @course.group_categories.create!(:name => "category")
topic = @course.discussion_topics.build(:title => "topic")
topic.group_category = group_category
topic.save!
group = @course.groups.create!(:name => "group 1", :group_category => group_category)
expect(topic.reload.child_topics.size).to eq 1
expect(group.reload.discussion_topics.size).to eq 1
end
it "should not break when groups have silly long names" do
group_category = @course.group_categories.create!(:name => "category")
topic = @course.discussion_topics.build(:title => "here's a reasonable topic name")
topic.group_category = group_category
topic.save!
group = @course.groups.create!(:name => "a" * 250, :group_category => group_category)
expect(topic.reload.child_topics.size).to eq 1
expect(group.reload.discussion_topics.size).to eq 1
end
it "should delete child topics when group category is removed" do
group_category = @course.group_categories.create!(:name => "category")
group = @course.groups.create!(:name => "group 1", :group_category => group_category)
topic = @course.discussion_topics.build(:title => "topic")
topic.group_category = group_category
topic.save!
expect(topic.reload.child_topics.active.count).to eq 1
expect(group.reload.discussion_topics.active.count).to eq 1
topic.group_category = nil
topic.save!
expect(topic.reload.child_topics.active.count).to eq 0
expect(group.reload.discussion_topics.active.count).to eq 0
end
context "in a group discussion" do
before :once do
group_discussion_assignment
end
it "should create a topic per active group in the category otherwise" do
@topic.refresh_subtopics
subtopics = @topic.reload.child_topics
expect(subtopics).not_to be_nil
expect(subtopics.size).to eq 2
subtopics.each { |t| expect(t.root_topic).to eq @topic }
expect(@group1.reload.discussion_topics).not_to be_empty
expect(@group2.reload.discussion_topics).not_to be_empty
end
it "should copy appropriate attributes from the parent topic to subtopics on updates to the parent" do
@topic.refresh_subtopics
subtopics = @topic.reload.child_topics
subtopics.each do |st|
expect(st.discussion_type).to eq 'side_comment'
expect(st.attachment_id).to be_nil
end
attachment_model(context: @course)
@topic.discussion_type = 'threaded'
@topic.attachment = @attachment
@topic.save!
subtopics = @topic.reload.child_topics
subtopics.each do |st|
expect(st.discussion_type).to eq 'threaded'
expect(st.attachment_id).to eq @attachment.id
end
end
it "should not rename the assignment to match a subtopic" do
original_name = @assignment.title
@assignment.reload
expect(@assignment.title).to eq original_name
end
end
end
context "root_topic?" do
it "should be false if the topic has a root topic" do
# subtopic has the assignment and group_category, but has a root topic
group_category = @course.group_categories.create(:name => "category")
@parent_topic = @course.discussion_topics.create(:title => "parent topic")
@parent_topic.group_category = group_category
@subtopic = @parent_topic.child_topics.build(:title => "subtopic")
@assignment = @course.assignments.build(:submission_types => 'discussion_topic', :title => @subtopic.title)
@assignment.infer_times
@assignment.saved_by = :discussion_topic
@subtopic.assignment = @assignment
@subtopic.group_category = group_category
@subtopic.save
expect(@subtopic).not_to be_root_topic
end
it "should be false unless the topic has an assignment" do
# topic has no root topic, but also has no assignment
@topic = @course.discussion_topics.create(:title => "subtopic")
expect(@topic).not_to be_root_topic
end
it "should be false unless the topic has a group_category" do
# topic has no root topic and has an assignment, but the assignment has no group_category
@topic = @course.discussion_topics.create(:title => "topic")
@assignment = @course.assignments.build(:submission_types => 'discussion_topic', :title => @topic.title)
@assignment.infer_times
@assignment.saved_by = :discussion_topic
@topic.assignment = @assignment
@topic.save
expect(@topic).not_to be_root_topic
end
it "should be true otherwise" do
# topic meets all criteria
group_category = @course.group_categories.create(:name => "category")
@topic = @course.discussion_topics.create(:title => "topic")
@topic.group_category = group_category
@assignment = @course.assignments.build(:submission_types => 'discussion_topic', :title => @topic.title)
@assignment.infer_times
@assignment.saved_by = :discussion_topic
@topic.assignment = @assignment
@topic.save
expect(@topic).to be_root_topic
end
end
context "#discussion_subentry_count" do
it "returns the count of all active discussion_entries" do
@topic = @course.discussion_topics.create(:title => "topic")
@topic.reply_from(:user => @teacher, :text => "entry 1").destroy # no count
@topic.reply_from(:user => @teacher, :text => "entry 1") # 1
@entry = @topic.reply_from(:user => @teacher, :text => "entry 2") # 2
@entry.reply_from(:user => @student, :html => "reply 1") # 3
@entry.reply_from(:user => @student, :html => "reply 2") # 4
# expect
expect(@topic.discussion_subentry_count).to eq 4
end
end
context "for_assignment?" do
it "should not be for_assignment? unless it has an assignment" do
@topic = @course.discussion_topics.create(:title => "topic")
expect(@topic).not_to be_for_assignment
@topic.assignment = @course.assignments.build(:submission_types => 'discussion_topic', :title => @topic.title)
@topic.assignment.infer_times
@topic.assignment.saved_by = :discussion_topic
@topic.save
expect(@topic).to be_for_assignment
end
end
context "for_group_discussion?" do
it "should not be for_group_discussion? unless it has a group_category" do
course_with_student(:active_all => true)
@topic = @course.discussion_topics.build(:title => "topic")
@assignment = @course.assignments.build(:submission_types => 'discussion_topic', :title => @topic.title)
@assignment.infer_times
@assignment.saved_by = :discussion_topic
@topic.assignment = @assignment
@topic.save
expect(@topic).not_to be_for_group_discussion
@topic.group_category = @course.group_categories.create(:name => "category")
@topic.save
expect(@topic).to be_for_group_discussion
end
end
context "should_send_to_stream" do
context "in a published course" do
it "should be true for non-assignment discussions" do
@topic = @course.discussion_topics.create(:title => "topic")
expect(@topic.should_send_to_stream).to be_truthy
end
it "should be true for non-group discussion assignments" do
@topic = @course.discussion_topics.build(:title => "topic")
@assignment = @course.assignments.build(:submission_types => 'discussion_topic', :title => @topic.title, :due_at => 1.day.from_now)
@assignment.saved_by = :discussion_topic
@topic.assignment = @assignment
@topic.save
expect(@topic.should_send_to_stream).to be_truthy
end
it "should be true for the parent topic only in group discussions, not the subtopics" do
group_category = @course.group_categories.create(:name => "category")
@parent_topic = @course.discussion_topics.create(:title => "parent topic")
@parent_topic.group_category = group_category
@parent_topic.save
@subtopic = @parent_topic.child_topics.build(:title => "subtopic")
@subtopic.group_category = group_category
@assignment = @course.assignments.build(:submission_types => 'discussion_topic', :title => @subtopic.title, :due_at => 1.day.from_now)
@assignment.saved_by = :discussion_topic
@subtopic.assignment = @assignment
@subtopic.save
expect(@parent_topic.should_send_to_stream).to be_truthy
expect(@subtopic.should_send_to_stream).to be_falsey
end
end
it "should not send stream items to students if course isn't published'" do
@course.update_attribute(:workflow_state, "created")
topic = @course.discussion_topics.create!(:title => "secret topic", :user => @teacher)
expect(@student.stream_item_instances.count).to eq 0
expect(@teacher.stream_item_instances.count).to eq 1
topic.discussion_entries.create!
expect(@student.stream_item_instances.count).to eq 0
expect(@teacher.stream_item_instances.count).to eq 1
end
it "should send stream items to students for graded discussions" do
@topic = @course.discussion_topics.build(:title => "topic")
@assignment = @course.assignments.build(:submission_types => 'discussion_topic', :title => @topic.title)
@assignment.saved_by = :discussion_topic
@topic.assignment = @assignment
@topic.save
expect(@student.stream_item_instances.count).to eq 1
end
end
context "posting first to view" do
before(:once) do
@observer = user_factory(active_all: true)
@context = @course
discussion_topic_model
@topic.require_initial_post = true
@topic.save
end
it "should allow admins to see posts without posting" do
expect(@topic.user_can_see_posts?(@teacher)).to eq true
end
it "should allow course admins to see posts in concluded group topics without posting" do
group_category = @course.group_categories.create(:name => "category")
@group = @course.groups.create(:name => "group", :group_category => group_category)
@topic.update_attribute(:group_category, group_category)
subtopic = @topic.child_topics.first
@course.complete!
expect(subtopic.user_can_see_posts?(@teacher)).to eq true
end
it "should only allow active admins to see posts without posting" do
@ta_enrollment = course_with_ta(:course => @course, :active_enrollment => true)
# TA should be able to see
expect(@topic.user_can_see_posts?(@ta)).to eq true
# Remove user as TA and enroll as student, should not be able to see
@ta_enrollment.destroy
# enroll as a student.
course_with_student(:course => @course, :user => @ta, :active_enrollment => true)
@topic.reload
@topic.clear_permissions_cache(@ta)
expect(@topic.user_can_see_posts?(@ta)).to eq false
end
it "shouldn't allow student who hasn't posted to see" do
expect(@topic.user_can_see_posts?(@student)).to eq false
end
it "should not allow participation in deleted discussions" do
@topic.destroy
expect {@topic.discussion_entries.create!(:message => "second message", :user => @student)}.to raise_error(ActiveRecord::RecordInvalid)
expect {@topic.discussion_entries.create!(:message => "second message", :user => @teacher)}.to raise_error(ActiveRecord::RecordInvalid)
end
it "should throw incomingMail error when reply to deleted discussion" do
@topic.destroy
expect { @topic.reply_from(:user => @teacher, :text => "hai") }.to raise_error(IncomingMail::Errors::ReplyToDeletedDiscussion)
expect { @topic.reply_from(:user => @student, :text => "hai") }.to raise_error(IncomingMail::Errors::ReplyToDeletedDiscussion)
end
it "should allow student who has posted to see" do
@topic.reply_from(:user => @student, :text => 'hai')
expect(@topic.user_can_see_posts?(@student)).to eq true
end
it "should work the same for group discussions" do
group_discussion_assignment
@topic.require_initial_post = true
@topic.save!
ct = @topic.child_topics.first
ct.context.add_user(@student)
expect(ct.user_can_see_posts?(@student)).to be_falsey
ct.reply_from(user: @student, text: 'ohai')
ct.user_ids_who_have_posted_and_admins
expect(ct.user_can_see_posts?(@student)).to be_truthy
end
describe "observers" do
before :once do
@other_student = user_factory(:active_all => true)
@course.enroll_student(@other_student, :enrollment_state => 'active')
@course.enroll_user(@observer, 'ObserverEnrollment',
:associated_user_id => @student, :enrollment_state => 'active')
@course.enroll_user(@observer, 'ObserverEnrollment',
:associated_user_id => @other_student, :enrollment_state => 'active')
end
it "does not allow observers to see replies to a discussion linked students haven't posted in" do
expect(@topic.initial_post_required?(@observer)).to be
end
# previously this worked for exactly one observer enrollment, whichever became @context_enrollment
# so test both ways
it "allows observers to see replies in a discussion a linked student has posted in (1/2)" do
@topic.reply_from(:user => @student, :text => 'wat')
expect(@topic.initial_post_required?(@observer)).not_to be
end
it "allows observers to see replies in a discussion a linked student has posted in (2/2)" do
@topic.reply_from(:user => @other_student, :text => 'wat')
expect(@topic.initial_post_required?(@observer)).not_to be
end
end
end
context "subscribers" do
before :once do
@context = @course
discussion_topic_model(:user => @teacher)
end
it "should automatically include the author" do
expect(@topic.subscribers).to include(@teacher)
end
it "should not include the author if they unsubscribe" do
@topic.unsubscribe(@teacher)
expect(@topic.subscribers).not_to include(@teacher)
end
it "should automatically include posters" do
@topic.reply_from(:user => @student, :text => "entry")
expect(@topic.subscribers).to include(@student)
end
it "should include author when topic was created before subscriptions where added" do
participant = @topic.update_or_create_participant(current_user: @topic.user, subscribed: nil)
expect(participant.subscribed).to be_nil
expect(@topic.subscribers.map(&:id)).to include(@teacher.id)
end
it "should include users that have posted entries before subscriptions were added" do
@topic.reply_from(:user => @student, :text => "entry")
participant = @topic.update_or_create_participant(current_user: @student, subscribed: nil)
expect(participant.subscribed).to be_nil
expect(@topic.subscribers.map(&:id)).to include(@student.id)
end
it "should not include posters if they unsubscribe" do
@topic.reply_from(:user => @student, :text => "entry")
@topic.unsubscribe(@student)
expect(@topic.subscribers).not_to include(@student)
end
it "should resubscribe unsubscribed users if they post" do
@topic.reply_from(:user => @student, :text => "entry")
@topic.unsubscribe(@student)
@topic.reply_from(:user => @student, :text => "another entry")
expect(@topic.subscribers).to include(@student)
end
it "should include users who subscribe" do
@topic.subscribe(@student)
expect(@topic.subscribers).to include(@student)
end
it "should not include anyone no longer in the course" do
@topic.subscribe(@student)
@topic2 = @course.discussion_topics.create!(:title => "student topic", :message => "I'm outta here", :user => @student)
@student.enrollments.first.destroy
expect(@topic.subscribers).not_to include(@student)
expect(@topic2.subscribers).not_to include(@student)
end
context "differentiated_assignments" do
before do
@assignment = @course.assignments.create!(:title => "some discussion assignment",only_visible_to_overrides: true)
@assignment.submission_types = 'discussion_topic'
@assignment.save!
@topic.assignment_id = @assignment.id
@topic.save!
@section = @course.course_sections.create!(name: "test section")
create_section_override_for_assignment(@topic.assignment, {course_section: @section})
end
context "enabled" do
it "should filter subscribers based on visibility" do
@topic.subscribe(@student)
expect(@topic.subscribers).not_to include(@student)
student_in_section(@section, user: @student)
expect(@topic.subscribers).to include(@student)
end
it "filters observers if their student cant see" do
@observer = user_factory(active_all: true, :name => "Observer")
observer_enrollment = @course.enroll_user(@observer, 'ObserverEnrollment', :section => @section, :enrollment_state => 'active')
observer_enrollment.update_attribute(:associated_user_id, @student.id)
@topic.subscribe(@observer)
expect(@topic.subscribers.include?(@observer)).to be_falsey
student_in_section(@section, user: @student)
expect(@topic.subscribers.include?(@observer)).to be_truthy
end
it "doesnt filter for observers with no student" do
@observer = user_factory(active_all: true)
observer_enrollment = @course.enroll_user(@observer, 'ObserverEnrollment', :section => @section, :enrollment_state => 'active')
@topic.subscribe(@observer)
expect(@topic.subscribers).to include(@observer)
end
it "should work for graded subtopics" do
group_discussion_assignment
ct = @topic.child_topics.first
ct.context.add_user(@student)
@topic = @topic.child_topics.first
@topic.subscribe(@student)
@topic.save!
expect(@topic.subscribers).to include(@student)
end
end
end
end
context "visible_to_students_in_course_with_da" do
before :once do
@context = @course
discussion_topic_model(:user => @teacher)
@assignment = @course.assignments.create!(:title => "some discussion assignment",only_visible_to_overrides: true)
@assignment.submission_types = 'discussion_topic'
@assignment.save!
@topic.assignment_id = @assignment.id
@topic.save!
@section = @course.course_sections.create!(name: "test section")
@student = create_users(1, return_type: :record).pop
student_in_section(@section, user: @student)
end
it "returns discussions that have assignment and visibility" do
create_section_override_for_assignment(@topic.assignment, {course_section: @section})
expect(DiscussionTopic.visible_to_students_in_course_with_da([@student.id],[@course.id])).to include(@topic)
end
it "returns discussions that have no assignment" do
@topic.assignment_id = nil
@topic.save!
expect(DiscussionTopic.visible_to_students_in_course_with_da([@student.id],[@course.id])).to include(@topic)
end
it "does not return discussions that have an assignment and no visibility" do
expect(DiscussionTopic.visible_to_students_in_course_with_da([@student.id],[@course.id])).not_to include(@topic)
end
end
context "posters" do
before :once do
@context = @course
discussion_topic_model(:user => @teacher)
end
it "should include the topic author" do
expect(@topic.posters).to include(@teacher)
end
it "should include users that have posted entries" do
@student = student_in_course(:active_all => true).user
@topic.reply_from(:user => @student, :text => "entry")
expect(@topic.posters).to include(@student)
end
it "should include users that have replies to entries" do
@entry = @topic.reply_from(:user => @teacher, :text => "entry")
@student = student_in_course(:active_all => true).user
@entry.reply_from(:user => @student, :html => "reply")
@topic.reload
expect(@topic.posters).to include(@student)
end
it "should dedupe users" do
@entry = @topic.reply_from(:user => @teacher, :text => "entry")
@student = student_in_course(:active_all => true).user
@entry.reply_from(:user => @student, :html => "reply 1")
@entry.reply_from(:user => @student, :html => "reply 2")
@topic.reload
expect(@topic.posters).to include(@teacher)
expect(@topic.posters).to include(@student)
expect(@topic.posters.size).to eq 2
end
it "should not include topic author if she is no longer enrolled in the course" do
student_in_course(:active_all => true)
@topic2 = @course.discussion_topics.create!(:title => "student topic", :message => "I'm outta here", :user => @student)
@entry = @topic2.discussion_entries.create!(:message => "go away", :user => @teacher)
expect(@topic2.posters.map(&:id).sort).to eql [@student.id, @teacher.id].sort
@student.enrollments.first.destroy
expect(@topic2.posters.map(&:id).sort).to eql [@teacher.id].sort
end
end
context "submissions when graded" do
before :once do
@context = @course
discussion_topic_model(:user => @teacher)
end
def build_submitted_assignment
@assignment = @course.assignments.create!(:title => "some discussion assignment")
@assignment.submission_types = 'discussion_topic'
@assignment.save!
@topic.assignment_id = @assignment.id
@topic.save!
@entry1 = @topic.discussion_entries.create!(:message => "second message", :user => @student)
@entry1.created_at = 1.week.ago
@entry1.save!
@submission = @assignment.submissions.where(:user_id => @entry1.user_id).first
end
it "should not re-flag graded discussion as needs grading if student make another comment" do
assignment = @course.assignments.create(:title => "discussion assignment", :points_possible => 20)
topic = @course.discussion_topics.create!(:title => 'discussion topic 1', :message => "this is a new discussion topic", :assignment => assignment)
topic.discussion_entries.create!(:message => "student message for grading", :user => @student)
submissions = Submission.where(user_id: @student, assignment_id: assignment).to_a
expect(submissions.count).to eq 1
student_submission = submissions.first
assignment.grade_student(@student, grade: 9, grader: @teacher)
student_submission.reload
expect(student_submission.workflow_state).to eq 'graded'
topic.discussion_entries.create!(:message => "student message 2 for grading", :user => @student)
submissions = Submission.where(user_id: @student, assignment_id: assignment).to_a
expect(submissions.count).to eq 1
student_submission = submissions.first
expect(student_submission.workflow_state).to eq 'graded'
end
it "should create submissions for existing entries when setting the assignment (even if locked)" do
@topic.reply_from(:user => @student, :text => "entry")
@student.reload
expect(@student.submissions).to be_empty
@assignment = assignment_model(:course => @course, :lock_at => 1.day.ago)
@topic.assignment = @assignment
@topic.save
@student.reload
expect(@student.submissions.size).to eq 1
expect(@student.submissions.first.submission_type).to eq 'discussion_topic'
end
it "should create submissions for existing entries in group topics when setting the assignment (even if locked)" do
group_category = @course.group_categories.create!(:name => "category")
@group1 = @course.groups.create!(:name => "group 1", :group_category => group_category)
@topic.group_category = group_category
@topic.save!
child_topic = @topic.child_topics.first
child_topic.context.add_user(@student)
child_topic.reply_from(:user => @student, :text => "entry")
@student.reload
expect(@student.submissions).to be_empty
@assignment = assignment_model(:course => @course, :lock_at => 1.day.ago)
@topic.assignment = @assignment
@topic.save
@student.reload
expect(@student.submissions.size).to eq 1
expect(@student.submissions.first.submission_type).to eq 'discussion_topic'
end
it "should have the correct submission date if submission has comment" do
@assignment = @course.assignments.create!(:title => "some discussion assignment")
@assignment.submission_types = 'discussion_topic'
@assignment.save!
@topic.assignment = @assignment
@topic.save
@submission = @assignment.find_or_create_submission(@student.id)
@submission_comment = @submission.add_comment(:author => @teacher, :comment => "some comment")
@submission.created_at = 1.week.ago
@submission.save!
expect(@submission.workflow_state).to eq 'unsubmitted'
expect(@submission.submitted_at).to be_nil
@entry = @topic.discussion_entries.create!(:message => "somne discussion message", :user => @student)
@submission.reload
expect(@submission.workflow_state).to eq 'submitted'
expect(@submission.submitted_at.to_i).to be >= @entry.created_at.to_i #this time may not be exact because it goes off of time.now in the submission
end
it "should fix submission date after deleting the oldest entry" do
build_submitted_assignment()
@entry2 = @topic.discussion_entries.create!(:message => "some message", :user => @student)
@entry2.created_at = 1.day.ago
@entry2.save!
@entry1.destroy
@topic.reload
expect(@topic.discussion_entries).not_to be_empty
expect(@topic.discussion_entries.active).not_to be_empty
@submission.reload
expect(@submission.submitted_at.to_i).to eq @entry2.created_at.to_i
expect(@submission.workflow_state).to eq 'submitted'
end
it "should mark submission as unsubmitted after deletion" do
build_submitted_assignment()
@entry1.destroy
@topic.reload
expect(@topic.discussion_entries).not_to be_empty
expect(@topic.discussion_entries.active).to be_empty
@submission.reload
expect(@submission.workflow_state).to eq 'unsubmitted'
expect(@submission.submission_type).to eq nil
expect(@submission.submitted_at).to eq nil
end
it "should have new submission date after deletion and re-submission" do
build_submitted_assignment()
@entry1.destroy
@topic.reload
expect(@topic.discussion_entries).not_to be_empty
expect(@topic.discussion_entries.active).to be_empty
@entry2 = @topic.discussion_entries.create!(:message => "some message", :user => @student)
@submission.reload
expect(@submission.submitted_at.to_i).to be >= @entry2.created_at.to_i #this time may not be exact because it goes off of time.now in the submission
expect(@submission.workflow_state).to eq 'submitted'
end
it "should not duplicate submissions for existing entries that already have submissions" do
@assignment = assignment_model(:course => @course)
@topic.assignment = @assignment
@topic.save
@topic.reload # to get the student in topic.assignment.context.students
@topic.reply_from(:user => @student, :text => "entry")
@student.reload
expect(@student.submissions.size).to eq 1
@existing_submission_id = @student.submissions.first.id
@topic.assignment = nil
@topic.save
@topic.reply_from(:user => @student, :text => "another entry")
@student.reload
expect(@student.submissions.size).to eq 1
expect(@student.submissions.first.id).to eq @existing_submission_id
@topic.assignment = @assignment
@topic.save
@student.reload
expect(@student.submissions.size).to eq 1
expect(@student.submissions.first.id).to eq @existing_submission_id
end
it "should not resubmit graded discussion submissions" do
@assignment = assignment_model(:course => @course)
@topic.assignment = @assignment
@topic.save!
@topic.reload
@topic.reply_from(:user => @student, :text => "entry")
@student.reload
@assignment.grade_student(@student, grade: 1, grader: @teacher)
@submission = Submission.where(:user_id => @student, :assignment_id => @assignment).first
expect(@submission.workflow_state).to eq 'graded'
@topic.ensure_submission(@student)
expect(@submission.reload.workflow_state).to eq 'graded'
end
it "should associate attachments with graded discussion submissions" do
@assignment = assignment_model(:course => @course)
@topic.assignment = @assignment
@topic.save!
@topic.reload
attachment_model(:context => @user, :uploaded_data => stub_png_data, :filename => "homework.png")
entry = @topic.reply_from(:user => @student, :text => "entry")
entry.attachment = @attachment
entry.save!
@topic.ensure_submission(@student)
sub = @assignment.submissions.where(:user_id => @student).first
expect(sub.attachments.to_a).to eq [@attachment]
end
it "should associate attachments with graded discussion submissions even with silly deleted topics" do
gc1 = group_category(:name => "gc1")
group_with_user(group_category: gc1, user: @student, :context => @course)
gc2 = group_category(:name => "gc2")
group_with_user(group_category: gc2, user: @student, :context => @course)
group2 = @group
@assignment = assignment_model(:course => @course)
@topic.assignment = @assignment
@topic.group_category = gc1
@topic.save!
@topic.group_category = gc2 # switching group categories deletes the old child topics
@topic.save!
@topic.reload
# can't use child_topic_for to show the exact bug
# because that's where the reported bug is
sub_topic = @topic.child_topics.where(:context_type => "Group", :context_id => group2).first
attachment_model(:context => @user, :uploaded_data => stub_png_data, :filename => "homework.png")
entry = sub_topic.reply_from(:user => @student, :text => "entry")
entry.attachment = @attachment
entry.save!
sub = @assignment.submissions.where(:user_id => @student).first
expect(sub.attachments.to_a).to eq [@attachment]
end
end
describe "#unread_count" do
let(:topic) do
@course.discussion_topics.create!(:title => "title", :message => "message")
end
it "returns 0 for a nil user" do
topic.discussion_entries.create!
expect(topic.unread_count(nil)).to eq 0
end
it "returns the default_unread_count if the user has no discussion_topic_participant" do
topic.discussion_entries.create!
student_in_course
expect(topic.unread_count(@student)).to eq 1
end
end
context "read/unread state" do
def check_read_state_scopes(read: false, user: nil)
return unless user
if read
expect(DiscussionTopic.read_for(user)).to be_include @topic
expect(DiscussionTopic.unread_for(user)).not_to be_include @topic
else
expect(DiscussionTopic.read_for(user)).not_to be_include @topic
expect(DiscussionTopic.unread_for(user)).to be_include @topic
end
end
before(:once) do
@topic = @course.discussion_topics.create!(:title => "title", :message => "message", :user => @teacher)
end
it "should mark a topic you created as read" do
expect(@topic.read?(@teacher)).to be_truthy
expect(@topic.unread_count(@teacher)).to eq 0
check_read_state_scopes read: true, user: @teacher
end
it "should be unread by default" do
expect(@topic.read?(@student)).to be_falsey
expect(@topic.unread_count(@student)).to eq 0
skip("check_read_state_scopes user: @student") # TODO: Fix
end
it "should allow being marked unread" do
@topic.change_read_state("unread", @teacher)
@topic.reload
expect(@topic.read?(@teacher)).to be_falsey
expect(@topic.unread_count(@teacher)).to eq 0
check_read_state_scopes user: @teacher
end
it "should allow being marked read" do
@topic.change_read_state("read", @student)
@topic.reload
expect(@topic.read?(@student)).to be_truthy
expect(@topic.unread_count(@student)).to eq 0
check_read_state_scopes read: true, user: @student
end
it "should allow mark all as unread with forced_read_state" do
@entry = @topic.discussion_entries.create!(:message => "Hello!", :user => @teacher)
@reply = @entry.reply_from(:user => @student, :text => "ohai!")
@reply.change_read_state('read', @teacher, :forced => false)
@topic.change_all_read_state("unread", @teacher, :forced => true)
@topic.reload
expect(@topic.read?(@teacher)).to be_falsey
expect(@entry.read?(@teacher)).to be_falsey
expect(@entry.find_existing_participant(@teacher)).to be_forced_read_state
expect(@reply.read?(@teacher)).to be_falsey
expect(@reply.find_existing_participant(@teacher)).to be_forced_read_state
expect(@topic.unread_count(@teacher)).to eq 2
check_read_state_scopes user: @teacher
end
it "should allow mark all as read without forced_read_state" do
@entry = @topic.discussion_entries.create!(:message => "Hello!", :user => @teacher)
@reply = @entry.reply_from(:user => @student, :text => "ohai!")
@reply.change_read_state('unread', @student, :forced => true)
@topic.change_all_read_state("read", @student)
@topic.reload
expect(@topic.read?(@student)).to be_truthy
expect(@entry.read?(@student)).to be_truthy
expect(@entry.find_existing_participant(@student)).not_to be_forced_read_state
expect(@reply.read?(@student)).to be_truthy
expect(@reply.find_existing_participant(@student)).to be_forced_read_state
expect(@topic.unread_count(@student)).to eq 0
check_read_state_scopes read: true, user: @student
end
it "should use unique_constaint_retry when updating read state" do
DiscussionTopic.expects(:unique_constraint_retry).once
@topic.change_read_state("read", @student)
end
it "should use unique_constaint_retry when updating all read state" do
DiscussionTopic.expects(:unique_constraint_retry).once
@topic.change_all_read_state("unread", @student)
end
it "should sync unread state with the stream item" do
@stream_item = @topic.reload_stream_item
expect(@stream_item.stream_item_instances.detect{|sii| sii.user_id == @teacher.id}).to be_read
expect(@stream_item.stream_item_instances.detect{|sii| sii.user_id == @student.id}).to be_unread
@topic.change_all_read_state("unread", @teacher)
@topic.change_all_read_state("read", @student)
@topic.reload
@stream_item = @topic.stream_item
expect(@stream_item.stream_item_instances.detect{|sii| sii.user_id == @teacher.id}).to be_unread
expect(@stream_item.stream_item_instances.detect{|sii| sii.user_id == @student.id}).to be_read
end
end
context "subscribing" do
before :once do
@context = @course
discussion_topic_model(:user => @teacher)
end
it "should allow subscription" do
expect(@topic.subscribed?(@student)).to be_falsey
@topic.subscribe(@student)
expect(@topic.subscribed?(@student)).to be_truthy
end
it "should allow unsubscription" do
expect(@topic.subscribed?(@teacher)).to be_truthy
@topic.unsubscribe(@teacher)
expect(@topic.subscribed?(@teacher)).to be_falsey
end
it "should be idempotent" do
expect(@topic.subscribed?(@student)).to be_falsey
@topic.unsubscribe(@student)
expect(@topic.subscribed?(@student)).to be_falsey
end
it "should assume the author is subscribed" do
expect(@topic.subscribed?(@teacher)).to be_truthy
end
it "should assume posters are subscribed" do
@topic.reply_from(:user => @student, :text => 'first post!')
expect(@topic.subscribed?(@student)).to be_truthy
end
context "when initial_post_required" do
it "should unsubscribe a user when all of their posts are deleted" do
@topic.require_initial_post = true
@topic.save!
@entry = @topic.reply_from(:user => @student, :text => 'first post!')
expect(@topic.subscribed?(@student)).to be_truthy
@entry.destroy
expect(@topic.subscribed?(@student)).to be_falsey
end
end
end
context "subscription holds" do
before :once do
@context = @course
end
it "should hold when requiring an initial post" do
discussion_topic_model(:user => @teacher, :require_initial_post => true)
expect(@topic.subscription_hold(@student, nil, nil)).to eql(:initial_post_required)
end
it "should hold when the user is not in a group set" do
# i.e. when you check holds on a root topic and no child topics are for groups
# the user is in
group_discussion_assignment
expect(@topic.subscription_hold(@student, nil, nil)).to eql(:not_in_group_set)
end
it "should hold when the user is not in a group" do
group_discussion_assignment
expect(@topic.child_topics.first.subscription_hold(@student, nil, nil)).to eql(:not_in_group)
end
it "should handle nil user case" do
group_discussion_assignment
expect(@topic.child_topics.first.subscription_hold(nil, nil, nil)).to be_nil
end
it "should not subscribe the author if there is a hold" do
group_discussion_assignment
@topic.user = @teacher
@topic.save!
expect(@topic.subscription_hold(@teacher, nil, nil)).to eql(:not_in_group_set)
expect(@topic.subscribed?(@teacher)).to be_falsey
end
it "should set the topic participant subscribed field to false when there is a hold" do
teacher_in_course(:active_all => true)
group_discussion_assignment
group_discussion = @topic.child_topics.first
group_discussion.user = @teacher
group_discussion.save!
group_discussion.change_read_state('read', @teacher) # quick way to make a participant
expect(group_discussion.discussion_topic_participants.where(:user_id => @teacher.id).first.subscribed).to eq false
end
end
context "a group topic subscription" do
before(:once) do
group_discussion_assignment
end
it "should return true if the user is subscribed to a child topic" do
@topic.child_topics.first.subscribe(@student)
expect(@topic.child_topics.first.subscribed?(@student)).to be_truthy
expect(@topic.subscribed?(@student)).to be_truthy
end
it "should return true if the user has posted to a child topic" do
child_topic = @topic.child_topics.first
child_topic.context.add_user(@student)
child_topic.reply_from(:user => @student, :text => "post")
child_topic_participant = child_topic.update_or_create_participant(:current_user => @student, :subscribed => nil)
expect(child_topic_participant.subscribed).to be_nil
expect(@topic.subscribed?(@student)).to be_truthy
end
it "should subscribe a group user to the child topic" do
child_one, child_two = @topic.child_topics
child_one.context.add_user(@student)
@topic.subscribe(@student)
expect(child_one.subscribed?(@student)).to be_truthy
expect(child_two.subscribed?(@student)).not_to be_truthy
expect(@topic.subscribed?(@student)).to be_truthy
end
it "should unsubscribe a group user from the child topic" do
child_one, child_two = @topic.child_topics
child_one.context.add_user(@student)
@topic.subscribe(@student)
@topic.unsubscribe(@student)
expect(child_one.subscribed?(@student)).not_to be_truthy
expect(child_two.subscribed?(@student)).not_to be_truthy
expect(@topic.subscribed?(@student)).not_to be_truthy
end
end
context "materialized view" do
before :once do
topic_with_nested_replies
end
around do |example|
# materialized view jobs are now delayed
Timecop.freeze(Time.zone.now + 20.seconds, &example)
end
it "should return nil if the view has not been built yet, and schedule a job" do
DiscussionTopic::MaterializedView.for(@topic).destroy
expect(@topic.materialized_view).to be_nil
expect(@topic.materialized_view).to be_nil
expect(Delayed::Job.strand_size("materialized_discussion:#{@topic.id}")).to eq 1
end
it "should return the materialized view if it's up to date" do
run_jobs
view = DiscussionTopic::MaterializedView.where(discussion_topic_id: @topic).first
expect(@topic.materialized_view).to eq [view.json_structure, view.participants_array, view.entry_ids_array, []]
end
it "should update the materialized view on new entry" do
run_jobs
expect(Delayed::Job.strand_size("materialized_discussion:#{@topic.id}")).to eq 0
@topic.reply_from(:user => @user, :text => "ohai")
expect(Delayed::Job.strand_size("materialized_discussion:#{@topic.id}")).to eq 1
end
it "should update the materialized view on edited entry" do
reply = @topic.reply_from(:user => @user, :text => "ohai")
run_jobs
expect(Delayed::Job.strand_size("materialized_discussion:#{@topic.id}")).to eq 0
reply.update_attributes(:message => "i got that wrong before")
expect(Delayed::Job.strand_size("materialized_discussion:#{@topic.id}")).to eq 1
end
it "should return empty data for a materialized view on a new (unsaved) topic" do
new_topic = DiscussionTopic.new(:context => @topic.context, :discussion_type => DiscussionTopic::DiscussionTypes::SIDE_COMMENT)
expect(new_topic).to be_new_record
expect(new_topic.materialized_view).to eq [ "[]", [], [], [] ]
expect(Delayed::Job.strand_size("materialized_discussion:#{new_topic.id}")).to eq 0
end
end
context "destroy" do
before(:once) { group_discussion_assignment }
it "should destroy the assignment and associated child topics" do
@topic.destroy
expect(@topic.reload).to be_deleted
@topic.child_topics.each{ |ct| expect(ct.reload).to be_deleted }
expect(@assignment.reload).to be_deleted
end
it "should not revive the assignment if updated when deleted" do
@topic.destroy
expect(@assignment.reload).to be_deleted
@topic.touch
expect(@assignment.reload).to be_deleted
end
end
context "restore" do
it "should restore the assignment and associated child topics" do
group_discussion_assignment
@topic.destroy
@topic.reload.assignment.expects(:restore).with(:discussion_topic).once
@topic.restore
expect(@topic.reload).to be_unpublished
@topic.child_topics.each { |ct| expect(ct.reload).to be_unpublished }
expect(@topic.assignment).to be_unpublished
end
it "should restore an announcement to active state" do
ann = @course.announcements.create!(:title => "something", :message => "somethingelse")
ann.destroy
ann.restore
expect(ann.reload).to be_active
end
it "should restore a topic with submissions to active state" do
discussion_topic_model(:context => @course)
@topic.reply_from(user: @student, text: "huttah!")
@topic.destroy
@topic.restore
expect(@topic.reload).to be_active
end
end
describe "reply_from" do
it "should ignore responses in deleted account" do
account = Account.create!
@teacher = course_with_teacher(:active_all => true, :account => account).user
@context = @course
discussion_topic_model(:user => @teacher)
account.destroy
expect { @topic.reload.reply_from(:user => @teacher, :text => "entry") }.to raise_error(IncomingMail::Errors::UnknownAddress)
end
it "should prefer html to text" do
discussion_topic_model
msg = @topic.reply_from(:user => @teacher, :text => "text body", :html => "<p>html body</p>")
expect(msg).not_to be_nil
expect(msg.message).to eq "<p>html body</p>"
end
it "should not allow replies from students to locked topics" do
course_with_teacher(:active_all => true)
discussion_topic_model(:context => @course)
@topic.lock!
@topic.reply_from(:user => @teacher, :text => "reply") # should not raise error
student_in_course(:course => @course).accept!
expect { @topic.reply_from(:user => @student, :text => "reply") }.to raise_error(IncomingMail::Errors::ReplyToLockedTopic)
end
it "should reflect course setting for when lock_all_announcements is enabled" do
announcement = @course.announcements.create!(message: "Lock this")
expect(announcement.comments_disabled?).to be_falsey
@course.lock_all_announcements = true
@course.save!
expect(announcement.reload.comments_disabled?).to be_truthy
end
it "should reflect account setting for when lock_all_announcements is enabled" do
announcement = @course.announcements.create!(message: "Lock this")
expect(announcement.comments_disabled?).to be_falsey
@course.account.tap{|a| a.settings[:lock_all_announcements] = {:value => true, :locked => true}; a.save!}
expect(announcement.reload.comments_disabled?).to be_truthy
end
it "should not allow replies from students to topics locked based on date" do
course_with_teacher(:active_all => true)
discussion_topic_model(:context => @course)
@topic.unlock_at = 1.day.from_now
@topic.save!
@topic.reply_from(:user => @teacher, :text => "reply") # should not raise error
student_in_course(:course => @course).accept!
expect { @topic.reply_from(:user => @student, :text => "reply") }.to raise_error(IncomingMail::Errors::ReplyToLockedTopic)
end
end
describe "update_order" do
it "should handle existing null positions" do
topics = (1..4).map{discussion_topic_model(pinned: true)}
topics.each {|x| x.position = nil; x.save}
new_order = [2, 3, 4, 1]
ids = new_order.map {|x| topics[x-1].id}
topics[0].update_order(ids)
expect(topics.first.list_scope.map(&:id)).to eq ids
end
end
describe "context_module_action" do
context "group discussion" do
before :once do
group_assignment_discussion(course: @course)
@module = @course.context_modules.create!
@topic_tag = @module.add_item(type: 'discussion_topic', id: @root_topic.id)
@module.completion_requirements = { @topic_tag.id => { type: 'must_contribute' } }
@module.save!
student_in_course active_all: true
@group.add_user @student, 'accepted'
end
it "fulfills module completion requirements on the root topic" do
@topic.reply_from(user: @student, text: "huttah!")
expect(@student.context_module_progressions.where(context_module_id: @module).first.requirements_met).to include({id: @topic_tag.id, type: 'must_contribute'})
end
end
end
describe "locked by context module" do
before(:once) do
discussion_topic_model(context: @course)
@module = @course.context_modules.create!(name: 'some module')
@module.add_item(type: 'discussion_topic', id: @topic.id)
@module.unlock_at = 2.months.from_now
@module.save!
@topic.reload
end
it "stays visible_for? student even when locked by module" do
expect(@topic.visible_for?(@student)).to be_truthy
end
it "is locked_for? students when locked by module" do
expect(@topic.locked_for?(@student, deep_check_if_needed: true)).to be_truthy
end
describe "reject_context_module_locked_topics" do
it "filters module locked topics for students" do
topics = DiscussionTopic.reject_context_module_locked_topics([@topic], @student)
expect(topics).to be_empty
end
it "does not filter module locked topics for teachers" do
topics = DiscussionTopic.reject_context_module_locked_topics([@topic], @teacher)
expect(topics).not_to be_empty
end
end
end
describe 'entries_for_feed' do
before(:once) do
@topic = @course.discussion_topics.create!(user: @teacher, message: 'topic')
@entry1 = @topic.discussion_entries.create!(user: @teacher, message: 'hi from teacher')
@entry2 = @topic.discussion_entries.create!(user: @student, message: 'hi')
end
it 'returns active entries by default' do
expect(@topic.entries_for_feed(@student)).to_not be_empty
end
it 'returns empty if user cannot see posts' do
expect(@topic.entries_for_feed(nil)).to be_empty
end
it 'returns empty if the topic is locked for the user' do
@topic.lock!
expect(@topic.entries_for_feed(@student)).to be_empty
end
it 'returns student entries if specified' do
@topic.update_attributes(podcast_has_student_posts: true)
expect(@topic.entries_for_feed(@student, true)).to match_array([@entry1, @entry2])
end
it 'only returns admin entries if specified' do
@topic.update_attributes(podcast_has_student_posts: false)
expect(@topic.entries_for_feed(@student, true)).to match_array([@entry1])
end
it 'returns student entries for group discussions even if not specified' do
group_category
membership = group_with_user(group_category: @group_category, user: @student)
@topic = @group.discussion_topics.create(title: "group topic", user: @teacher)
@topic.discussion_entries.create(message: "some message", user: @student)
@topic.update_attributes(podcast_has_student_posts: false)
expect(@topic.entries_for_feed(@student, true)).to_not be_empty
end
end
describe 'to_podcast' do
it "includes media extension in enclosure url even though it is a redirect (for itunes)" do
@topic = @course.discussion_topics.create!(
user: @teacher,
message: 'topic'
)
attachment_model(:context => @course, :filename => 'test.mp4', :content_type => 'video')
@attachment.podcast_associated_asset = @topic
rss = DiscussionTopic.to_podcast([@attachment])
expect(rss.first.enclosure.url).to match(%r{download.mp4})
end
end
context "announcements" do
context "scopes" do
context "by_posted_at" do
let(:c) { Course.create! }
let(:new_ann) do
lambda do
Announcement.create!({
context: c,
message: "Test Message",
})
end
end
it "properly sorts collections by delayed_post_at and posted_at" do
anns = 10.times.map do |i|
ann = new_ann.call
setter = [:delayed_post_at=, :posted_at=][i % 2]
ann.send(setter, i.days.ago)
ann.position = 1
ann.save!
ann
end
expect(c.announcements.by_posted_at).to eq(anns)
end
end
end
end
context "notifications" do
before :once do
user_with_pseudonym(:active_all => true)
course_with_teacher(:user => @user, :active_enrollment => true)
n = Notification.create!(:name => "New Discussion Topic", :category => "TestImmediately")
NotificationPolicy.create!(:notification => n, :communication_channel => @user.communication_channel, :frequency => "immediately")
end
it "should send a message for a published course" do
@course.offer!
topic = @course.discussion_topics.create!(:title => "title")
expect(topic.messages_sent["New Discussion Topic"].map(&:user)).to be_include(@user)
end
it "should not send a message for an unpublished course" do
topic = @course.discussion_topics.create!(:title => "title")
expect(topic.messages_sent["New Discussion Topic"]).to be_blank
end
context "group discussions" do
before :once do
group_model(:context => @course)
@group.add_user(@user)
end
it "should send a message for a group discussion in a published course" do
@course.offer!
topic = @group.discussion_topics.create!(:title => "title")
expect(topic.messages_sent["New Discussion Topic"].map(&:user)).to be_include(@user)
end
it "should not send a message for a group discussion in an unpublished course" do
topic = @group.discussion_topics.create!(:title => "title")
expect(topic.messages_sent["New Discussion Topic"]).to be_blank
end
end
end
it "should let course admins reply to concluded topics" do
course_with_teacher(:active_all => true)
topic = @course.discussion_topics.create!
group_model(:context => @course)
group_topic = @group.discussion_topics.create!
@course.complete!
expect(topic.grants_right?(@teacher, :reply)).to be_truthy
expect(group_topic.grants_right?(@teacher, :reply)).to be_truthy
end
end