Add assignTo button to discussions index

closes LF-1383
flag=differentiated_modules

Test Plan
1. Open discussions index page
2. Open various discussions using new assign to menu option
3. Verify that discussion are displayed correctly
4. Verify that assignments are made correctly

Change-Id: Ib1d0b58c65d34a9643b726b72e651db4f04c8d3f
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/345869
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Sarah Gerard <sarah.gerard@instructure.com>
QA-Review: Sarah Gerard <sarah.gerard@instructure.com>
Product-Review: Jason Gillett <jason.gillett@instructure.com>
This commit is contained in:
Jason Gillett 2024-04-22 09:26:20 -06:00
parent 2fec65b431
commit ae0554b895
7 changed files with 200 additions and 43 deletions

View File

@ -30,29 +30,6 @@ describe "discussions" do
include ItemsAssignToTray
include ContextModulesCommon
def generate_expected_overrides(assignment)
expected_overrides = []
if assignment.assignment_overrides.active.empty?
expected_overrides << ["Everyone"]
else
unless assignment.only_visible_to_overrides
expected_overrides << ["Everyone else"]
end
assignment.assignment_overrides.active.each do |override|
if override.set_type == "CourseSection"
expected_overrides << [override.title]
elsif override.set_type == "ADHOC"
student_names = override.assignment_override_students.map { |student| student.user.name }
expected_overrides << student_names
end
end
end
expected_overrides
end
let(:course) { course_model.tap(&:offer!) }
let(:teacher) { teacher_in_course(course:, name: "teacher", active_all: true).user }
let(:teacher_topic) { course.discussion_topics.create!(user: teacher, title: "teacher topic title", message: "teacher topic message") }
@ -657,26 +634,6 @@ describe "discussions" do
end
context "graded" do
def create_graded_discussion(discussion_course, assignment_options = {})
default_assignment_options = {
name: "Default Assignment",
points_possible: 10,
assignment_group: discussion_course.assignment_groups.create!(name: "Default Assignment Group"),
only_visible_to_overrides: false
}
options = default_assignment_options.merge(assignment_options)
discussion_assignment = discussion_course.assignments.create!(options)
all_graded_discussion_options = {
user: teacher,
title: "assignment topic title",
message: "assignment topic message",
discussion_type: "threaded",
assignment: discussion_assignment,
}
discussion_course.discussion_topics.create!(all_graded_discussion_options)
end
it "displays graded assignment options correctly when initially opening edit page" do
grading_standard = course.grading_standards.create!(title: "Win/Lose", data: [["Winner", 0.94], ["Loser", 0]])

View File

@ -18,9 +18,15 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
require_relative "pages/discussions_index_page"
require_relative "../helpers/discussions_common"
require_relative "../helpers/context_modules_common"
require_relative "../helpers/items_assign_to_tray"
describe "discussions index" do
include_context "in-process server selenium tests"
include ContextModulesCommon
include ItemsAssignToTray
include DiscussionsCommon
context "as a teacher" do
discussion1_title = "Meaning of life"
@ -225,5 +231,88 @@ describe "discussions index" do
@course.reload
expect(@course.allow_student_discussion_topics).to be false
end
context "differentiated modules assignToTray" do
# Since the itemAssignTo Tray contains all of the logic for setting
# assignment values. We only need to test that the correct overrides are
# Displayed from the index page
before do
differentiated_modules_on
@student1 = student_in_course(course: @course, active_all: true).user
@student2 = student_in_course(course: @course, active_all: true).user
@course_section = @course.course_sections.create!(name: "section alpha")
@course_section_2 = @course.course_sections.create!(name: "section Beta")
end
it "displays module_override correctly" do
graded_discussion = create_graded_discussion(@course)
module1 = @course.context_modules.create!(name: "Module 1")
graded_discussion.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)
login_and_visit_course(@teacher, @course)
DiscussionsIndex.click_assign_to_menu_option(graded_discussion.title)
expect(module_item_assign_to_card.count).to eq 1
expect(module_item_assign_to_card[0].find_all(assignee_selected_option_selector).map(&:text)).to eq ["User"]
expect(inherited_from.last.text).to eq("Inherited from #{module1.name}")
end
it "displays ungraded availability correctly" do
available_from_date = "Tue, 23 Apr 2024 18:28:42.003452000 UTC +00:00"
@ungraded_discussion_with_dates = @course.discussion_topics.create!(
title: "ungraded overrides",
message: "Could it be 43?",
user: @teacher,
delayed_post_at: available_from_date
)
login_and_visit_course(@teacher, @course)
DiscussionsIndex.click_assign_to_menu_option(@ungraded_discussion_with_dates.title)
expect(item_tray_exists?).to be_truthy
expect(module_item_assign_to_card.count).to eq 1
expect(selected_assignee_options.first.find("span").text).to eq "Everyone"
expect(assign_to_due_date(0).attribute("value")).to eq("Apr 23, 2024")
expect(assign_to_due_time(0).attribute("value")).to eq("6:28 PM")
expect(assign_to_available_from_date(0).attribute("value")).to eq("")
expect(assign_to_available_from_time(0).attribute("value")).to eq("")
end
it "displays graded discussion overrides correctly" do
graded_discussion = create_graded_discussion(@course)
# Create overrides
# Card 1 = ["Everyone else"], Set by: only_visible_to_overrides: false
# Card 2
graded_discussion.assignment.assignment_overrides.create!(set_type: "CourseSection", set_id: @course_section.id)
# Card 3
graded_discussion.assignment.assignment_overrides.create!(set_type: "CourseSection", set_id: @course_section_2.id)
# Card 4
graded_discussion.assignment.assignment_overrides.create!(set_type: "ADHOC")
graded_discussion.assignment.assignment_overrides.last.assignment_override_students.create!(user: @student1)
graded_discussion.assignment.assignment_overrides.last.assignment_override_students.create!(user: @student2)
login_and_visit_course(@teacher, @course)
DiscussionsIndex.click_assign_to_menu_option(graded_discussion.title)
# Check that displayed cards and overrides are correct
expect(module_item_assign_to_card.count).to eq 4
displayed_overrides = module_item_assign_to_card.map do |card|
card.find_all(assignee_selected_option_selector).map(&:text)
end
expected_overrides = generate_expected_overrides(graded_discussion.assignment)
expect(displayed_overrides).to match_array(expected_overrides)
end
end
end
end

View File

@ -158,6 +158,10 @@ class DiscussionsIndex
f("#duplicate-discussion-menu-option")
end
def assign_to_menu_option
f("#assignTo-discussion-menu-option")
end
def create_discussions_checkbox
fj("label:contains('Create discussion')")
end
@ -214,6 +218,11 @@ class DiscussionsIndex
duplicate_menu_option.click
end
def click_assign_to_menu_option(title)
discussion_menu(title).click
assign_to_menu_option.click
end
def click_confirm_delete
confirm_delete_button.click
end
@ -231,6 +240,10 @@ class DiscussionsIndex
create_discussions_checkbox.click
end
def click_assign_to_discussions_setting
create_discussions_checkbox.click
end
def submit_discussion_settings
discussion_settings_submit_button.click
end

View File

@ -38,6 +38,48 @@ module DiscussionsCommon
@course.discussion_topics.create!(title: discussion_name, discussion_type:)
end
def create_graded_discussion(discussion_course, assignment_options = {})
default_assignment_options = {
name: "Default Assignment",
points_possible: 10,
assignment_group: discussion_course.assignment_groups.create!(name: "Default Assignment Group"),
only_visible_to_overrides: false
}
options = default_assignment_options.merge(assignment_options)
discussion_assignment = discussion_course.assignments.create!(options)
all_graded_discussion_options = {
title: "assignment topic title",
message: "assignment topic message",
discussion_type: "threaded",
assignment: discussion_assignment,
}
discussion_course.discussion_topics.create!(all_graded_discussion_options)
end
def generate_expected_overrides(assignment)
expected_overrides = []
if assignment.assignment_overrides.active.empty?
expected_overrides << ["Everyone"]
else
unless assignment.only_visible_to_overrides
expected_overrides << ["Everyone else"]
end
assignment.assignment_overrides.active.each do |override|
if override.set_type == "CourseSection"
expected_overrides << [override.title]
elsif override.set_type == "ADHOC"
student_names = override.assignment_override_students.map { |student| student.user.name }
expected_overrides << student_names
end
end
end
expected_overrides
end
def edit_topic(discussion_name, message)
wait_for_tiny(f("textarea[name=message]"))
replace_content(f("input[name=title]"), discussion_name)

View File

@ -79,6 +79,7 @@ export class DiscussionsContainer extends Component {
// this really is used
handleDrop: func, // eslint-disable-line react/no-unused-prop-types
onMoveDiscussion: func,
onOpenAssignToTray: func,
permissions: propTypes.permissions.isRequired,
pinned: bool,
renderContainerBackground: func.isRequired,
@ -92,6 +93,7 @@ export class DiscussionsContainer extends Component {
},
handleDrop: undefined,
onMoveDiscussion: null,
onOpenAssignToTray: null,
pinned: undefined,
}
@ -178,6 +180,7 @@ export class DiscussionsContainer extends Component {
deleteDiscussion={this.props.deleteDiscussion}
getDiscussionPosition={this.getDiscussionPosition}
onMoveDiscussion={this.props.onMoveDiscussion}
onOpenAssignToTray={this.props.onOpenAssignToTray}
moveCard={this.moveCard}
draggable={true}
/>
@ -187,6 +190,7 @@ export class DiscussionsContainer extends Component {
discussion={discussion}
deleteDiscussion={this.props.deleteDiscussion}
onMoveDiscussion={this.props.onMoveDiscussion}
onOpenAssignToTray={this.props.onOpenAssignToTray}
draggable={false}
/>
)

View File

@ -51,6 +51,7 @@ import {
IconUnpublishedLine,
IconUpdownLine,
IconUserLine,
IconPermissionsLine,
} from '@instructure/ui-icons'
import {Link} from '@instructure/ui-link'
import {ScreenReaderContent} from '@instructure/ui-a11y-content'
@ -131,6 +132,8 @@ class DiscussionRow extends Component {
masteryPathsPillLabel: string, // required if displayMasteryPathsPill is true
displayManageMenu: bool.isRequired,
displayPinMenuItem: bool.isRequired,
displayDifferentiatedModulesTray: bool.isRequired,
onOpenAssignToTray: func,
draggable: bool,
duplicateDiscussion: func.isRequired,
isDragging: bool,
@ -159,6 +162,7 @@ class DiscussionRow extends Component {
displayMasteryPathsPill: false,
masteryPathsPillLabel: '',
onMoveDiscussion: null,
onOpenAssignToTray: null,
}
componentDidMount = () => {
@ -239,6 +243,9 @@ class DiscussionRow extends Component {
case 'ltiMenuTool':
window.location = `${menuTool.base_url}&discussion_topics[]=${id}`
break
case 'assignTo':
this.props.onOpenAssignToTray(this.props.discussion)
break
default:
throw new Error('Unknown manage discussion action encountered')
}
@ -514,6 +521,19 @@ class DiscussionRow extends Component {
)
}
if (this.props.displayDifferentiatedModulesTray) {
menuList.push(
this.createMenuItem(
'assignTo',
<span aria-hidden="true">
<IconPermissionsLine />
&nbsp;&nbsp;{I18n.t('Assign To...')}
</span>,
I18n.t('Set Assign to for %{title}', {title: discussionTitle})
)
)
}
if (this.props.displayPinMenuItem) {
const screenReaderContent = this.props.discussion.pinned
? I18n.t('Unpin discussion %{title}', {title: discussionTitle})
@ -934,6 +954,8 @@ const mapState = (state, ownProps) => {
discussion.permissions.delete ||
(state.DIRECT_SHARE_ENABLED && state.permissions.read_as_admin),
displayPinMenuItem: state.permissions.moderate,
displayDifferentiatedModulesTray:
ENV?.FEATURES?.differentiated_modules && discussion.permissions.update,
masterCourseData: state.masterCourseData,
isMasterCourse: masterCourse,
DIRECT_SHARE_ENABLED: state.DIRECT_SHARE_ENABLED,

View File

@ -29,6 +29,7 @@ import {ScreenReaderContent} from '@instructure/ui-a11y-content'
import {Text} from '@instructure/ui-text'
import {Heading} from '@instructure/ui-heading'
import {Spinner} from '@instructure/ui-spinner'
import ItemAssignToTray from '@canvas/context-modules/differentiated-modules/react/Item/ItemAssignToTray'
import DirectShareCourseTray from '@canvas/direct-sharing/react/components/DirectShareCourseTray'
import DirectShareUserModal from '@canvas/direct-sharing/react/components/DirectShareUserModal'
@ -85,6 +86,8 @@ export default class DiscussionsIndex extends Component {
state = {
showDelete: false,
deleteFunction: () => {},
showAssignToTray: false,
discussionDetails: {},
}
componentDidMount() {
@ -104,6 +107,14 @@ export default class DiscussionsIndex extends Component {
this.setState({showDelete: false, deleteFunction: () => {}})
}
openAssignToTray = discussion => {
this.setState({showAssignToTray: true, discussionDetails: discussion})
}
closeAssignToTray = () => {
this.setState({showAssignToTray: false, discussionDetails: null})
}
selectPage(page) {
return () => this.props.getDiscussions({page, select: true})
}
@ -212,6 +223,7 @@ export default class DiscussionsIndex extends Component {
discussions={this.props.pinnedDiscussions}
deleteDiscussion={this.openDeleteDiscussionsModal}
onMoveDiscussion={this.renderMoveDiscussionTray}
onOpenAssignToTray={this.openAssignToTray}
pinned={true}
renderContainerBackground={() =>
pinnedDiscussionBackground({
@ -225,6 +237,7 @@ export default class DiscussionsIndex extends Component {
title={I18n.t('Discussions')}
discussions={this.props.unpinnedDiscussions}
deleteDiscussion={this.openDeleteDiscussionsModal}
onOpenAssignToTray={this.openAssignToTray}
closedState={false}
renderContainerBackground={() =>
unpinnedDiscussionsBackground({
@ -240,6 +253,7 @@ export default class DiscussionsIndex extends Component {
title={I18n.t('Closed for Comments')}
discussions={this.props.closedForCommentsDiscussions}
deleteDiscussion={this.openDeleteDiscussionsModal}
onOpenAssignToTray={this.openAssignToTray}
closedState={true}
renderContainerBackground={() =>
closedDiscussionBackground({
@ -271,6 +285,22 @@ export default class DiscussionsIndex extends Component {
onDismiss={() => this.props.setSendToOpen(false)}
/>
)}{' '}
{ENV?.FEATURES?.differentiated_modules && this.state.showAssignToTray && (
<ItemAssignToTray
open={this.state.showAssignToTray}
onClose={this.closeAssignToTray}
onDismiss={this.closeAssignToTray}
courseId={ENV.COURSE_ID}
itemName={this.state.discussionDetails.title}
itemType="discussion"
iconType="discussion"
pointsPossible={this.state?.discussionDetails?.assignment?.points_possible || null}
itemContentId={this.state.discussionDetails.id}
locale={ENV.LOCALE || 'en'}
timezone={ENV.TIMEZONE || 'UTC'}
removeDueDateInput={!this.state?.discussionDetails?.assignment_id}
/>
)}
</View>
)
}