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:
parent
c29d7b6bf8
commit
d4cbf3eca0
|
@ -116,7 +116,8 @@ class LearningObjectDatesController < ApplicationController
|
|||
Api.paginate(section_visibilities, self, route)
|
||||
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
|
||||
|
||||
render json: {
|
||||
|
|
|
@ -557,4 +557,13 @@ class AssignmentOverride < ActiveRecord::Base
|
|||
def set_root_account_id
|
||||
write_attribute(:root_account_id, root_account_id) unless read_attribute(:root_account_id)
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
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]
|
||||
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
|
||||
|
@ -52,6 +52,9 @@ module Api::V1::AssignmentOverride
|
|||
when "Noop"
|
||||
json[:noop_id] = override.set_id
|
||||
end
|
||||
if include_child_override_due_dates
|
||||
json[:sub_assignment_due_dates] = override.sub_assignment_due_dates
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -70,7 +73,7 @@ module Api::V1::AssignmentOverride
|
|||
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)
|
||||
# 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 }
|
||||
|
@ -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_names = ContextModule.where(id: module_ids).pluck(:id, :name).to_h
|
||||
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
|
||||
|
||||
def assignment_override_collection(learning_object, include_students: false)
|
||||
|
|
|
@ -515,6 +515,30 @@ describe AssignmentOverride do
|
|||
expect(child_override.reload.workflow_state).to eq "deleted"
|
||||
expect(parent_override.reload.workflow_state).to eq "deleted"
|
||||
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
|
||||
|
||||
|
|
|
@ -420,16 +420,37 @@ describe "Discussion Topic Show" do
|
|||
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(
|
||||
discussion_topic: dt,
|
||||
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
|
||||
)
|
||||
Checkpoints::DiscussionCheckpointCreatorService.call(
|
||||
discussion_topic: dt,
|
||||
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,
|
||||
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).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)
|
||||
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
|
||||
|
||||
it "does not show the button when the user does not have the moderate_forum permission" do
|
||||
|
|
|
@ -137,6 +137,22 @@ module ItemsAssignToTray
|
|||
"[data-testid = 'lock_at_input']"
|
||||
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 ------------------------------
|
||||
def add_assign_to_card
|
||||
f(add_assign_to_card_selector)
|
||||
|
@ -302,6 +318,22 @@ module ItemsAssignToTray
|
|||
f(tray_header_selector)
|
||||
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 ------------------------------
|
||||
|
||||
def click_add_assign_to_card
|
||||
|
@ -389,4 +421,50 @@ module ItemsAssignToTray
|
|||
end
|
||||
wait_for_ajaximations
|
||||
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
|
||||
|
|
|
@ -52,6 +52,7 @@ import ItemAssignToCard, {
|
|||
} from './ItemAssignToCard'
|
||||
import {getOverriddenAssignees, itemTypeToApiURL} from '../../utils/assignToHelper'
|
||||
import {getEveryoneOption, type ItemAssignToTrayProps} from './ItemAssignToTray'
|
||||
import {getDueAtForCheckpointTag} from './utils'
|
||||
|
||||
const I18n = useI18nScope('differentiated_modules')
|
||||
|
||||
|
@ -84,6 +85,8 @@ export interface ItemAssignToTrayContentProps
|
|||
}
|
||||
|
||||
const MAX_PAGES = 10
|
||||
const REPLY_TO_TOPIC = 'reply_to_topic'
|
||||
const REPLY_TO_ENTRY = 'reply_to_entry'
|
||||
|
||||
function makeCardId(): string {
|
||||
return uid('assign-to-card', 12)
|
||||
|
@ -338,6 +341,19 @@ const ItemAssignToTrayContent = ({
|
|||
const overriddenTargets = getOverriddenAssignees(overrides)
|
||||
delete dateDetailsApiResponse.overrides
|
||||
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 allModuleAssignees: string[] = []
|
||||
const hasModuleOverride = overrides?.some(override => override.context_module_id)
|
||||
|
@ -354,8 +370,8 @@ const ItemAssignToTrayContent = ({
|
|||
isValid: true,
|
||||
hasAssignees: true,
|
||||
due_at: baseDates.due_at,
|
||||
reply_to_topic_due_at: null,
|
||||
required_replies_due_at: null,
|
||||
reply_to_topic_due_at: baseDates.reply_to_topic_due_at,
|
||||
required_replies_due_at: baseDates.required_replies_due_at,
|
||||
original_due_at: baseDates.due_at,
|
||||
unlock_at: baseDates.unlock_at,
|
||||
lock_at: baseDates.lock_at,
|
||||
|
@ -412,13 +428,16 @@ const ItemAssignToTrayContent = ({
|
|||
return
|
||||
}
|
||||
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({
|
||||
key: cardId,
|
||||
isValid: true,
|
||||
hasAssignees: true,
|
||||
due_at: override.due_at,
|
||||
reply_to_topic_due_at: null,
|
||||
required_replies_due_at: null,
|
||||
reply_to_topic_due_at,
|
||||
required_replies_due_at,
|
||||
original_due_at: override.due_at,
|
||||
unlock_at: override.unlock_at,
|
||||
lock_at: override.lock_at,
|
||||
|
|
|
@ -36,6 +36,30 @@ type UseDatesHookArgs = {
|
|||
cardId: string
|
||||
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 = [
|
||||
// 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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue