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
podcastHasStudentPosts
isSectionSpecific
replyToEntryRequiredCount
groupSet {
_id
}
@ -119,6 +120,13 @@ describe Mutations::CreateDiscussionTopic do
title
}
}
checkpoints {
dueAt
name
onlyVisibleToOverrides
pointsPossible
tag
}
}
}
errors {
@ -1096,7 +1104,18 @@ describe Mutations::CreateDiscussionTopic do
GQL
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
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
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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,8 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {REPLY_TO_TOPIC, REPLY_TO_ENTRY} from './constants'
const prepareOverride = (
overrideDueDate,
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 = (
isEditing,
title,
@ -165,7 +191,8 @@ export const prepareAssignmentPayload = (
peerReviewsPerStudent,
peerReviewDueDate,
intraGroupPeerReviews,
masteryPathsOption
masteryPathsOption,
isCheckpoints
) => {
// Return null immediately if the assignment is not graded
if (!isGraded) return null
@ -176,10 +203,8 @@ export const prepareAssignmentPayload = (
info.assignedList.includes(defaultEveryoneOption.assetCode) ||
info.assignedList.includes(defaultEveryoneElseOption.assetCode)
) || {}
// Common payload properties for graded assignments
let payload = {
pointsPossible,
postToSis,
gradingType: displayGradeAs,
assignmentGroupId: assignmentGroup || null,
@ -195,11 +220,19 @@ export const prepareAssignmentPayload = (
defaultEveryoneOption,
masteryPathsOption
),
dueAt: everyoneOverride.dueDate || null,
lockAt: everyoneOverride.availableUntil || null,
unlockAt: everyoneOverride.availableFrom || null,
onlyVisibleToOverrides: !Object.keys(everyoneOverride).length,
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
if (!isEditing) {