Display checkpointed discussion in assign to tray

closes VICE-4299
flag=discussion_checkpoint

Note: Updating checkpointed dates from the tray
will not work until VICE-4300

Test Plan
1. Create a checkpointed discussion with
various states of override due dates and availability
2. Compare the discussion edit page with the assignTo tray
cards
3. Should display all override due dates corectly
3a. assign to tray locations
discussion show, discussion index, assignment index

Change-Id: Ia7125e027370ee7e96cfd8482e5592961316e59a
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/357825
Reviewed-by: Omar Soto-Fortuño <omar.soto@instructure.com>
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Juan Chavez <juan.chavez@instructure.com>
QA-Review: Juan Chavez <juan.chavez@instructure.com>
Product-Review: Jason Gillett <jason.gillett@instructure.com>
This commit is contained in:
Jason Gillett 2024-09-18 13:42:57 -06:00
parent c29d7b6bf8
commit d4cbf3eca0
8 changed files with 221 additions and 10 deletions

View File

@ -116,7 +116,8 @@ class LearningObjectDatesController < ApplicationController
Api.paginate(section_visibilities, self, route) Api.paginate(section_visibilities, self, route)
end end
all_overrides = assignment_overrides_json(overrides, @current_user, include_names: true) include_child_override_due_dates = Account.site_admin.feature_enabled?(:discussion_checkpoints)
all_overrides = assignment_overrides_json(overrides, @current_user, include_names: true, include_child_override_due_dates:)
all_overrides += section_visibility_to_override_json(section_visibilities, overridable) if visibilities_to_override all_overrides += section_visibility_to_override_json(section_visibilities, overridable) if visibilities_to_override
render json: { render json: {

View File

@ -557,4 +557,13 @@ class AssignmentOverride < ActiveRecord::Base
def set_root_account_id def set_root_account_id
write_attribute(:root_account_id, root_account_id) unless read_attribute(:root_account_id) write_attribute(:root_account_id, root_account_id) unless read_attribute(:root_account_id)
end end
def sub_assignment_due_dates
child_overrides.active.preload(:assignment).map do |child|
{
sub_assignment_tag: child.assignment&.sub_assignment_tag,
due_at: child.due_at
}
end
end
end end

View File

@ -23,7 +23,7 @@ module Api::V1::AssignmentOverride
OVERRIDABLE_ID_FIELDS = %i[assignment_id quiz_id context_module_id discussion_topic_id wiki_page_id attachment_id].freeze OVERRIDABLE_ID_FIELDS = %i[assignment_id quiz_id context_module_id discussion_topic_id wiki_page_id attachment_id].freeze
def assignment_override_json(override, visible_users = nil, student_names: nil, module_names: nil) def assignment_override_json(override, visible_users = nil, student_names: nil, module_names: nil, include_child_override_due_dates: nil)
fields = %i[id title unassign_item] fields = %i[id title unassign_item]
OVERRIDABLE_ID_FIELDS.each { |f| fields << f if override.send(f).present? } OVERRIDABLE_ID_FIELDS.each { |f| fields << f if override.send(f).present? }
fields.push(:due_at, :all_day, :all_day_date) if override.due_at_overridden fields.push(:due_at, :all_day, :all_day_date) if override.due_at_overridden
@ -52,6 +52,9 @@ module Api::V1::AssignmentOverride
when "Noop" when "Noop"
json[:noop_id] = override.set_id json[:noop_id] = override.set_id
end end
if include_child_override_due_dates
json[:sub_assignment_due_dates] = override.sub_assignment_due_dates
end
end end
end end
@ -70,7 +73,7 @@ module Api::V1::AssignmentOverride
end end
end end
def assignment_overrides_json(overrides, user = nil, include_names: false) def assignment_overrides_json(overrides, user = nil, include_names: false, include_child_override_due_dates: false)
visible_users_ids = ::AssignmentOverride.visible_enrollments_for(overrides.compact, user).select(:user_id) visible_users_ids = ::AssignmentOverride.visible_enrollments_for(overrides.compact, user).select(:user_id)
# we most likely already have the student_ids preloaded here because of overridden_for, but just in case # we most likely already have the student_ids preloaded here because of overridden_for, but just in case
if overrides.any? { |ov| ov.present? && ov.set_type == "ADHOC" && !ov.preloaded_student_ids } if overrides.any? { |ov| ov.present? && ov.set_type == "ADHOC" && !ov.preloaded_student_ids }
@ -82,7 +85,7 @@ module Api::V1::AssignmentOverride
module_ids = overrides.select { |ov| ov.present? && ov.context_module_id.present? }.map(&:context_module_id).uniq module_ids = overrides.select { |ov| ov.present? && ov.context_module_id.present? }.map(&:context_module_id).uniq
module_names = ContextModule.where(id: module_ids).pluck(:id, :name).to_h module_names = ContextModule.where(id: module_ids).pluck(:id, :name).to_h
end end
overrides.map { |override| assignment_override_json(override, visible_users_ids, student_names:, module_names:) if override } overrides.map { |override| assignment_override_json(override, visible_users_ids, student_names:, module_names:, include_child_override_due_dates:) if override }
end end
def assignment_override_collection(learning_object, include_students: false) def assignment_override_collection(learning_object, include_students: false)

View File

@ -515,6 +515,30 @@ describe AssignmentOverride do
expect(child_override.reload.workflow_state).to eq "deleted" expect(child_override.reload.workflow_state).to eq "deleted"
expect(parent_override.reload.workflow_state).to eq "deleted" expect(parent_override.reload.workflow_state).to eq "deleted"
end end
describe "#sub_assignment_due_dates" do
before :once do
@parent_assignment = @course.assignments.create!
@parent_override = AssignmentOverride.create!(title: "Parent Override", set_type: "ADHOC", assignment: @parent_assignment)
@child_assignment1 = parent_assignment.sub_assignments.create!(context: @course, sub_assignment_tag: CheckpointLabels::REPLY_TO_TOPIC)
@child_assignment2 = parent_assignment.sub_assignments.create!(context: @course, sub_assignment_tag: CheckpointLabels::REPLY_TO_ENTRY)
@child_override1 = AssignmentOverride.create!(title: "Child Override 1", set_type: "ADHOC", assignment: @child_assignment1, due_at: 1.day.from_now, parent_override: @parent_override)
@child_override2 = AssignmentOverride.create!(title: "Child Override 2", set_type: "ADHOC", assignment: @child_assignment2, due_at: 2.days.from_now, parent_override: @parent_override)
end
it "returns the correct sub_assignment_due_dates" do
expected_result = [
{ sub_assignment_tag: CheckpointLabels::REPLY_TO_TOPIC, due_at: @child_override1.due_at },
{ sub_assignment_tag: CheckpointLabels::REPLY_TO_ENTRY, due_at: @child_override2.due_at }
]
expect(@parent_override.sub_assignment_due_dates).to eq(expected_result)
end
it "returns an empty array when there are no child overrides" do
@parent_override.child_overrides.destroy_all
expect(@parent_override.sub_assignment_due_dates).to eq([])
end
end
end end
end end

View File

@ -420,16 +420,37 @@ describe "Discussion Topic Show" do
assignment: assignment:
) )
group = dt.course.groups.create!
dt.update!(group_category: group.group_category)
student_in_group = student_in_course(course: dt.course, active_all: true).user
group.group_memberships.create!(user: student_in_group)
new_section = @topic.course.course_sections.create!
everyone_due_at = 3.days.from_now
student_due_at = 4.days.from_now
group_due_at = 5.days.from_now
section_due_at = 6.days.from_now
student_lock_at = 11.days.from_now
student_unlock_at = 1.day.from_now
Checkpoints::DiscussionCheckpointCreatorService.call( Checkpoints::DiscussionCheckpointCreatorService.call(
discussion_topic: dt, discussion_topic: dt,
checkpoint_label: CheckpointLabels::REPLY_TO_TOPIC, checkpoint_label: CheckpointLabels::REPLY_TO_TOPIC,
dates: [{ type: "everyone", due_at: 2.days.from_now }], dates: [{ type: "everyone", due_at: everyone_due_at },
{ type: "override", set_type: "ADHOC", student_ids: [@student.id], due_at: student_due_at, lock_at: student_lock_at, unlock_at: student_unlock_at },
{ type: "override", set_type: "CourseSection", set_id: new_section.id, due_at: section_due_at },
{ type: "override", set_type: "Group", set_id: group.id, due_at: group_due_at }],
points_possible: 6 points_possible: 6
) )
Checkpoints::DiscussionCheckpointCreatorService.call( Checkpoints::DiscussionCheckpointCreatorService.call(
discussion_topic: dt, discussion_topic: dt,
checkpoint_label: CheckpointLabels::REPLY_TO_ENTRY, checkpoint_label: CheckpointLabels::REPLY_TO_ENTRY,
dates: [{ type: "everyone", due_at: 3.days.from_now }], dates: [{ type: "everyone", due_at: everyone_due_at },
{ type: "override", set_type: "ADHOC", student_ids: [@student.id], due_at: student_due_at, lock_at: student_lock_at, unlock_at: student_unlock_at },
{ type: "override", set_type: "CourseSection", set_id: new_section.id, due_at: section_due_at },
{ type: "override", set_type: "Group", set_id: group.id, due_at: group_due_at }],
points_possible: 7, points_possible: 7,
replies_required: 2 replies_required: 2
) )
@ -441,6 +462,31 @@ describe "Discussion Topic Show" do
expect(module_item_assign_to_card.first).not_to contain_css(due_date_input_selector) expect(module_item_assign_to_card.first).not_to contain_css(due_date_input_selector)
expect(module_item_assign_to_card.first).to contain_css(reply_to_topic_due_date_input_selector) expect(module_item_assign_to_card.first).to contain_css(reply_to_topic_due_date_input_selector)
expect(module_item_assign_to_card.first).to contain_css(required_replies_due_date_input_selector) expect(module_item_assign_to_card.first).to contain_css(required_replies_due_date_input_selector)
all_dates = get_all_dates_for_all_cards
# Everyone Card
expect(format_date_for_view(all_dates[0][:reply_to_topic], "%b %d, %Y %I:%M %p")).to eq(format_date_for_view(everyone_due_at.in_time_zone("UTC"), "%b %d, %Y %I:%M %p"))
expect(format_date_for_view(all_dates[0][:required_replies], "%b %d, %Y %I:%M %p")).to eq(format_date_for_view(everyone_due_at.in_time_zone("UTC"), "%b %d, %Y %I:%M %p"))
expect(all_dates[0][:available_from]).to eq("")
expect(all_dates[0][:until]).to eq("")
# Student Card
expect(format_date_for_view(all_dates[1][:reply_to_topic], "%b %d, %Y %I:%M %p")).to eq(format_date_for_view(student_due_at.in_time_zone("UTC"), "%b %d, %Y %I:%M %p"))
expect(format_date_for_view(all_dates[1][:required_replies], "%b %d, %Y %I:%M %p")).to eq(format_date_for_view(student_due_at.in_time_zone("UTC"), "%b %d, %Y %I:%M %p"))
expect(format_date_for_view(all_dates[1][:available_from], "%b %d, %Y %I:%M %p")).to eq(format_date_for_view(student_unlock_at.in_time_zone("UTC"), "%b %d, %Y %I:%M %p"))
expect(format_date_for_view(all_dates[1][:until], "%b %d, %Y %I:%M %p")).to eq(format_date_for_view(student_lock_at.in_time_zone("UTC"), "%b %d, %Y %I:%M %p"))
# Section Card
expect(format_date_for_view(all_dates[2][:reply_to_topic], "%b %d, %Y %I:%M %p")).to eq(format_date_for_view(section_due_at.in_time_zone("UTC"), "%b %d, %Y %I:%M %p"))
expect(format_date_for_view(all_dates[2][:required_replies], "%b %d, %Y %I:%M %p")).to eq(format_date_for_view(section_due_at.in_time_zone("UTC"), "%b %d, %Y %I:%M %p"))
expect(all_dates[2][:available_from]).to eq("")
expect(all_dates[2][:until]).to eq("")
# Group Card
expect(format_date_for_view(all_dates[3][:reply_to_topic], "%b %d, %Y %I:%M %p")).to eq(format_date_for_view(group_due_at.in_time_zone("UTC"), "%b %d, %Y %I:%M %p"))
expect(format_date_for_view(all_dates[3][:required_replies], "%b %d, %Y %I:%M %p")).to eq(format_date_for_view(group_due_at.in_time_zone("UTC"), "%b %d, %Y %I:%M %p"))
expect(all_dates[3][:available_from]).to eq("")
expect(all_dates[3][:until]).to eq("")
end end
it "does not show the button when the user does not have the moderate_forum permission" do it "does not show the button when the user does not have the moderate_forum permission" do

View File

@ -137,6 +137,22 @@ module ItemsAssignToTray
"[data-testid = 'lock_at_input']" "[data-testid = 'lock_at_input']"
end end
def reply_to_topic_datetime_selector
"[data-testid='reply_to_topic_due_at_input'] input[type='text']"
end
def required_replies_datetime_selector
"[data-testid='required_replies_due_at_input'] input[type='text']"
end
def available_from_datetime_selector
"[data-testid='unlock_at_input'] input[type='text']"
end
def until_datetime_selector
"[data-testid='lock_at_input'] input[type='text']"
end
#------------------------------ Elements ------------------------------ #------------------------------ Elements ------------------------------
def add_assign_to_card def add_assign_to_card
f(add_assign_to_card_selector) f(add_assign_to_card_selector)
@ -302,6 +318,22 @@ module ItemsAssignToTray
f(tray_header_selector) f(tray_header_selector)
end end
def reply_to_topic_datetime_inputs
ff(reply_to_topic_datetime_selector)
end
def required_replies_datetime_inputs
ff(required_replies_datetime_selector)
end
def available_from_datetime_inputs
ff(available_from_datetime_selector)
end
def until_datetime_inputs
ff(until_datetime_selector)
end
#------------------------------ Actions ------------------------------ #------------------------------ Actions ------------------------------
def click_add_assign_to_card def click_add_assign_to_card
@ -389,4 +421,50 @@ module ItemsAssignToTray
end end
wait_for_ajaximations wait_for_ajaximations
end end
def combine_date_and_time(date_input, time_input)
date = date_input.attribute("value")
time = time_input.attribute("value")
return "" if date.empty? && time.empty?
"#{date} #{time}".strip
end
def get_reply_to_topic_datetime(card_index)
combine_date_and_time(reply_to_topic_datetime_inputs[card_index * 2], reply_to_topic_datetime_inputs[(card_index * 2) + 1])
end
def get_required_replies_datetime(card_index)
combine_date_and_time(required_replies_datetime_inputs[card_index * 2], required_replies_datetime_inputs[(card_index * 2) + 1])
end
def get_available_from_datetime(card_index)
combine_date_and_time(available_from_datetime_inputs[card_index * 2], available_from_datetime_inputs[(card_index * 2) + 1])
end
def get_until_datetime(card_index)
combine_date_and_time(until_datetime_inputs[card_index * 2], until_datetime_inputs[(card_index * 2) + 1])
end
def get_all_dates_for_card(card_index)
{
reply_to_topic: get_reply_to_topic_datetime(card_index),
required_replies: get_required_replies_datetime(card_index),
available_from: get_available_from_datetime(card_index),
until: get_until_datetime(card_index)
}
end
def get_all_dates_for_all_cards
card_count = [
reply_to_topic_datetime_inputs.length,
required_replies_datetime_inputs.length,
available_from_datetime_inputs.length,
until_datetime_inputs.length
].max / 2 # Divide by 2 because we have separate inputs for date and time
(0...card_count).map do |card_index|
get_all_dates_for_card(card_index)
end
end
end end

View File

@ -52,6 +52,7 @@ import ItemAssignToCard, {
} from './ItemAssignToCard' } from './ItemAssignToCard'
import {getOverriddenAssignees, itemTypeToApiURL} from '../../utils/assignToHelper' import {getOverriddenAssignees, itemTypeToApiURL} from '../../utils/assignToHelper'
import {getEveryoneOption, type ItemAssignToTrayProps} from './ItemAssignToTray' import {getEveryoneOption, type ItemAssignToTrayProps} from './ItemAssignToTray'
import {getDueAtForCheckpointTag} from './utils'
const I18n = useI18nScope('differentiated_modules') const I18n = useI18nScope('differentiated_modules')
@ -84,6 +85,8 @@ export interface ItemAssignToTrayContentProps
} }
const MAX_PAGES = 10 const MAX_PAGES = 10
const REPLY_TO_TOPIC = 'reply_to_topic'
const REPLY_TO_ENTRY = 'reply_to_entry'
function makeCardId(): string { function makeCardId(): string {
return uid('assign-to-card', 12) return uid('assign-to-card', 12)
@ -338,6 +341,19 @@ const ItemAssignToTrayContent = ({
const overriddenTargets = getOverriddenAssignees(overrides) const overriddenTargets = getOverriddenAssignees(overrides)
delete dateDetailsApiResponse.overrides delete dateDetailsApiResponse.overrides
const baseDates: BaseDateDetails = dateDetailsApiResponse const baseDates: BaseDateDetails = dateDetailsApiResponse
if (
dateDetailsApiResponse.checkpoints &&
Array.isArray(dateDetailsApiResponse.checkpoints)
) {
dateDetailsApiResponse.checkpoints.forEach((checkpoint: any) => {
if (checkpoint.tag === REPLY_TO_ENTRY) {
baseDates.required_replies_due_at = checkpoint.due_at
} else if (checkpoint.tag === REPLY_TO_TOPIC) {
baseDates.reply_to_topic_due_at = checkpoint.due_at
}
})
}
const onlyOverrides = !dateDetailsApiResponse.visible_to_everyone const onlyOverrides = !dateDetailsApiResponse.visible_to_everyone
const allModuleAssignees: string[] = [] const allModuleAssignees: string[] = []
const hasModuleOverride = overrides?.some(override => override.context_module_id) const hasModuleOverride = overrides?.some(override => override.context_module_id)
@ -354,8 +370,8 @@ const ItemAssignToTrayContent = ({
isValid: true, isValid: true,
hasAssignees: true, hasAssignees: true,
due_at: baseDates.due_at, due_at: baseDates.due_at,
reply_to_topic_due_at: null, reply_to_topic_due_at: baseDates.reply_to_topic_due_at,
required_replies_due_at: null, required_replies_due_at: baseDates.required_replies_due_at,
original_due_at: baseDates.due_at, original_due_at: baseDates.due_at,
unlock_at: baseDates.unlock_at, unlock_at: baseDates.unlock_at,
lock_at: baseDates.lock_at, lock_at: baseDates.lock_at,
@ -412,13 +428,16 @@ const ItemAssignToTrayContent = ({
return return
} }
const cardId = makeCardId() const cardId = makeCardId()
const reply_to_topic_due_at = getDueAtForCheckpointTag(override, REPLY_TO_TOPIC)
const required_replies_due_at = getDueAtForCheckpointTag(override, REPLY_TO_ENTRY)
cards.push({ cards.push({
key: cardId, key: cardId,
isValid: true, isValid: true,
hasAssignees: true, hasAssignees: true,
due_at: override.due_at, due_at: override.due_at,
reply_to_topic_due_at: null, reply_to_topic_due_at,
required_replies_due_at: null, required_replies_due_at,
original_due_at: override.due_at, original_due_at: override.due_at,
unlock_at: override.unlock_at, unlock_at: override.unlock_at,
lock_at: override.lock_at, lock_at: override.lock_at,

View File

@ -36,6 +36,30 @@ type UseDatesHookArgs = {
cardId: string cardId: string
onCardDatesChange?: (cardId: string, dateAttribute: string, dateValue: string | null) => void onCardDatesChange?: (cardId: string, dateAttribute: string, dateValue: string | null) => void
} }
type Override = {
id: string
assignment_id: string
title: string
due_at: string | null
all_day: boolean
all_day_date: string | null
unlock_at: string
lock_at: string
unassign_item: boolean
student_ids: string[]
students: Student[]
sub_assignment_due_dates: SubAssignmentDueDate[]
}
type SubAssignmentDueDate = {
sub_assignment_tag: string
due_at: string | null
}
type Student = {
id: string
name: string
}
type UseDatesHookResult = [ type UseDatesHookResult = [
// requiredRepliesDueDate // requiredRepliesDueDate
@ -442,3 +466,10 @@ export const generateCardActionLabels = (selected: string[]) => {
} }
} }
} }
export const getDueAtForCheckpointTag = (override: Override, checkpointTag: String) => {
return override.sub_assignment_due_dates
? override.sub_assignment_due_dates.find(item => item.sub_assignment_tag === checkpointTag)
?.due_at || null
: null
}