Guard ungraded discussions with new student_visibilities

refs LF-1051
flag=differentiated_modules

Test Plan
1. Check the following places for ungraded discussion
1a: Graphql discussion_type should be propertly guarded
1b. Discussion index page should be correctly guarded
1c. Discussion show page should be correctly guarded
1d. Discussion API should be correctly guarded
1e. Create an ungraded discussion that will unlock in the future
and verify that the correct message appears when opening it as a
student

Change-Id: I54f7a3667312ae06e0181e68859156774e7ebe21
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/346014
Reviewed-by: Jackson Howe <jackson.howe@instructure.com>
QA-Review: Jackson Howe <jackson.howe@instructure.com>
Product-Review: Jason Gillett <jason.gillett@instructure.com>
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
This commit is contained in:
Jason Gillett 2024-04-23 15:25:20 -06:00
parent 75c00a176b
commit 337c8fb2c2
6 changed files with 366 additions and 4 deletions

View File

@ -741,9 +741,23 @@ class DiscussionTopic < ActiveRecord::Base
topic_participant
end
scope :joins_ungraded_discussion_student_visibilities, lambda { |user_ids, course_ids|
joins(:ungraded_discussion_student_visibilities)
.where(ungraded_discussion_student_visibilities: { user_id: user_ids, course_id: course_ids })
}
scope :visible_to_students_in_course_with_da, lambda { |user_ids, course_ids|
without_assignment_in_course(course_ids)
.union(joins_assignment_student_visibilities(user_ids, course_ids))
if Account.site_admin.feature_enabled?(:differentiated_modules)
where.not(assignment_id: nil)
.joins_assignment_student_visibilities(user_ids, course_ids)
.union(
where(assignment_id: nil)
.joins_ungraded_discussion_student_visibilities(user_ids, course_ids)
)
else
without_assignment_in_course(course_ids)
.union(joins_assignment_student_visibilities(user_ids, course_ids))
end
}
scope :not_ignored_by, lambda { |user, purpose|
@ -1620,14 +1634,27 @@ class DiscussionTopic < ActiveRecord::Base
next false unless section_visibilities.intersect?(course_specific_sections)
end
end
# Verify that section limited teachers/ta's are properly restricted when differentiated_modules is enabled
if context.is_a?(Course) && (Account.site_admin.feature_enabled?(:differentiated_modules) && !visible_to_everyone && context.user_is_instructor?(user))
section_overrides = assignment_overrides.active.where(set_type: "CourseSection").pluck(:set_id)
visible_sections_for_user = context.course_section_visibility(user)
next false if visible_sections_for_user == :none
# If there are no section_overrides, then no check for section_specific instructor roles is needed
if visible_sections_for_user != :all && section_overrides.any?
course_specific_sections = course_sections.pluck(:id)
next false unless visible_sections_for_user.intersect?(course_specific_sections)
end
end
# user is an admin in the context (teacher/ta/designer) OR
# user is an account admin with appropriate permission
next true if context.grants_any_right?(user, :manage, :read_course_content)
# assignment exists and isn't assigned to user (differentiated assignments)
if for_assignment? && !assignment.visible_to_user?(user)
next false
if for_assignment?
next false unless assignment.visible_to_user?(user)
elsif Account.site_admin.feature_enabled?(:differentiated_modules)
next false unless visible_to_user?(user)
end
# topic is not published

View File

@ -53,6 +53,8 @@ module DifferentiableAssignment
ModuleStudentVisibility
when "WikiPage"
WikiPageStudentVisibility
when "DiscussionTopic"
UngradedDiscussionStudentVisibility
else
Quizzes::QuizStudentVisibility
end
@ -66,6 +68,8 @@ module DifferentiableAssignment
:context_module_id
when "WikiPage"
:wiki_page_id
when "DiscussionTopic"
:discussion_topic_id
else
:quiz_id
end

View File

@ -1163,6 +1163,88 @@ describe DiscussionTopicsController, type: :request do
end
end
end
describe "differentiated modules" do
before do
Account.site_admin.enable_feature! :differentiated_modules
end
context "ungraded discussions" do
before do
course_factory(active_all: true)
@course_section = @course.course_sections.create
@student1, @student2 = create_users(2, return_type: :record)
@course.enroll_student(@student1, enrollment_state: "active")
@course.enroll_student(@student2, enrollment_state: "active")
student_in_section(@course.course_sections.first, user: @student1)
student_in_section(@course.course_sections.second, user: @student2)
@teacher = teacher_in_course(course: @course, active_enrollment: true).user
@topic_visible_to_everyone = discussion_topic_model(user: @teacher, context: @course)
@topic = discussion_topic_model(user: @teacher, context: @course)
@topic.update!(only_visible_to_overrides: true)
end
it "shows only the visible topics" do
override = @topic.assignment_overrides.create!
override.assignment_override_students.create!(user: @student1)
@user = @student2
json = api_call(:get,
"/api/v1/courses/#{@course.id}/discussion_topics.json",
{ controller: "discussion_topics", action: "index", format: "json", course_id: @course.id.to_s })
expect(json.size).to eq 1
@user = @student1
json = api_call(:get,
"/api/v1/courses/#{@course.id}/discussion_topics.json",
{ controller: "discussion_topics", action: "index", format: "json", course_id: @course.id.to_s })
expect(json.size).to eq 2
end
it "is visible only to users who can access the assigned section" do
@topic.assignment_overrides.create!(set: @course_section)
@user = @student2
json = api_call(:get,
"/api/v1/courses/#{@course.id}/discussion_topics.json",
{ controller: "discussion_topics", action: "index", format: "json", course_id: @course.id.to_s })
expect(json.size).to eq 2
@user = @student1
json = api_call(:get,
"/api/v1/courses/#{@course.id}/discussion_topics.json",
{ controller: "discussion_topics", action: "index", format: "json", course_id: @course.id.to_s })
expect(json.size).to eq 1
end
it "is visible only to students in module override section" do
context_module = @course.context_modules.create!(name: "module")
context_module.content_tags.create!(content: @topic, context: @course)
override2 = @topic.assignment_overrides.create!(unlock_at: "2022-02-01T01:00:00Z",
unlock_at_overridden: true,
lock_at: "2022-02-02T01:00:00Z",
lock_at_overridden: true)
override2.assignment_override_students.create!(user: @student2)
@user = @student2
json = api_call(:get,
"/api/v1/courses/#{@course.id}/discussion_topics.json",
{ controller: "discussion_topics", action: "index", format: "json", course_id: @course.id.to_s })
expect(json.size).to eq 2
@user = @student1
json = api_call(:get,
"/api/v1/courses/#{@course.id}/discussion_topics.json",
{ controller: "discussion_topics", action: "index", format: "json", course_id: @course.id.to_s })
expect(json.size).to eq 1
end
end
end
end
describe "GET 'show'" do

View File

@ -368,6 +368,99 @@ describe DiscussionTopicsController do
expect(InstStatsd::Statsd).to have_received(:count).with("discussion_topic.index.visit.closed_for_comments", 0).at_least(:once)
end
end
describe "differentiated modules" do
before do
Account.site_admin.enable_feature! :differentiated_modules
end
context "ungraded discussions" do
before do
course_factory(active_all: true)
@course_section = @course.course_sections.create
@student1, @student2 = create_users(2, return_type: :record)
@course.enroll_student(@student1, enrollment_state: "active")
@course.enroll_student(@student2, enrollment_state: "active")
student_in_section(@course.course_sections.first, user: @student1)
student_in_section(@course.course_sections.second, user: @student2)
@teacher = teacher_in_course(course: @course, active_enrollment: true).user
@topic_visible_to_everyone = discussion_topic_model(user: @teacher, context: @course)
@topic = discussion_topic_model(user: @teacher, context: @course)
@topic.update!(only_visible_to_overrides: true)
end
it "shows only assigned topics" do
override = @topic.assignment_overrides.create!
override.assignment_override_students.create!(user: @student1)
user_session(@student2)
get "index", params: { course_id: @course.id }, format: :json
parsed_json = json_parse(response.body)
visible_ids_to_student_2 = parsed_json.pluck("id")
expect(assigns["topics"]).not_to include(@topic)
expect(visible_ids_to_student_2).to include(@topic_visible_to_everyone.id)
expect(visible_ids_to_student_2).not_to include(@topic.id)
user_session(@student1)
get "index", params: { course_id: @course.id }, format: :json
parsed_json = json_parse(response.body)
visible_ids_to_student_1 = parsed_json.pluck("id")
expect(visible_ids_to_student_1).to include(@topic_visible_to_everyone.id)
expect(visible_ids_to_student_1).to include(@topic.id)
end
it "is visible only to users who can access the assigned section" do
@topic.assignment_overrides.create!(set: @course_section)
user_session(@student2)
get "index", params: { course_id: @course.id }, format: :json
parsed_json = json_parse(response.body)
visible_ids_to_student_2 = parsed_json.pluck("id")
expect(visible_ids_to_student_2).to include(@topic_visible_to_everyone.id)
expect(visible_ids_to_student_2).to include(@topic.id)
user_session(@student1)
get "index", params: { course_id: @course.id }, format: :json
parsed_json = json_parse(response.body)
visible_ids_to_student_1 = parsed_json.pluck("id")
expect(visible_ids_to_student_1).to include(@topic_visible_to_everyone.id)
expect(visible_ids_to_student_1).not_to include(@topic.id)
end
it "is visible only to students in module override section" do
context_module = @course.context_modules.create!(name: "module")
context_module.content_tags.create!(content: @topic, context: @course)
override2 = @topic.assignment_overrides.create!(unlock_at: "2022-02-01T01:00:00Z",
unlock_at_overridden: true,
lock_at: "2022-02-02T01:00:00Z",
lock_at_overridden: true)
override2.assignment_override_students.create!(user: @student2)
user_session(@student2)
get "index", params: { course_id: @course.id }, format: :json
parsed_json = json_parse(response.body)
visible_ids_to_student_2 = parsed_json.pluck("id")
expect(visible_ids_to_student_2).to include(@topic_visible_to_everyone.id)
expect(visible_ids_to_student_2).to include(@topic.id)
user_session(@student1)
get "index", params: { course_id: @course.id }, format: :json
parsed_json = json_parse(response.body)
visible_ids_to_student_1 = parsed_json.pluck("id")
expect(visible_ids_to_student_1).to include(@topic_visible_to_everyone.id)
expect(visible_ids_to_student_1).not_to include(@topic.id)
end
end
end
end
describe "GET 'show'" do

View File

@ -905,4 +905,58 @@ describe Types::DiscussionType do
expect(discussion_type.resolve("delayedPostAt")).to eq discussion.delayed_post_at&.iso8601
end
end
context "differentiated modules" do
before do
Account.site_admin.enable_feature! :differentiated_modules
end
context "ungraded discussions" do
before do
course_factory(active_all: true)
@topic = discussion_topic_model(user: @teacher, context: @course)
@topic.update!(only_visible_to_overrides: true)
@course_section = @course.course_sections.create
@student1 = student_in_course(course: @course, active_enrollment: true).user
@student2 = student_in_course(course: @course, active_enrollment: true, section: @course_section).user
@teacher1 = teacher_in_course(course: @course, active_enrollment: true).user
@student1_type = GraphQLTypeTester.new(@topic, current_user: @student1)
@student2_type = GraphQLTypeTester.new(@topic, current_user: @student2)
@teacher1_type = GraphQLTypeTester.new(@topic, current_user: @teacher1)
end
it "is visible only to the assigned student" do
override = @topic.assignment_overrides.create!
override.assignment_override_students.create!(user: @student1)
expect(@student1_type.resolve("_id")).to be_truthy
expect(@student2_type.resolve("_id")).to be_nil
expect(@teacher1_type.resolve("_id")).to be_truthy
end
it "is visible only to users who can access the assigned section" do
@topic.assignment_overrides.create!(set: @course_section)
expect(@student1_type.resolve("_id")).to be_nil
expect(@student2_type.resolve("_id")).to be_truthy
expect(@teacher1_type.resolve("_id")).to be_truthy
end
it "is visible only to students in module override section" do
context_module = @course.context_modules.create!(name: "module")
context_module.content_tags.create!(content: @topic, context: @course)
override2 = @topic.assignment_overrides.create!(unlock_at: "2022-02-01T01:00:00Z",
unlock_at_overridden: true,
lock_at: "2022-02-02T01:00:00Z",
lock_at_overridden: true)
override2.assignment_override_students.create!(user: @student1)
expect(@student1_type.resolve("_id")).to be_truthy
expect(@student2_type.resolve("_id")).to be_nil
expect(@teacher1_type.resolve("_id")).to be_truthy
end
end
end
end

View File

@ -529,6 +529,61 @@ describe DiscussionTopic do
expect(@announcement.active_participants_include_tas_and_teachers.include?(user)).to be_falsey
end
end
context "differentiated modules" do
before do
Account.site_admin.enable_feature! :differentiated_modules
end
context "ungraded discussions" do
before do
@topic = discussion_topic_model(user: @teacher, context: @course)
@topic.update!(only_visible_to_overrides: true)
@course_section = @course.course_sections.create
@student1 = student_in_course(course: @course, active_enrollment: true).user
@student2 = student_in_course(course: @course, active_enrollment: true, section: @course_section).user
@teacher1 = teacher_in_course(course: @course, active_enrollment: true).user
@teacher2_limited_to_section = teacher_in_course(course: @course, active_enrollment: true).user
Enrollment.limit_privileges_to_course_section!(@course, @teacher2_limited_to_section, true)
end
it "is visible only to the assigned student" do
override = @topic.assignment_overrides.create!
override.assignment_override_students.create!(user: @student1)
expect(@topic.visible_for?(@student1)).to be_truthy
expect(@topic.visible_for?(@student2)).to be_falsey
expect(@topic.visible_for?(@teacher1)).to be_truthy
expect(@topic.visible_for?(@teacher2_limited_to_section)).to be_truthy
end
it "is visible only to users who can access the assigned section" do
@topic.assignment_overrides.create!(set: @course_section)
expect(@topic.visible_for?(@student1)).to be_falsey
expect(@topic.visible_for?(@student2)).to be_truthy
expect(@topic.visible_for?(@teacher1)).to be_truthy
expect(@topic.visible_for?(@teacher2_limited_to_section)).to be_falsey
end
it "is visible only to students in module override section" do
context_module = @course.context_modules.create!(name: "module")
context_module.content_tags.create!(content: @topic, context: @course)
override2 = @topic.assignment_overrides.create!(unlock_at: "2022-02-01T01:00:00Z",
unlock_at_overridden: true,
lock_at: "2022-02-02T01:00:00Z",
lock_at_overridden: true)
override2.assignment_override_students.create!(user: @student1)
expect(@topic.visible_for?(@student1)).to be_truthy
expect(@topic.visible_for?(@student2)).to be_falsey
expect(@topic.visible_for?(@teacher1)).to be_truthy
expect(@topic.visible_for?(@teacher2_limited_to_section)).to be_truthy
end
end
end
end
context "differentiated assignements" do
@ -1554,6 +1609,53 @@ describe DiscussionTopic do
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
context "differentiated modules" do
before do
Account.site_admin.enable_feature! :differentiated_modules
end
context "ungraded discussions" do
before do
@topic = discussion_topic_model(user: @teacher, context: @course)
@topic.update!(only_visible_to_overrides: true)
@course_section = @course.course_sections.create
@student1 = student_in_course(course: @course, active_enrollment: true).user
@student2 = student_in_course(course: @course, active_enrollment: true, section: @course_section).user
@teacher1 = teacher_in_course(course: @course, active_enrollment: true).user
@teacher2_limited_to_section = teacher_in_course(course: @course, active_enrollment: true).user
Enrollment.limit_privileges_to_course_section!(@course, @teacher2_limited_to_section, true)
end
it "is visible only to the assigned student" do
override = @topic.assignment_overrides.create!
override.assignment_override_students.create!(user: @student1)
expect(DiscussionTopic.visible_to_students_in_course_with_da([@student1.id], [@course.id])).to include(@topic)
expect(DiscussionTopic.visible_to_students_in_course_with_da([@student2.id], [@course.id])).not_to include(@topic)
end
it "is visible only to users who can access the assigned section" do
@topic.assignment_overrides.create!(set: @course_section)
expect(DiscussionTopic.visible_to_students_in_course_with_da([@student1.id], [@course.id])).not_to include(@topic)
expect(DiscussionTopic.visible_to_students_in_course_with_da([@student2.id], [@course.id])).to include(@topic)
end
it "is visible only to students in module override section" do
context_module = @course.context_modules.create!(name: "module")
context_module.content_tags.create!(content: @topic, context: @course)
override2 = @topic.assignment_overrides.create!(unlock_at: "2022-02-01T01:00:00Z",
unlock_at_overridden: true,
lock_at: "2022-02-02T01:00:00Z",
lock_at_overridden: true)
override2.assignment_override_students.create!(user: @student1)
expect(DiscussionTopic.visible_to_students_in_course_with_da([@student1.id], [@course.id])).to include(@topic)
expect(DiscussionTopic.visible_to_students_in_course_with_da([@student2.id], [@course.id])).not_to include(@topic)
end
end
end
end
context "posters" do