Add graded options to discussion create/edit

closes VICE-3762
flag=discussion_create

Test Plan:
- with discussion_create ff on
- go to discussion create page
- check "Graded" checkbox
- should see:
 - points possible input
 - display grade as selector
 - assignment group selector

Change-Id: I11589a814a841543ce8a00fe0f2fe80655e3eec1
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/329838
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
QA-Review: Jason Gillett <jason.gillett@instructure.com>
Product-Review: Drake Harper <drake.harper@instructure.com>
Reviewed-by: Caleb Guanzon <cguanzon@instructure.com>
This commit is contained in:
Drake Harper 2023-10-09 15:07:12 -06:00
parent dda1f49fe0
commit 605a770890
13 changed files with 484 additions and 11 deletions

View File

@ -0,0 +1,37 @@
/*
* Copyright (C) 2023 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {string} from 'prop-types'
import gql from 'graphql-tag'
export const AssignmentGroup = {
fragment: gql`
fragment AssignmentGroup on AssignmentGroup {
_id
name
}
`,
shape: {
_id: string,
name: string,
},
mock: () => ({
_id: '1',
name: 'Homework',
}),
}

View File

@ -17,6 +17,7 @@
*/
import {GroupSet} from './GroupSet'
import {AssignmentGroup} from './AssignmentGroup'
import {Section} from './Section'
import {arrayOf, shape, string} from 'prop-types'
import gql from 'graphql-tag'
@ -27,6 +28,11 @@ export const Course = {
_id
id
name
assignmentGroupsConnection {
nodes {
...AssignmentGroup
}
}
enrollmentsConnection {
nodes {
user {
@ -48,11 +54,15 @@ export const Course = {
}
}
}
${AssignmentGroup.fragment}
`,
shape: shape({
_id: string,
id: string,
name: string,
assignmentGroupsConnection: shape({
nodes: arrayOf(AssignmentGroup.shape),
}),
enrollmentsConnection: shape({
nodes: arrayOf({
user: {
@ -72,6 +82,9 @@ export const Course = {
_id: '1',
id: 'K3n9F08vw4',
name: 'X-Men School',
assignmentGroupsConnection: shape({
nodes: [AssignmentGroup.mock()],
}),
enrollmentsConnection: shape({
nodes: [
{

View File

@ -35,6 +35,7 @@ import {DateTimeInput} from '@instructure/ui-date-time-input'
import CanvasMultiSelect from '@canvas/multi-select'
import CanvasRce from '@canvas/rce/react/CanvasRce'
import {Alert} from '@instructure/ui-alerts'
import {GradedDiscussionOptions} from '../GradedDiscussionOptions/GradedDiscussionOptions'
const I18n = useI18nScope('discussion_create')
@ -42,6 +43,7 @@ export default function DiscussionTopicForm({
isEditing,
currentDiscussionTopic,
isStudent,
assignmentGroups,
sections,
groupCategories,
onSubmit,
@ -118,12 +120,12 @@ export default function DiscussionTopicForm({
const [published, setPublished] = useState(false)
// To be implemented in phase 2, kept as a reminder
// const [pointsPossible, setPointsPossible] = useState(0)
// const [displayGradeAs, setDisplayGradeAs] = useState('letter')
// const [assignmentGroup, setAssignmentGroup] = useState('')
// const [peerReviewAssignment, setPeerReviewAssignment] = useState('off')
// const [assignTo, setAssignTo] = useState('')
// const [dueDate, setDueDate] = useState(0)
const [pointsPossible, setPointsPossible] = useState(0)
const [displayGradeAs, setDisplayGradeAs] = useState('points')
const [assignmentGroup, setAssignmentGroup] = useState('')
const [peerReviewAssignment, setPeerReviewAssignment] = useState('off')
const [assignTo, setAssignTo] = useState('')
const [dueDate, setDueDate] = useState('')
const [showGroupCategoryModal, setShowGroupCategoryModal] = useState(false)
@ -316,7 +318,7 @@ export default function DiscussionTopicForm({
}
setDiscussionAnonymousState(value)
}}
disabled={isEditing}
disabled={isEditing || isGraded}
>
<RadioInput
key="off"
@ -540,7 +542,23 @@ export default function DiscussionTopicForm({
{!isAnnouncement &&
!isGroupContext &&
(isGraded ? (
<div>Graded options here</div>
<View as="div" data-testid="assignment-settings-section">
<GradedDiscussionOptions
assignmentGroups={assignmentGroups}
pointsPossible={pointsPossible}
setPointsPossible={setPointsPossible}
displayGradeAs={displayGradeAs}
setDisplayGradeAs={setDisplayGradeAs}
assignmentGroup={assignmentGroup}
setAssignmentGroup={setAssignmentGroup}
peerReviewAssignment={peerReviewAssignment}
setPeerReviewAssignment={setPeerReviewAssignment}
assignTo={assignTo}
setAssignTo={setAssignTo}
dueDate={dueDate}
setDueDate={setDueDate}
/>
</View>
) : (
<FormFieldGroup description="" width={inputWidth}>
<DateTimeInput
@ -610,6 +628,7 @@ export default function DiscussionTopicForm({
}
DiscussionTopicForm.propTypes = {
assignmentGroups: PropTypes.arrayOf(PropTypes.object),
isEditing: PropTypes.bool,
currentDiscussionTopic: PropTypes.object,
isStudent: PropTypes.bool,

View File

@ -26,6 +26,7 @@ jest.mock('@canvas/rce/react/CanvasRce')
const setup = ({
isEditing = false,
currentDiscussionTopic = {},
assignmentGroups = [],
isStudent = false,
sections = [],
groupCategories = [],
@ -33,6 +34,7 @@ const setup = ({
} = {}) => {
return render(
<DiscussionTopicForm
assignmentGroups={assignmentGroups}
isEditing={isEditing}
currentDiscussionTopic={currentDiscussionTopic}
isStudent={isStudent}
@ -171,14 +173,14 @@ describe('DiscussionTopicForm', () => {
},
}
const {queryByText, getByLabelText, queryByLabelText} = setup()
const {queryByText, queryByTestId, getByLabelText, queryByLabelText} = setup()
expect(queryByLabelText('Add to student to-do')).toBeInTheDocument()
expect(queryByText('All Sections')).toBeInTheDocument()
expect(queryByText('Graded options here')).not.toBeInTheDocument() // TODO: Update in Phase 2
expect(queryByTestId('assignment-settings-section')).not.toBeInTheDocument()
getByLabelText('Graded').click()
expect(queryByLabelText('Add to student to-do')).not.toBeInTheDocument()
expect(queryByLabelText('Post to')).not.toBeInTheDocument()
expect(queryByText('Graded options here')).toBeInTheDocument() // TODO: Update in Phase 2
expect(queryByTestId('assignment-settings-section')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,46 @@
/*
* Copyright (C) 2023 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react'
import {useScope as useI18nScope} from '@canvas/i18n'
import {SimpleSelect} from '@instructure/ui-simple-select'
const I18n = useI18nScope('discussion_create')
export const AssignmentGroupSelect = ({
assignmentGroup,
setAssignmentGroup,
availableAssignmentGroups,
}) => {
return (
<SimpleSelect
renderLabel={I18n.t('Assignment Group')}
value={assignmentGroup}
onChange={(_event, {id}) => {
setAssignmentGroup(id)
}}
>
{availableAssignmentGroups.map(group => (
<SimpleSelect.Option id={group._id} key={group._id} value={group._id}>
{group.name}
</SimpleSelect.Option>
))}
</SimpleSelect>
)
}

View File

@ -0,0 +1,63 @@
/*
* Copyright (C) 2023 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react'
import {useScope as useI18nScope} from '@canvas/i18n'
import {SimpleSelect} from '@instructure/ui-simple-select'
const I18n = useI18nScope('discussion_create')
const gradedDiscussionOptions = [
{
id: 'points',
label: I18n.t('Points'),
},
{
id: 'percentage',
label: I18n.t('Percentage'),
},
{
id: 'complete_incomplete',
label: I18n.t('Complete/Incomplete'),
},
{
id: 'letter_grade',
label: I18n.t('Letter Grade'),
},
{
id: 'gpa_scale',
label: I18n.t('GPA Scale'),
},
]
export const DisplayGradeAs = ({displayGradeAs, setDisplayGradeAs}) => {
return (
<SimpleSelect
renderLabel={I18n.t('Display Grade As')}
value={displayGradeAs}
onChange={(_event, {id}) => setDisplayGradeAs(id)}
>
{gradedDiscussionOptions.map(option => (
<SimpleSelect.Option id={option.id} key={option.id} value={option.id}>
{option.label}
</SimpleSelect.Option>
))}
</SimpleSelect>
)
}

View File

@ -0,0 +1,88 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/*
* Copyright (C) 2023 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react'
import PropTypes from 'prop-types'
import {Flex} from '@instructure/ui-flex'
import {Text} from '@instructure/ui-text'
import {View} from '@instructure/ui-view'
import {AssignmentGroupSelect} from './AssignmentGroupSelect'
import {PointsPossible} from './PointsPossible'
import {DisplayGradeAs} from './DisplayGradeAs'
// TODO: remove eslint-disable once this component is implemented
// at the top of the file
export const GradedDiscussionOptions = ({
assignmentGroups,
pointsPossible,
setPointsPossible,
displayGradeAs,
setDisplayGradeAs,
assignmentGroup,
setAssignmentGroup,
peerReviewAssignment,
setPeerReviewAssignment,
assignTo,
setAssignTo,
dueDate,
setDueDate,
}) => {
return (
<View as="div">
<View as="div" margin="medium 0">
<PointsPossible pointsPossible={pointsPossible} setPointsPossible={setPointsPossible} />
</View>
<View as="div" margin="medium 0">
<DisplayGradeAs displayGradeAs={displayGradeAs} setDisplayGradeAs={setDisplayGradeAs} />
</View>
<View as="div" margin="medium 0">
<AssignmentGroupSelect
assignmentGroup={assignmentGroup}
setAssignmentGroup={setAssignmentGroup}
availableAssignmentGroups={assignmentGroups}
/>
</View>
<View as="div" margin="medium 0">
<Text>Peer Review</Text>
</View>
<View as="div" margin="medium 0">
<Text>Assignment Settings</Text>
</View>
</View>
)
}
GradedDiscussionOptions.propTypes = {
assignmentGroups: PropTypes.array,
pointsPossible: PropTypes.number,
setPointsPossible: PropTypes.func,
displayGradeAs: PropTypes.string,
setDisplayGradeAs: PropTypes.func,
assignmentGroup: PropTypes.string,
setAssignmentGroup: PropTypes.func,
peerReviewAssignment: PropTypes.string,
setPeerReviewAssignment: PropTypes.func,
assignTo: PropTypes.string,
setAssignTo: PropTypes.func,
dueDate: PropTypes.string,
setDueDate: PropTypes.func,
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (C) 2023 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react'
import {useScope as useI18nScope} from '@canvas/i18n'
import {NumberInput} from '@instructure/ui-number-input'
const I18n = useI18nScope('discussion_create')
export const PointsPossible = ({pointsPossible, setPointsPossible}) => {
return (
<NumberInput
renderLabel={I18n.t('Points Possible')}
onIncrement={() => setPointsPossible(pointsPossible + 1)}
onDecrement={() => setPointsPossible(pointsPossible - 1)}
value={pointsPossible}
onChange={event => {
// don't allow non-numeric values
if (!/^\d*\.?\d*$/.test(event.target.value)) return
setPointsPossible(Number.parseInt(event.target.value, 10))
}}
/>
)
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (C) 2023 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {render} from '@testing-library/react'
import React from 'react'
import {AssignmentGroupSelect} from '../AssignmentGroupSelect'
const defaultProps = {
availableAssignmentGroups: [],
assignmentGroup: '',
setAssignmentGroup: () => {},
}
const renderAssignmentGroupSelect = () => {
return render(<AssignmentGroupSelect {...defaultProps} />)
}
describe('AssignmentGroupSelect', () => {
it('renders', () => {
const {getByText} = renderAssignmentGroupSelect()
expect(getByText('Assignment Group')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,37 @@
/*
* Copyright (C) 2023 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {render} from '@testing-library/react'
import React from 'react'
import {DisplayGradeAs} from '../DisplayGradeAs'
const defaultProps = {
displayGradeAs: 'points',
setDisplayGradeAs: () => {},
}
const renderDisplayGradeAs = () => {
return render(<DisplayGradeAs {...defaultProps} />)
}
describe('DisplayGradeAs', () => {
it('renders', () => {
const {getByText} = renderDisplayGradeAs()
expect(getByText('Display Grade As')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,52 @@
/*
* Copyright (C) 2023 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {render} from '@testing-library/react'
import React from 'react'
import {GradedDiscussionOptions} from '../GradedDiscussionOptions'
const defaultProps = {
assignmentGroups: [],
pointsPossible: 10,
setPointsPossible: () => {},
displayGradeAs: 'points',
setDisplayGradeAs: () => {},
assignmentGroup: '',
setAssignmentGroup: () => {},
peerReviewAssignment: '',
setPeerReviewAssignment: () => {},
assignTo: '',
setAssignTo: () => {},
dueDate: '',
setDueDate: () => {},
}
const renderGradedDiscussionOptions = () => {
return render(<GradedDiscussionOptions {...defaultProps} />)
}
describe('GradedDiscussionOptions', () => {
it('renders', () => {
const {getByText} = renderGradedDiscussionOptions()
expect(getByText('Points Possible')).toBeInTheDocument()
expect(getByText('Display Grade As')).toBeInTheDocument()
expect(getByText('Assignment Group')).toBeInTheDocument()
expect(getByText('Peer Review')).toBeInTheDocument()
expect(getByText('Assignment Settings')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,37 @@
/*
* Copyright (C) 2023 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {render} from '@testing-library/react'
import React from 'react'
import {PointsPossible} from '../PointsPossible'
const defaultProps = {
pointsPossible: 10,
setPointsPossible: () => {},
}
const renderPointsPossible = () => {
return render(<PointsPossible {...defaultProps} />)
}
describe('PointsPossible', () => {
it('renders', () => {
const {getByText} = renderPointsPossible()
expect(getByText('Points Possible')).toBeInTheDocument()
})
})

View File

@ -91,6 +91,7 @@ export default function DiscussionTopicFormContainer() {
isEditing={isEditing}
currentDiscussionTopic={currentDiscussionTopic}
isStudent={ENV.current_user_is_student}
assignmentGroups={currentContext?.assignmentGroupsConnection?.nodes}
sections={sections}
groupCategories={groupCategories}
onSubmit={({