add checkpoint to create discussion mutation

add checkpoint to create discussion mutation.
This only supports reply to entry, reply to topic,
and additional replies required fields

closes VICE-4104
flag=discussion_checkpoints
flag=discussion_create

Test Plan:
- create a discussion with checkpoints
- set points possible for reply to entry and
  reply to topic
- set additional replies required
- submit the discussion topic
- go to edit the discussion and confirm that
  the discussion is checkpointed with the
  reply to topic, reply to entry, and additional
  replies required fields populated with the
  values set at creation
- go to the rails console and confirm that the
  newly created discussion topic's assignment
  contains two sub assignments with the correct
  point values and tag

Change-Id: I4f40c9b3abbadccab35b78661574a84a1e64ee6d
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/345228
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Product-Review: Samuel Lee <samuel.lee@instructure.com>
Reviewed-by: Omar Soto-Fortuño <omar.soto@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
This commit is contained in:
Samuel Lee 2024-04-15 08:56:09 -05:00
parent fc41228955
commit 97234c71ed
7 changed files with 169 additions and 39 deletions

View File

@ -92,6 +92,7 @@ describe Mutations::CreateDiscussionTopic do
podcastEnabled podcastEnabled
podcastHasStudentPosts podcastHasStudentPosts
isSectionSpecific isSectionSpecific
replyToEntryRequiredCount
groupSet { groupSet {
_id _id
} }
@ -119,6 +120,13 @@ describe Mutations::CreateDiscussionTopic do
title title
} }
} }
checkpoints {
dueAt
name
onlyVisibleToOverrides
pointsPossible
tag
}
} }
} }
errors { errors {
@ -1096,7 +1104,18 @@ describe Mutations::CreateDiscussionTopic do
GQL GQL
result = execute_with_input_with_assignment(query) result = execute_with_input_with_assignment(query)
expect(result["errors"]).to be_nil discussion_topic = result.dig("data", "createDiscussionTopic", "discussionTopic")
reply_to_topic_checkpoint = discussion_topic["assignment"]["checkpoints"].find { |checkpoint| checkpoint["tag"] == CheckpointLabels::REPLY_TO_TOPIC }
reply_to_entry_checkpoint = discussion_topic["assignment"]["checkpoints"].find { |checkpoint| checkpoint["tag"] == CheckpointLabels::REPLY_TO_ENTRY }
aggregate_failures do
expect(result["errors"]).to be_nil
expect(discussion_topic["assignment"]["checkpoints"][0]["name"]).to eq title
expect(reply_to_topic_checkpoint).to be_truthy
expect(reply_to_entry_checkpoint).to be_truthy
expect(reply_to_topic_checkpoint["pointsPossible"]).to eq 10
expect(reply_to_entry_checkpoint["pointsPossible"]).to eq 15
expect(discussion_topic["replyToEntryRequiredCount"]).to eq 3
end
end end
it "successfully creates a discussion topic with checkpoints using dueAt, lockAt, unlockAt" do it "successfully creates a discussion topic with checkpoints using dueAt, lockAt, unlockAt" do

View File

@ -1717,6 +1717,48 @@ describe "discussions" do
expect(assignment.only_visible_to_overrides).to be true expect(assignment.only_visible_to_overrides).to be true
end end
end end
context "checkpoints" do
before do
course.root_account.enable_feature!(:discussion_checkpoints)
end
it "successfully creates a discussion topic with checkpoints" do
get "/courses/#{course.id}/discussion_topics/new"
title = "Graded Discussion Topic with checkpoints"
f("input[placeholder='Topic Title']").send_keys title
force_click_native('input[type=checkbox][value="graded"]')
wait_for_ajaximations
force_click_native('input[type=checkbox][value="checkpoints"]')
f("input[data-testid='points-possible-input-reply-to-topic']").send_keys "5"
f("input[data-testid='reply-to-entry-required-count']").send_keys :backspace
f("input[data-testid='reply-to-entry-required-count']").send_keys 3
f("input[data-testid='points-possible-input-reply-to-entry']").send_keys "7"
f("button[data-testid='save-and-publish-button']").click
wait_for_ajaximations
dt = DiscussionTopic.last
expect(dt.reply_to_entry_required_count).to eq 3
assignment = Assignment.last
expect(assignment.has_sub_assignments?).to be true
sub_assignments = SubAssignment.where(parent_assignment_id: assignment.id)
sub_assignment1 = sub_assignments.find_by(sub_assignment_tag: CheckpointLabels::REPLY_TO_TOPIC)
sub_assignment2 = sub_assignments.find_by(sub_assignment_tag: CheckpointLabels::REPLY_TO_ENTRY)
expect(sub_assignment1.sub_assignment_tag).to eq "reply_to_topic"
expect(sub_assignment1.points_possible).to eq 5
expect(sub_assignment2.sub_assignment_tag).to eq "reply_to_entry"
expect(sub_assignment2.points_possible).to eq 7
end
end
end end
end end
end end

View File

@ -42,6 +42,7 @@ export const CREATE_DISCUSSION_TOPIC = gql`
$specificSections: String $specificSections: String
$groupCategoryId: ID $groupCategoryId: ID
$assignment: AssignmentCreate $assignment: AssignmentCreate
$checkpoints: [DiscussionCheckpoints!]
$fileId: ID $fileId: ID
) { ) {
createDiscussionTopic( createDiscussionTopic(
@ -66,6 +67,7 @@ export const CREATE_DISCUSSION_TOPIC = gql`
specificSections: $specificSections specificSections: $specificSections
groupCategoryId: $groupCategoryId groupCategoryId: $groupCategoryId
assignment: $assignment assignment: $assignment
checkpoints: $checkpoints
fileId: $fileId fileId: $fileId
} }
) { ) {
@ -109,6 +111,13 @@ export const CREATE_DISCUSSION_TOPIC = gql`
dueAt dueAt
enabled enabled
} }
checkpoints {
dueAt
name
onlyVisibleToOverrides
pointsPossible
tag
}
} }
attachment { attachment {
...Attachment ...Attachment

View File

@ -51,6 +51,8 @@ import {
defaultEveryoneElseOption, defaultEveryoneElseOption,
masteryPathsOption, masteryPathsOption,
useShouldShowContent, useShouldShowContent,
REPLY_TO_TOPIC,
REPLY_TO_ENTRY,
} from '../../util/constants' } from '../../util/constants'
import {AttachmentDisplay} from '@canvas/discussions/react/components/AttachmentDisplay/AttachmentDisplay' import {AttachmentDisplay} from '@canvas/discussions/react/components/AttachmentDisplay/AttachmentDisplay'
@ -59,7 +61,7 @@ import {UsageRightsContainer} from '../../containers/usageRights/UsageRightsCont
import {AlertManagerContext} from '@canvas/alerts/react/AlertManager' import {AlertManagerContext} from '@canvas/alerts/react/AlertManager'
import {ScreenReaderContent} from '@instructure/ui-a11y-content' import {ScreenReaderContent} from '@instructure/ui-a11y-content'
import {prepareAssignmentPayload} from '../../util/payloadPreparations' import {prepareAssignmentPayload, prepareCheckpointsPayload} from '../../util/payloadPreparations'
import {validateTitle, validateFormFields} from '../../util/formValidation' import {validateTitle, validateFormFields} from '../../util/formValidation'
import { import {
@ -227,13 +229,21 @@ export default function DiscussionTopicForm({
const [isCheckpoints, setIsCheckpoints] = useState( const [isCheckpoints, setIsCheckpoints] = useState(
currentDiscussionTopic?.assignment?.hasSubAssignments || false currentDiscussionTopic?.assignment?.hasSubAssignments || false
) )
const getCheckpointsPointsPossible = (checkpointLabel) => { const getCheckpointsPointsPossible = checkpointLabel => {
const checkpoint = currentDiscussionTopic?.assignment?.checkpoints?.find(c => c.tag === checkpointLabel) const checkpoint = currentDiscussionTopic?.assignment?.checkpoints?.find(
c => c.tag === checkpointLabel
)
return checkpoint ? checkpoint.pointsPossible : 0 return checkpoint ? checkpoint.pointsPossible : 0
} }
const [pointsPossibleReplyToTopic, setPointsPossibleReplyToTopic] = useState(getCheckpointsPointsPossible('reply_to_topic')) const [pointsPossibleReplyToTopic, setPointsPossibleReplyToTopic] = useState(
const [pointsPossibleReplyToEntry, setPointsPossibleReplyToEntry] = useState(getCheckpointsPointsPossible('reply_to_entry')) getCheckpointsPointsPossible(REPLY_TO_TOPIC)
const [replyToEntryRequiredCount, setReplyToEntryRequiredCount] = useState(currentDiscussionTopic?.replyToEntryRequiredCount || 1) )
const [pointsPossibleReplyToEntry, setPointsPossibleReplyToEntry] = useState(
getCheckpointsPointsPossible(REPLY_TO_ENTRY)
)
const [replyToEntryRequiredCount, setReplyToEntryRequiredCount] = useState(
currentDiscussionTopic?.replyToEntryRequiredCount || 1
)
const assignmentDueDateContext = { const assignmentDueDateContext = {
assignedInfoList, assignedInfoList,
@ -360,7 +370,14 @@ export default function DiscussionTopicForm({
peerReviewsPerStudent, peerReviewsPerStudent,
peerReviewDueDate, peerReviewDueDate,
intraGroupPeerReviews, intraGroupPeerReviews,
masteryPathsOption masteryPathsOption,
isCheckpoints
),
checkpoints: prepareCheckpointsPayload(
pointsPossibleReplyToTopic,
pointsPossibleReplyToEntry,
replyToEntryRequiredCount,
isCheckpoints
), ),
groupCategoryId: isGroupDiscussion ? groupCategoryId : null, groupCategoryId: isGroupDiscussion ? groupCategoryId : null,
specificSections: shouldShowPostToSectionOption ? sectionIdsToPostTo.join() : 'all', specificSections: shouldShowPostToSectionOption ? sectionIdsToPostTo.join() : 'all',

View File

@ -22,8 +22,9 @@ import userEvent from '@testing-library/user-event'
import React from 'react' import React from 'react'
import DiscussionTopicForm from '../DiscussionTopicForm' import DiscussionTopicForm from '../DiscussionTopicForm'
import {DiscussionTopic} from '../../../../graphql/DiscussionTopic' import {DiscussionTopic} from '../../../../graphql/DiscussionTopic'
import { Assignment } from '../../../../graphql/Assignment' import {Assignment} from '../../../../graphql/Assignment'
import {GroupSet} from '../../../../graphql/GroupSet' import {GroupSet} from '../../../../graphql/GroupSet'
import {REPLY_TO_TOPIC, REPLY_TO_ENTRY} from '../../../util/constants'
jest.mock('@canvas/rce/react/CanvasRce') jest.mock('@canvas/rce/react/CanvasRce')
@ -455,19 +456,21 @@ describe('DiscussionTopicForm', () => {
}) })
it('renders the checkpoints checkbox as selected when there are existing checkpoints', () => { it('renders the checkpoints checkbox as selected when there are existing checkpoints', () => {
const {getByTestId} = setup({ const {getByTestId} = setup({
currentDiscussionTopic: DiscussionTopic.mock({assignment: Assignment.mock({hasSubAssignments: true})}), currentDiscussionTopic: DiscussionTopic.mock({
assignment: Assignment.mock({hasSubAssignments: true}),
}),
}) })
const checkbox = getByTestId('checkpoints-checkbox') const checkbox = getByTestId('checkpoints-checkbox')
expect(checkbox.checked).toBe(true) expect(checkbox.checked).toBe(true)
}) })
describe('Checkpoints Settings', () => { describe('Checkpoints Settings', () => {
let getByTestId, getByLabelText; let getByTestId, getByLabelText
const setupCheckpoints = (setupFunction) => { const setupCheckpoints = setupFunction => {
const discussionTopicSetup = setupFunction const discussionTopicSetup = setupFunction
getByTestId = discussionTopicSetup.getByTestId; getByTestId = discussionTopicSetup.getByTestId
getByLabelText = discussionTopicSetup.getByLabelText; getByLabelText = discussionTopicSetup.getByLabelText
getByLabelText('Graded').click() getByLabelText('Graded').click()
@ -570,24 +573,28 @@ describe('DiscussionTopicForm', () => {
}) })
it('sets the correct checkpoint settings values when there are existing checkpoints', () => { it('sets the correct checkpoint settings values when there are existing checkpoints', () => {
const {getByTestId} = setup({ const {getByTestId} = setup({
currentDiscussionTopic: DiscussionTopic.mock({replyToEntryRequiredCount: 5, assignment: Assignment.mock({ currentDiscussionTopic: DiscussionTopic.mock({
hasSubAssignments: true, replyToEntryRequiredCount: 5,
checkpoints: [{ assignment: Assignment.mock({
"dueAt": null, hasSubAssignments: true,
"name": "checkpoint discussion", checkpoints: [
"onlyVisibleToOverrides": false, {
"pointsPossible": 6, dueAt: null,
"tag": "reply_to_topic" name: 'checkpoint discussion',
}, onlyVisibleToOverrides: false,
{ pointsPossible: 6,
"dueAt": null, tag: REPLY_TO_TOPIC,
"name": "checkpoint discussion", },
"onlyVisibleToOverrides": false, {
"pointsPossible": 7, dueAt: null,
"tag": "reply_to_entry" name: 'checkpoint discussion',
} onlyVisibleToOverrides: false,
]} pointsPossible: 7,
)}), tag: REPLY_TO_ENTRY,
},
],
}),
}),
}) })
const numberInputReplyToTopic = getByTestId('points-possible-input-reply-to-topic') const numberInputReplyToTopic = getByTestId('points-possible-input-reply-to-topic')
@ -599,4 +606,4 @@ describe('DiscussionTopicForm', () => {
}) })
}) })
}) })
}) })

View File

@ -63,6 +63,9 @@ export const ASSIGNMENT_OVERRIDE_GRAPHQL_TYPENAMES = {
export const minimumReplyToEntryRequiredCount = 1 export const minimumReplyToEntryRequiredCount = 1
export const maximumReplyToEntryRequiredCount = 10 export const maximumReplyToEntryRequiredCount = 10
export const REPLY_TO_TOPIC = 'reply_to_topic'
export const REPLY_TO_ENTRY = 'reply_to_entry'
export const useShouldShowContent = ( export const useShouldShowContent = (
isGraded, isGraded,
isAnnouncement, isAnnouncement,

View File

@ -16,6 +16,8 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import {REPLY_TO_TOPIC, REPLY_TO_ENTRY} from './constants'
const prepareOverride = ( const prepareOverride = (
overrideDueDate, overrideDueDate,
overrideAvailableUntil, overrideAvailableUntil,
@ -149,6 +151,30 @@ const preparePeerReviewPayload = (
} }
} }
export const prepareCheckpointsPayload = (
pointsPossibleReplyToTopic,
pointsPossibleReplyToEntry,
replyToEntryRequiredCount,
isCheckpoints
) => {
return isCheckpoints
? [
{
checkpointLabel: REPLY_TO_TOPIC,
pointsPossible: pointsPossibleReplyToTopic,
dates: [],
repliesRequired: replyToEntryRequiredCount,
},
{
checkpointLabel: REPLY_TO_ENTRY,
pointsPossible: pointsPossibleReplyToEntry,
dates: [],
repliesRequired: replyToEntryRequiredCount,
},
]
: []
}
export const prepareAssignmentPayload = ( export const prepareAssignmentPayload = (
isEditing, isEditing,
title, title,
@ -165,7 +191,8 @@ export const prepareAssignmentPayload = (
peerReviewsPerStudent, peerReviewsPerStudent,
peerReviewDueDate, peerReviewDueDate,
intraGroupPeerReviews, intraGroupPeerReviews,
masteryPathsOption masteryPathsOption,
isCheckpoints
) => { ) => {
// Return null immediately if the assignment is not graded // Return null immediately if the assignment is not graded
if (!isGraded) return null if (!isGraded) return null
@ -176,10 +203,8 @@ export const prepareAssignmentPayload = (
info.assignedList.includes(defaultEveryoneOption.assetCode) || info.assignedList.includes(defaultEveryoneOption.assetCode) ||
info.assignedList.includes(defaultEveryoneElseOption.assetCode) info.assignedList.includes(defaultEveryoneElseOption.assetCode)
) || {} ) || {}
// Common payload properties for graded assignments // Common payload properties for graded assignments
let payload = { let payload = {
pointsPossible,
postToSis, postToSis,
gradingType: displayGradeAs, gradingType: displayGradeAs,
assignmentGroupId: assignmentGroup || null, assignmentGroupId: assignmentGroup || null,
@ -195,11 +220,19 @@ export const prepareAssignmentPayload = (
defaultEveryoneOption, defaultEveryoneOption,
masteryPathsOption masteryPathsOption
), ),
dueAt: everyoneOverride.dueDate || null,
lockAt: everyoneOverride.availableUntil || null,
unlockAt: everyoneOverride.availableFrom || null,
onlyVisibleToOverrides: !Object.keys(everyoneOverride).length, onlyVisibleToOverrides: !Object.keys(everyoneOverride).length,
gradingStandardId: gradingSchemeId || null, gradingStandardId: gradingSchemeId || null,
forCheckpoints: isCheckpoints,
}
// Additional properties if graded assignment is not checkpointed
if (!isCheckpoints) {
payload = {
...payload,
pointsPossible,
dueAt: everyoneOverride.dueDate || null,
lockAt: everyoneOverride.availableUntil || null,
unlockAt: everyoneOverride.availableFrom || null,
}
} }
// Additional properties for creation of a graded assignment // Additional properties for creation of a graded assignment
if (!isEditing) { if (!isEditing) {