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 && (
+
+
+
+
+ {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: {