diff --git a/lib/api/v1/discussion_topics.rb b/lib/api/v1/discussion_topics.rb index cfda9f13792..316d732601b 100644 --- a/lib/api/v1/discussion_topics.rb +++ b/lib/api/v1/discussion_topics.rb @@ -163,7 +163,8 @@ module Api::V1::DiscussionTopics override_dates: opts[:override_dates], include_all_dates: opts[:include_all_dates], exclude_response_fields: excludes, - include_overrides: opts[:include_overrides] }.merge(opts[:assignment_opts])) + include_overrides: opts[:include_overrides], + include_checkpoints: true }.merge(opts[:assignment_opts])) end # ignore :include_sections_user_count for non-course contexts like groups diff --git a/spec/selenium/discussions/discussions_index_page_student_v2_spec.rb b/spec/selenium/discussions/discussions_index_page_student_v2_spec.rb index b0627935b77..78c89cb43a7 100644 --- a/spec/selenium/discussions/discussions_index_page_student_v2_spec.rb +++ b/spec/selenium/discussions/discussions_index_page_student_v2_spec.rb @@ -18,24 +18,33 @@ # with this program. If not, see . require_relative "pages/discussions_index_page" +require_relative "../helpers/items_assign_to_tray" +require_relative "pages/discussion_page" +require_relative "../common" describe "discussions index" do include_context "in-process server selenium tests" + def setup_course_and_students + @teacher = user_with_pseudonym(active_user: true) + @student = user_with_pseudonym(active_user: true) + @account = Account.create(name: "New Account", default_time_zone: "UTC") + @course = course_factory(course_name: "Aaron 101", + account: @account, + active_course: true) + course_with_teacher(user: @teacher, active_course: true, active_enrollment: true) + course_with_student(course: @course, active_enrollment: true) + @student2 = user_factory(name: "second user", short_name: "second") + user_with_pseudonym(user: @student2, active_user: true) + @course.enroll_student(@student2, enrollment_state: "active") + end + context "as a student" do discussion1_title = "Meaning of life" discussion2_title = "Meaning of the universe" before :once do - @teacher = user_with_pseudonym(active_user: true) - @student = user_with_pseudonym(active_user: true) - @account = Account.create(name: "New Account", default_time_zone: "UTC") - @course = course_factory(course_name: "Aaron 101", - account: @account, - active_course: true) - course_with_teacher(user: @teacher, active_course: true, active_enrollment: true) - course_with_student(course: @course, active_enrollment: true) - + setup_course_and_students # Discussion attributes: title, message, delayed_post_at, user @discussion1 = @course.discussion_topics.create!( title: discussion1_title, @@ -80,4 +89,57 @@ describe "discussions index" do expect_new_page_load { DiscussionsIndex.click_add_discussion } end end + + context "discussion checkpoints" do + include ItemsAssignToTray + + before :once do + Account.default.enable_feature! :discussion_create + Account.default.enable_feature! :discussion_checkpoints + Account.default.enable_feature! :react_discussions_post + setup_course_and_students + end + + it "show checkpoint info on the index page", :ignore_js_errors do + user_session(@teacher) + Discussion.start_new_discussion(@course.id) + wait_for_ajaximations + Discussion.topic_title_input.send_keys("Test Checkpoint") + Discussion.update_discussion_message + Discussion.click_graded_checkbox + Discussion.click_checkpoints_checkbox + Discussion.reply_to_topic_points_possible_input.send_keys("10") + Discussion.reply_to_entry_required_count_input.send_keys("1") + Discussion.points_possible_reply_to_entry_input.send_keys("10") + Discussion.click_assign_to_button + next_week = 1.week.from_now + half_month = 2.weeks.from_now + update_reply_to_topic_date(0, format_date_for_view(next_week)) + update_reply_to_topic_time(0, "11:59 PM") + update_required_replies_date(0, format_date_for_view(next_week)) + update_required_replies_time(0, "11:59 PM") + click_add_assign_to_card + select_module_item_assignee(1, @student.name) + update_reply_to_topic_date(1, format_date_for_view(half_month)) + update_reply_to_topic_time(1, "11:59 PM") + update_required_replies_date(1, format_date_for_view(half_month)) + update_required_replies_time(1, "11:59 PM") + click_save_button("Apply") + Discussion.save_and_publish_button.click + # student within everyone + user_session(@student2) + get "/courses/#{@course.id}/discussion_topics" + expect(fj("span:contains('Reply to topic: #{format_date_for_view(next_week)}')")).to be_present + expect(fj("span:contains('Required replies (1): #{format_date_for_view(next_week)}')")).to be_present + expect("body").not_to contain_jqcss("span:contains('Reply to topic: #{format_date_for_view(half_month)}')") + expect("body").not_to contain_jqcss("span:contains('Required replies (1): #{format_date_for_view(half_month)}')") + # student in the second assign card + user_session(@student) + get "/courses/#{@course.id}/discussion_topics" + expect(fj("span:contains('Reply to topic: #{format_date_for_view(half_month)}')")).to be_present + expect(fj("span:contains('Required replies (1): #{format_date_for_view(half_month)}')")).to be_present + expect("body").not_to contain_jqcss("span:contains('Reply to topic: #{format_date_for_view(next_week)}')") + expect("body").not_to contain_jqcss("span:contains('Required replies (1): #{format_date_for_view(next_week)}')") + end + end end diff --git a/spec/selenium/discussions/pages/discussion_page.rb b/spec/selenium/discussions/pages/discussion_page.rb index c3b3a7453be..9b4ad1fce16 100644 --- a/spec/selenium/discussions/pages/discussion_page.rb +++ b/spec/selenium/discussions/pages/discussion_page.rb @@ -39,6 +39,10 @@ class Discussion "input[type=checkbox][value='graded']" end + def checkpoints_checkbox_selector + "input[data-testid='checkpoints-checkbox']" + end + def topic_input_selector "input[placeholder='Topic Title']" end @@ -51,6 +55,18 @@ class Discussion "input[data-testid='points-possible-input']" end + def reply_to_topic_points_possible_input_selector + "input[data-testid='points-possible-input-reply-to-topic']" + end + + def reply_to_entry_required_count_input_selector + "input[data-testid='reply-to-entry-required-count']" + end + + def points_possible_reply_to_entry_input_selector + "input[data-testid='points-possible-input-reply-to-entry']" + end + def save_and_publish_button_selector "button[data-testid='save-and-publish-button']" end @@ -133,6 +149,10 @@ class Discussion f(grade_checkbox_selector) end + def checkpoints_checkbox + f(checkpoints_checkbox_selector) + end + def post_reply_button fj('button:contains("Post Reply")') end @@ -215,6 +235,18 @@ class Discussion f(points_possible_input_selector) end + def reply_to_topic_points_possible_input + f(reply_to_topic_points_possible_input_selector) + end + + def reply_to_entry_required_count_input + f(reply_to_entry_required_count_input_selector) + end + + def points_possible_reply_to_entry_input + f(points_possible_reply_to_entry_input_selector) + end + def save_and_publish_button f(save_and_publish_button_selector) end @@ -253,6 +285,10 @@ class Discussion force_click_native(grade_checkbox_selector) end + def click_checkpoints_checkbox + force_click_native(checkpoints_checkbox_selector) + end + def click_summarize_button summarize_button.click end diff --git a/ui/features/discussion_topics_index/react/components/DiscussionRow.jsx b/ui/features/discussion_topics_index/react/components/DiscussionRow.jsx index c28d27d30a3..88a20a500a3 100644 --- a/ui/features/discussion_topics_index/react/components/DiscussionRow.jsx +++ b/ui/features/discussion_topics_index/react/components/DiscussionRow.jsx @@ -111,7 +111,8 @@ const dropTarget = { props.moveCard(dragIndex, hoverIndex) }, } - +const REPLY_TO_TOPIC = 'reply_to_topic' +const REPLY_TO_ENTRY = 'reply_to_entry' class DiscussionRow extends Component { static propTypes = { canPublish: bool.isRequired, @@ -795,6 +796,39 @@ class DiscussionRow extends Component { ) } + renderCheckpointInfo = (size, timestampStyleOverride) => { + const {assignment} = this.props.discussion + let dueDateString = null + + if (assignment && assignment?.checkpoints?.length > 0) { + const replyToTopic = assignment.checkpoints.find(e => e.tag === REPLY_TO_TOPIC).due_at + const replyToEntry = assignment.checkpoints.find(e => e.tag === REPLY_TO_ENTRY).due_at + const noDate = I18n.t('No Due Date') + + dueDateString = I18n.t( + ' Reply to topic: %{topicDate} Required replies (%{count}): %{entryDate}', + { + topicDate: replyToTopic ? this.props.dateFormatter(replyToTopic) : noDate, + entryDate: replyToEntry ? this.props.dateFormatter(replyToEntry) : noDate, + count: this.props.discussion.reply_to_entry_required_count, + } + ) + } + return ( + dueDateString && ( + + + + + + ) + ) + } + renderIcon = () => { const accessibleGradedIcon = (isSuccessColor = true) => ( + {this.renderCheckpointInfo(timestampTextSize, timestampStyleOverride)} diff --git a/ui/features/discussion_topics_index/react/components/__tests__/DiscussionRow.test.jsx b/ui/features/discussion_topics_index/react/components/__tests__/DiscussionRow.test.jsx index 5b6b545924e..852fa8987a6 100644 --- a/ui/features/discussion_topics_index/react/components/__tests__/DiscussionRow.test.jsx +++ b/ui/features/discussion_topics_index/react/components/__tests__/DiscussionRow.test.jsx @@ -319,6 +319,66 @@ describe('DiscussionRow', () => { expect(screen.queryByText('To do', {exact: false})).not.toBeInTheDocument() }) + it('renders checkpoint information', () => { + const props = makeProps({ + discussion: { + reply_to_entry_required_count: 2, + assignment: { + checkpoints: [ + { + tag: 'reply_to_topic', + points_possible: 20, + due_at: '2024-09-14T05:59:00Z', + }, + { + tag: 'reply_to_entry', + points_possible: 10, + due_at: '2024-09-21T05:59:00Z', + }, + ], + }, + }, + }) + render() + expect(screen.queryByText('Reply to topic:', {exact: false})).toBeInTheDocument() + expect(screen.queryByText('Required replies (2):', {exact: false})).toBeInTheDocument() + expect( + screen.queryByText(props.dateFormatter('2024-09-14T05:59:00Z'), {exact: false}) + ).toBeInTheDocument() + expect( + screen.queryByText(props.dateFormatter('2024-09-21T05:59:00Z'), {exact: false}) + ).toBeInTheDocument() + expect(screen.queryByText('No Due Date', {exact: false})).not.toBeInTheDocument() + }) + + it('renders checkpoint information without due dates', () => { + const props = makeProps({ + discussion: { + reply_to_entry_required_count: 4, + assignment: { + checkpoints: [ + { + tag: 'reply_to_topic', + points_possible: 10, + due_at: null, + }, + { + tag: 'reply_to_entry', + points_possible: 20, + due_at: null, + }, + ], + }, + }, + }) + render() + expect( + screen.queryByText('Reply to topic: No Due Date Required replies (4): No Due Date', { + exact: false, + }) + ).toBeInTheDocument() + }) + it('renders to do date if ungraded with a to do date', () => { const props = makeProps({ discussion: {