move SQL from UngradedDiscussionStudentVisibility view to rails

moving logic from a view (that has become too complex
to properly push down WHERE clause predicates) to a
service.  The SQL is existing view definition SQL,
from MigrationHelpers::StudentVisibilities::StudentVisibilitiesV7
with the improvements recommended by DBAs (don't use a view;
put the WHERE clause on each SELECT in the UNION or EXCEPT)

New tests call this service, but no application
code is calling it yet.

flag = differentiated_modules

refs LX-1727

Test Plan:
  - tests pass

Change-Id: Ia6f3fd3f6edc4f90f5d0686ce8f84e38c722d6fb
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/348646
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Jen Smith <jen.smith@instructure.com>
QA-Review: Jen Smith <jen.smith@instructure.com>
Product-Review: Jen Smith <jen.smith@instructure.com>
This commit is contained in:
Sarah Gerard 2024-05-29 16:25:59 -07:00
parent 7718f4ed6d
commit 08d9ceb884
7 changed files with 672 additions and 1 deletions

View File

@ -0,0 +1,58 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - 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/>.
module UngradedDiscussionVisibility
module Entities
# When a discussion topic is visible to a (student) user
class UngradedDiscussionVisibleToStudent
attr_reader :course_id,
:user_id,
:discussion_topic_id
def initialize(course_id:,
user_id:,
discussion_topic_id:)
raise ArgumentError, "course_id cannot be nil" if course_id.nil?
raise ArgumentError, "user_id cannot be nil" if user_id.nil?
raise ArgumentError, "discussion_topic_id cannot be nil" if discussion_topic_id.nil?
@course_id = course_id
@user_id = user_id
@discussion_topic_id = discussion_topic_id
end
# two UngradedDiscussionVisibleToStudent DTOs are equal if all of their attributes are equal
def ==(other)
return false unless other.is_a?(UngradedDiscussionVisibleToStudent)
course_id == other.course_id &&
user_id == other.user_id &&
discussion_topic_id == other.discussion_topic_id
end
def eql?(other)
self == other
end
def hash
[course_id, user_id, discussion_topic_id].hash
end
end
end
end

View File

@ -0,0 +1,230 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - 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/>.
module UngradedDiscussionVisibility
module Repositories
class UngradedDiscussionVisibleToStudentRepository
class << self
# NOTE: context module has a pretty different function for a few of the functions implemented here
# if only_visible_to_overrides is false, or there's related modules with no overrides, then everyone can see it
def find_discussion_topics_visible_to_everyone(course_id_params:, user_id_params:, discussion_topic_id_params:)
filter_condition_sql = filter_condition_sql(course_id_params:, user_id_params:, discussion_topic_id_params:)
query_sql = <<~SQL.squish
#{discussion_topic_select_sql}
/* join active student enrollments */
#{VisibilitySqlHelper.enrollment_join_sql}
/* join context modules */
#{VisibilitySqlHelper.module_items_join_sql(content_tag_type: "DiscussionTopic")}
/* join assignment override */
#{VisibilitySqlHelper.assignment_override_everyone_join_sql}
/* filtered to course_id, user_id, discussion_topic_id, and additional conditions */
#{VisibilitySqlHelper.assignment_override_everyone_filter_sql(filter_condition_sql:)}
SQL
query_params = query_params(course_id_params:, user_id_params:, discussion_topic_id_params:)
exec_find_discussion_topic_visibility_query(query_sql:, query_params:)
end
# section overrides and related module section overrides
def find_discussion_topics_visible_to_sections(course_id_params:, user_id_params:, discussion_topic_id_params:)
filter_condition_sql = filter_condition_sql(course_id_params:, user_id_params:, discussion_topic_id_params:)
query_sql = <<~SQL.squish
#{discussion_topic_select_sql}
/* join active student enrollments */
#{VisibilitySqlHelper.enrollment_join_sql}
/* join context modules */
#{VisibilitySqlHelper.module_items_join_sql(content_tag_type: "DiscussionTopic")}
/* join assignment overrides (assignment or related context module) for CourseSection */
#{VisibilitySqlHelper.assignment_override_section_join_sql(id_column_name: "discussion_topic_id")}
/* filtered to course_id, user_id, discussion_topic_id, and additional conditions */
#{VisibilitySqlHelper.section_override_filter_sql(filter_condition_sql:)}
SQL
query_params = query_params(course_id_params:, user_id_params:, discussion_topic_id_params:)
exec_find_discussion_topic_visibility_query(query_sql:, query_params:)
end
# students with unassigned section overrides
def find_discussion_topics_with_unassigned_section_overrides(course_id_params:, user_id_params:, discussion_topic_id_params:)
filter_condition_sql = filter_condition_sql(course_id_params:, user_id_params:, discussion_topic_id_params:)
query_sql = <<~SQL.squish
#{discussion_topic_select_sql}
/* join active student enrollments */
#{VisibilitySqlHelper.enrollment_join_sql}
/* join assignment override for 'CourseSection' (no module check) */
#{VisibilitySqlHelper.assignment_override_unassign_section_join_sql(id_column_name: "discussion_topic_id")}
/* filtered to course_id, user_id, discussion_topic_id, and additional conditions */
#{VisibilitySqlHelper.assignment_override_unassign_section_filter_sql(filter_condition_sql:)}
SQL
query_params = query_params(course_id_params:, user_id_params:, discussion_topic_id_params:)
exec_find_discussion_topic_visibility_query(query_sql:, query_params:)
end
# students with unassigned adhoc overrides
def find_discussion_topics_with_unassigned_adhoc_overrides(course_id_params:, user_id_params:, discussion_topic_id_params:)
filter_condition_sql = filter_condition_sql(course_id_params:, user_id_params:, discussion_topic_id_params:)
query_sql = <<~SQL.squish
#{discussion_topic_select_sql}
/* join active student enrollments */
#{VisibilitySqlHelper.enrollment_join_sql}
/* join assignment overrides for 'ADHOC' (no module check) */
#{VisibilitySqlHelper.assignment_override_unassign_adhoc_join_sql(id_column_name: "discussion_topic_id")}
/* filtered to course_id, user_id, discussion_topic_id, and additional conditions */
#{VisibilitySqlHelper.assignment_override_unassign_adhoc_filter_sql(filter_condition_sql:)}
SQL
query_params = query_params(course_id_params:, user_id_params:, discussion_topic_id_params:)
exec_find_discussion_topic_visibility_query(query_sql:, query_params:)
end
# ADHOC overrides and related module ADHOC overrides
def find_discussion_topics_visible_to_adhoc_overrides(course_id_params:, user_id_params:, discussion_topic_id_params:)
filter_condition_sql = filter_condition_sql(course_id_params:, user_id_params:, discussion_topic_id_params:)
query_sql = <<~SQL.squish
#{discussion_topic_select_sql}
/* join active student enrollments */
#{VisibilitySqlHelper.enrollment_join_sql}
/* join context modules */
#{VisibilitySqlHelper.module_items_join_sql(content_tag_type: "DiscussionTopic")}
/* join assignment override for 'ADHOC' */
#{VisibilitySqlHelper.assignment_override_adhoc_join_sql(id_column_name: "discussion_topic_id")}
/* join AssignmentOverrideStudent */
#{VisibilitySqlHelper.assignment_override_student_join_sql}
/* filtered to course_id, user_id, discussion_topic_id, and additional conditions */
#{VisibilitySqlHelper.adhoc_override_filter_sql(filter_condition_sql:)}
SQL
query_params = query_params(course_id_params:, user_id_params:, discussion_topic_id_params:)
exec_find_discussion_topic_visibility_query(query_sql:, query_params:)
end
# course overrides
def find_discussion_topics_visible_to_course_overrides(course_id_params:, user_id_params:, discussion_topic_id_params:)
filter_condition_sql = filter_condition_sql(course_id_params:, user_id_params:, discussion_topic_id_params:)
query_sql = <<~SQL.squish
#{discussion_topic_select_sql}
/* join active student enrollments */
#{VisibilitySqlHelper.enrollment_join_sql}
/* join assignment override for 'Course' */
#{VisibilitySqlHelper.assignment_override_course_join_sql(id_column_name: "discussion_topic_id")}
/* filtered to course_id, user_id, discussion_topic_id, and additional conditions */
#{VisibilitySqlHelper.course_override_filter_sql(filter_condition_sql:)}
SQL
query_params = query_params(course_id_params:, user_id_params:, discussion_topic_id_params:)
exec_find_discussion_topic_visibility_query(query_sql:, query_params:)
end
private
def exec_find_discussion_topic_visibility_query(query_sql:, query_params:)
# safely replace parameters in the filter clause
sanitized_sql = ActiveRecord::Base.sanitize_sql_array([query_sql, query_params])
# Execute the query
query_results = ActiveRecord::Base.connection.exec_query(sanitized_sql)
# map the results to an array of AssignmentVisibleToStudent (DTO / PORO) and return it
query_results.map do |row|
UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(course_id: row["course_id"], discussion_topic_id: row["discussion_topic_id"], user_id: row["user_id"])
end
end
def query_params(course_id_params:, user_id_params:, discussion_topic_id_params:)
query_params = {}
query_params[:course_id] = course_id_params unless course_id_params.nil?
query_params[:user_id] = user_id_params unless user_id_params.nil?
query_params[:discussion_topic_id] = discussion_topic_id_params unless discussion_topic_id_params.nil?
query_params
end
# Create a filter clause SQL from the params - something like: e.user_id IN ['1', '2'] AND course_id = '20'
# Note that at least one of the params must be non nil
def filter_condition_sql(course_id_params: nil, user_id_params: nil, discussion_topic_id_params: nil)
query_conditions = []
if discussion_topic_id_params
query_conditions << if discussion_topic_id_params.is_a?(Array)
"o.id IN (:discussion_topic_id)"
else
"o.id = :discussion_topic_id"
end
end
if user_id_params
query_conditions << if user_id_params.is_a?(Array)
"e.user_id IN (:user_id)"
else
"e.user_id = :user_id"
end
end
if course_id_params
query_conditions << if course_id_params.is_a?(Array)
"e.course_id IN (:course_id)"
else
"e.course_id = :course_id"
end
end
if query_conditions.empty?
raise ArgumentError, "UngradedDiscussionsVisibleToStudents must have a limiting where clause of at least one course_id, user_id, or discussion_topic_id (for performance reasons)"
end
query_conditions.join(" AND ")
end
def discussion_topic_select_sql
<<~SQL.squish
SELECT DISTINCT o.id as discussion_topic_id,
e.user_id as user_id,
e.course_id as course_id
FROM #{DiscussionTopic.quoted_table_name} o
SQL
end
end
end
end
end

View File

@ -0,0 +1,126 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - 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/>.
module UngradedDiscussionVisibility
class UngradedDiscussionVisibilityService
class << self
def discussion_topics_visible_to_student(course_id:, user_id:)
raise ArgumentError, "course_id cannot be nil" if course_id.nil?
raise ArgumentError, "course_id must not be an array" if course_id.is_a?(Array)
raise ArgumentError, "user_id cannot be nil" if user_id.nil?
raise ArgumentError, "user_id must not be an array" if user_id.is_a?(Array)
discussion_topics_visible_to_students(course_id_params: course_id, user_id_params: user_id)
end
def discussion_topics_visible_to_students_in_courses(course_ids:, user_ids:)
raise ArgumentError, "course_ids cannot be nil" if course_ids.nil?
raise ArgumentError, "course_ids must be an array" unless course_ids.is_a?(Array)
raise ArgumentError, "user_ids cannot be nil" if user_ids.nil?
raise ArgumentError, "user_ids must be an array" unless user_ids.is_a?(Array)
discussion_topics_visible_to_students(course_id_params: course_ids, user_id_params: user_ids)
end
def discussion_topic_visible_to_student(discussion_topic_id:, user_id:)
raise ArgumentError, "discussion_topic_id cannot be nil" if discussion_topic_id.nil?
raise ArgumentError, "discussion_topic_id must not be an array" if discussion_topic_id.is_a?(Array)
raise ArgumentError, "user_id cannot be nil" if user_id.nil?
raise ArgumentError, "user_id must not be an array" if user_id.is_a?(Array)
discussion_topics_visible_to_students(discussion_topic_id_params: discussion_topic_id, user_id_params: user_id)
end
def discussion_topic_visible_to_students(discussion_topic_id:, user_ids:)
raise ArgumentError, "discussion_topic_id cannot be nil" if discussion_topic_id.nil?
raise ArgumentError, "discussion_topic_id must not be an array" if discussion_topic_id.is_a?(Array)
raise ArgumentError, "user_ids cannot be nil" if user_ids.nil?
raise ArgumentError, "user_ids must be an array" unless user_ids.is_a?(Array)
discussion_topics_visible_to_students(discussion_topic_id_params: discussion_topic_id, user_id_params: user_ids)
end
def discussion_topic_visible_to_students_in_course(discussion_topic_id:, user_ids:, course_id:)
raise ArgumentError, "course_id cannot be nil" if course_id.nil?
raise ArgumentError, "course_id must not be an array" if course_id.is_a?(Array)
raise ArgumentError, "discussion_topic_id cannot be nil" if discussion_topic_id.nil?
raise ArgumentError, "discussion_topic_id must not be an array" if discussion_topic_id.is_a?(Array)
raise ArgumentError, "user_ids cannot be nil" if user_ids.nil?
raise ArgumentError, "user_ids must be an array" unless user_ids.is_a?(Array)
discussion_topics_visible_to_students(course_id_params: course_id, discussion_topic_id_params: discussion_topic_id, user_id_params: user_ids)
end
def discussion_topic_visible_in_course(discussion_topic_id:, course_id:)
raise ArgumentError, "course_id cannot be nil" if course_id.nil?
raise ArgumentError, "course_id must not be an array" if course_id.is_a?(Array)
raise ArgumentError, "discussion_topic_id cannot be nil" if discussion_topic_id.nil?
raise ArgumentError, "discussion_topic_id must not be an array" if discussion_topic_id.is_a?(Array)
discussion_topics_visible_to_students(course_id_params: course_id, discussion_topic_id_params: discussion_topic_id)
end
private
def discussion_topics_visible_to_students(course_id_params: nil, user_id_params: nil, discussion_topic_id_params: nil)
if course_id_params.nil? && user_id_params.nil? && discussion_topic_id_params.nil?
raise ArgumentError, "at least one non nil course_id, user_id, or discussion_topic_id_params is required (for query performance reasons)"
end
visible_discussion_topics = []
# add discussion topics visible to everyone
discussion_topics_visible_to_all = UngradedDiscussionVisibility::Repositories::UngradedDiscussionVisibleToStudentRepository
.find_discussion_topics_visible_to_everyone(course_id_params:, user_id_params:, discussion_topic_id_params:)
visible_discussion_topics |= discussion_topics_visible_to_all
# add discussion topics visible to sections (and related module section overrides)
discussion_topics_visible_to_sections = UngradedDiscussionVisibility::Repositories::UngradedDiscussionVisibleToStudentRepository
.find_discussion_topics_visible_to_sections(course_id_params:, user_id_params:, discussion_topic_id_params:)
visible_discussion_topics |= discussion_topics_visible_to_sections
# remove discussion topics for students with unassigned section overrides
discussion_topics_with_unassigned_section_overrides = UngradedDiscussionVisibility::Repositories::UngradedDiscussionVisibleToStudentRepository
.find_discussion_topics_with_unassigned_section_overrides(course_id_params:, user_id_params:, discussion_topic_id_params:)
visible_discussion_topics -= discussion_topics_with_unassigned_section_overrides
# add discussion topics visible due to ADHOC overrides (and related module ADHOC overrides)
discussion_topics_visible_to_adhoc_overrides = UngradedDiscussionVisibility::Repositories::UngradedDiscussionVisibleToStudentRepository
.find_discussion_topics_visible_to_adhoc_overrides(course_id_params:, user_id_params:, discussion_topic_id_params:)
visible_discussion_topics |= discussion_topics_visible_to_adhoc_overrides
# remove discussion topics for students with unassigned ADHOC overrides
discussion_topics_with_unassigned_adhoc_overrides = UngradedDiscussionVisibility::Repositories::UngradedDiscussionVisibleToStudentRepository
.find_discussion_topics_with_unassigned_adhoc_overrides(course_id_params:, user_id_params:, discussion_topic_id_params:)
visible_discussion_topics -= discussion_topics_with_unassigned_adhoc_overrides
# add discussion topics visible due to course overrides
discussion_topics_visible_to_course_overrides = UngradedDiscussionVisibility::Repositories::UngradedDiscussionVisibleToStudentRepository
.find_discussion_topics_visible_to_course_overrides(course_id_params:, user_id_params:, discussion_topic_id_params:)
visible_discussion_topics | discussion_topics_visible_to_course_overrides
end
def empty_id_hash(ids)
# [1,2,3] => {1:[],2:[],3:[]}
ids.zip(ids.map { [] }).to_h
end
end
end
end

View File

@ -21,7 +21,7 @@ module StudentVisibilityCommon
def ids_visible_to_user(user, learning_object_type)
case learning_object_type
when "discussion_topic"
UngradedDiscussionStudentVisibility.where(course_id: @course.id, user_id: user.id).pluck(:discussion_topic_id)
UngradedDiscussionVisibility::UngradedDiscussionVisibilityService.discussion_topics_visible_to_student(course_id: @course.id, user_id: user.id).map(&:discussion_topic_id)
when "wiki_page"
WikiPageVisibility::WikiPageVisibilityService.wiki_pages_visible_to_student(course_id: @course.id, user_id: user.id).map(&:wiki_page_id)
end

View File

@ -0,0 +1,126 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - 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 "../../../spec_helper"
describe UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent do
describe "testing DTO" do
it "can be initialized" do
discussion_topic_visible_to_student = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 7)
expect(discussion_topic_visible_to_student.discussion_topic_id).to eq 7
end
it "raises error if passed nil course_id" do
expect { UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: nil, discussion_topic_id: 7) }.to raise_error(ArgumentError, "course_id cannot be nil")
end
it "raises error if passed nil user_id" do
expect { UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: nil, course_id: 6, discussion_topic_id: 7) }.to raise_error(ArgumentError, "user_id cannot be nil")
end
it "raises error if passed nil discussion_topic_id" do
expect { UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: nil) }.to raise_error(ArgumentError, "discussion_topic_id cannot be nil")
end
it "equality is attribute based" do
discussion_topic_visible_to_student = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 7)
discussion_topic_visible_to_student_2 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 7)
expect(discussion_topic_visible_to_student).to eq discussion_topic_visible_to_student_2
# with different user_ids
discussion_topic_visible_to_student_3 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 4, course_id: 6, discussion_topic_id: 7)
discussion_topic_visible_to_student_4 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 7)
expect(discussion_topic_visible_to_student_3).not_to eq discussion_topic_visible_to_student_4
# with different discussion_topic_ids
discussion_topic_visible_to_student_5 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 3)
discussion_topic_visible_to_student_6 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 7)
expect(discussion_topic_visible_to_student_5).not_to eq discussion_topic_visible_to_student_6
# with different course_ids
discussion_topic_visible_to_student_7 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 8, discussion_topic_id: 7)
discussion_topic_visible_to_student_8 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 7)
expect(discussion_topic_visible_to_student_7).not_to eq discussion_topic_visible_to_student_8
end
it "hashcode is attribute based" do
discussion_topic_visible_to_student = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 7)
discussion_topic_visible_to_student_2 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 7)
expect(discussion_topic_visible_to_student.hash).to eq discussion_topic_visible_to_student_2.hash
# with different user_ids
discussion_topic_visible_to_student_3 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 4, course_id: 6, discussion_topic_id: 7)
discussion_topic_visible_to_student_4 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 7)
expect(discussion_topic_visible_to_student_3.hash).not_to eq discussion_topic_visible_to_student_4.hash
# with different discussion_topic_ids
discussion_topic_visible_to_student_5 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 3)
discussion_topic_visible_to_student_6 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 7)
expect(discussion_topic_visible_to_student_5.hash).not_to eq discussion_topic_visible_to_student_6.hash
# with different course_ids
discussion_topic_visible_to_student_7 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 8, discussion_topic_id: 7)
discussion_topic_visible_to_student_8 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 7)
expect(discussion_topic_visible_to_student_7.hash).not_to eq discussion_topic_visible_to_student_8.hash
end
it "can be unioned in array (set operation)" do
# 4 discussion_topic_visible_to_student, one is a duplicate of another
discussion_topic_visible_to_student = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 7)
discussion_topic_visible_to_student_duplicate = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 7)
discussion_topic_visible_to_student_2 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 3, course_id: 4, discussion_topic_id: 5)
discussion_topic_visible_to_student_3 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 8)
array_1 = [discussion_topic_visible_to_student, discussion_topic_visible_to_student_2]
array_2 = [discussion_topic_visible_to_student_duplicate, discussion_topic_visible_to_student_3]
# union the two arrays
union_array = array_1 | array_2
# the duplicate should be removed
expect(union_array.length).to eq 3
expect(union_array).to include(discussion_topic_visible_to_student)
expect(union_array).to include(discussion_topic_visible_to_student_2)
expect(union_array).to include(discussion_topic_visible_to_student_3)
end
it "can be removed from array (set operation)" do
# 4 discussion_topic_visible_to_student, one is a duplicate of another
discussion_topic_visible_to_student = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 7)
discussion_topic_visible_to_student_duplicate = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 7)
discussion_topic_visible_to_student_2 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 3, course_id: 4, discussion_topic_id: 5)
discussion_topic_visible_to_student_3 = UngradedDiscussionVisibility::Entities::UngradedDiscussionVisibleToStudent.new(user_id: 5, course_id: 6, discussion_topic_id: 8)
array_1 = [discussion_topic_visible_to_student, discussion_topic_visible_to_student_2, discussion_topic_visible_to_student_3]
array_2 = [discussion_topic_visible_to_student_duplicate]
# remove all elements in array_2 from array_1
array_with_removal = array_1 - array_2
expect(array_with_removal.length).to eq 2
expect(array_with_removal).not_to include(discussion_topic_visible_to_student)
expect(array_with_removal).not_to include(discussion_topic_visible_to_student_duplicate)
expect(array_with_removal).to include(discussion_topic_visible_to_student_2)
expect(array_with_removal).to include(discussion_topic_visible_to_student_3)
end
end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - 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 "../../../spec_helper"
# See discussion_topic_visibility_service_spec for more (integration) tests that exercise this repository
describe UngradedDiscussionVisibility::Repositories::UngradedDiscussionVisibleToStudentRepository do
describe "testing things" do
it "raises error if called with no filter parameters" do
expect do
UngradedDiscussionVisibility::Repositories::UngradedDiscussionVisibleToStudentRepository
.find_discussion_topics_visible_to_everyone(course_id_params: nil, user_id_params: nil, discussion_topic_id_params: nil)
end.to raise_error(ArgumentError, "UngradedDiscussionsVisibleToStudents must have a limiting where clause of at least one course_id, user_id, or discussion_topic_id (for performance reasons)")
end
end
end

View File

@ -0,0 +1,99 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - 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 "../../models/student_visibility/student_visibility_common"
describe "UngradedDiscussionVisibility" do
include StudentVisibilityCommon
def assignment_ids_visible_to_user(user)
AssignmentVisibility::AssignmentVisibilityService.assignments_visible_to_student(course_id: @course.id, user_id: user.id).map(&:assignment_id)
end
before :once do
course_factory(active_all: true)
@section1 = @course.default_section
@section2 = @course.course_sections.create!(name: "Section 2")
@student1 = student_in_course(active_all: true, section: @section1).user
@student2 = student_in_course(active_all: true, section: @section2).user
@discussion1 = DiscussionTopic.create!(context: @course, title: "Page 1")
@discussion2 = DiscussionTopic.create!(context: @course, title: "Page 2")
end
context "discussion topic visibility" do
let(:learning_object1) { @discussion1 }
let(:learning_object2) { @discussion2 }
let(:learning_object_type) { "discussion_topic" }
it_behaves_like "learning object visiblities with modules"
it_behaves_like "learning object visiblities"
it "does not include unpublished discussion topics" do
@discussion1.workflow_state = "unpublished"
@discussion1.save!
expect(ids_visible_to_user(@student1, "discussion_topic")).to contain_exactly(@discussion2.id)
end
end
context "graded discussion topic visibility" do
# graded discussion topics must use assignment_student_visibility as their
# assignment overrides are on the assignment, not the discussion topic
before :once do
@discussion1_assignment = @course.assignments.create!
@discussion2_assignment = @course.assignments.create!
@discussion1.update!(assignment: @discussion1_assignment)
@discussion2.update!(assignment: @discussion2_assignment)
end
it "ungraded_discussion_student_visibilities does not include graded discussion's assignment overrides" do
@discussion1_assignment.only_visible_to_overrides = true
@discussion1_assignment.save!
@discussion1.assignment.assignment_overrides.create!(set_type: "CourseSection", set_id: @section2.id)
# The overrides are on the assignment, not the discussion topic, so discussion visibilities will not be affected
expect(ids_visible_to_user(@student1, "discussion_topic")).to contain_exactly(@discussion1.id, @discussion2.id)
end
it "assignment_student_visibilities shows correct visibilities for graded discussion topic's assignment" do
@discussion1_assignment.only_visible_to_overrides = true
@discussion1_assignment.save!
@discussion1.assignment.assignment_overrides.create!(set_type: "CourseSection", set_id: @section2.id)
expect(assignment_ids_visible_to_user(@student1)).to contain_exactly(@discussion2.assignment.id)
expect(assignment_ids_visible_to_user(@student2)).to contain_exactly(@discussion1.assignment.id, @discussion2.assignment.id)
end
it "gets module overrides from graded discussion topic's assignment" do
@module1 = @course.context_modules.create!(name: "Module 1")
@module2 = @course.context_modules.create!(name: "Module 2")
@discussion1.context_module_tags.create! context_module: @module1, context: @course, tag_type: "context_module"
override = @module1.assignment_overrides.create!
override.assignment_override_students.create!(user: @student1)
expect(ids_visible_to_user(@student1, "discussion_topic")).to contain_exactly(@discussion1.id, @discussion2.id)
expect(ids_visible_to_user(@student2, "discussion_topic")).to contain_exactly(@discussion2.id)
expect(assignment_ids_visible_to_user(@student1)).to contain_exactly(@discussion1.assignment.id, @discussion2.assignment.id)
expect(assignment_ids_visible_to_user(@student2)).to contain_exactly(@discussion2.assignment.id)
end
end
end