add checkpoints to assignments index student

flag=react_discussions_post
flag=discussion_checkpoints
flag=assignments_2_student

fixes VICE-4289

test plan:
- turn on all feature flags listed above
- create a checkpointed discussion with due dates
, points possible, and replies required for checkpoints
- as a student, visit assignments index
- verify checkpointed discussions match designs

Change-Id: I90decc73e193c37926f5065137114a23b4809fdb
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/350105
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
QA-Review: Chawn Neal <chawn.neal@instructure.com>
Reviewed-by: Jason Gillett <jason.gillett@instructure.com>
Product-Review: Sam Garza <sam.garza@instructure.com>
This commit is contained in:
Caleb Guanzon 2024-06-13 10:45:19 -06:00
parent 02debf1861
commit 6612087944
7 changed files with 376 additions and 1 deletions

26
ui/api.d.ts vendored
View File

@ -180,6 +180,8 @@ export type Assignment = Readonly<{
automatic_peer_reviews: boolean
can_duplicate: boolean
course_id: string
checkpoints: Checkpoint[]
discussion_topic: DiscussionTopic
due_date_required: boolean
final_grader_id: null | string
grade_group_students_individually: boolean
@ -646,3 +648,27 @@ export type ReleaseNote = {
date: string
new: boolean
}
export type DiscussionTopic = {
reply_to_entry_required_count: number
}
export type Checkpoint = {
due_at: string | null
name: string
only_visible_to_overrides: boolean
overrides: CheckpointOverride[]
points_possible: number
tag: string
}
export type CheckpointOverride = {
all_day: boolean
all_day_date: string
assignment_id: string
due_at: string
id: string
student_ids: string[]
title: string
unassign_item: boolean
}

View File

@ -24,6 +24,7 @@ import DirectShareCourseTray from '@canvas/direct-sharing/react/components/Direc
import DirectShareUserModal from '@canvas/direct-sharing/react/components/DirectShareUserModal'
import {scoreToPercentage} from '@canvas/grading/GradeCalculationHelper'
import {useScope as useI18nScope} from '@canvas/i18n'
import {ListViewCheckpoints} from '@canvas/list-view-checkpoints/react/ListViewCheckpoints'
import LockIconView from '@canvas/lock-icon'
import * as MoveItem from '@canvas/move-item-tray'
import PublishIconView from '@canvas/publish-icon-view'
@ -43,6 +44,7 @@ import scoreTemplate from '../../jst/_assignmentListItemScore.handlebars'
import AssignmentKeyBindingsMixin from '../mixins/AssignmentKeyBindingsMixin'
import CreateAssignmentView from './CreateAssignmentView'
import ItemAssignToTray from '@canvas/context-modules/differentiated-modules/react/Item/ItemAssignToTray'
import {captureException} from '@sentry/browser'
const I18n = useI18nScope('AssignmentListItemView')
@ -323,7 +325,28 @@ export default AssignmentListItemView = (function () {
}
const {attributes = {}} = this.model
const {assessment_requests: assessmentRequests} = attributes
const {assessment_requests: assessmentRequests, checkpoints} = attributes
if (checkpoints && checkpoints.length && !this.canManage()) {
const checkpointsElem =
this.$el.find(`#assignment_student_checkpoints_${this.model.id}`) ?? []
const mountPoint = checkpointsElem[0]
try {
ReactDOM.render(
React.createElement(ListViewCheckpoints, {
assignment: attributes,
}),
mountPoint
)
} catch (error) {
const errorMessage = I18n.t('Checkpoints mount point element not found')
// eslint-disable-next-line no-console
console.error(errorMessage, error)
captureException(new Error(errorMessage), error)
}
}
if (assessmentRequests && assessmentRequests.length) {
const peerReviewElem =
this.$el.find(`#assignment_student_peer_review_${this.model.id}`) ?? []

View File

@ -508,6 +508,7 @@
{{#if canManage}}<form data-view="edit-assignment" class="form-dialog"></form>{{/if}}
{{/if}}{{/if}}{{/if}}{{/if}}{{/if}}{{/if}}{{/if}}{{/if}}
</div>
<div id="assignment_student_checkpoints_{{id}}"></div>
<div id="assignment_student_peer_review_{{id}}">
<div id="assign-to-mount-point"></div>
</div>

View File

@ -0,0 +1,6 @@
{
"name": "@canvas/list-view-checkpoints",
"private": true,
"version": "1.0.0"
}

View File

@ -0,0 +1,129 @@
/*
* Copyright (C) 2024 - 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 {IconArrowNestLine} from '@instructure/ui-icons'
import {View} from '@instructure/ui-view'
import {useScope as useI18nScope} from '@canvas/i18n'
import type {Assignment, Checkpoint} from '../../../api.d'
const I18n = useI18nScope('assignment')
const REPLY_TO_TOPIC: string = 'reply_to_topic'
export type AssignmentCheckpoints = Pick<Assignment, 'id' | 'checkpoints' | 'discussion_topic'>
export type StudentViewCheckpointProps = {
assignment: AssignmentCheckpoints
}
export type CheckpointProps = {
assignment: AssignmentCheckpoints
checkpoint: Checkpoint
}
const createDateTimeFormatter = () => {
return Intl.DateTimeFormat(ENV.LOCALE, {
month: 'short',
day: 'numeric',
timeZone: ENV.TIMEZONE,
})
}
const dateFormatter = createDateTimeFormatter()
export const ListViewCheckpoints = ({assignment}: StudentViewCheckpointProps) => {
return (
<>
{assignment.checkpoints.map(checkpoint => (
<CheckpointItem
checkpoint={checkpoint}
assignment={assignment}
key={`${assignment.id}_${checkpoint.tag}`}
/>
))}
</>
)
}
const CheckpointItem = React.memo(({checkpoint, assignment}: CheckpointProps) => {
const getCheckpointDueDate = () => {
if (checkpoint.due_at) {
return dateFormatter.format(new Date(checkpoint.due_at))
}
// Once VICE-4350 is completed, modify this to find the due date from the checkpoint overrides
for (const override of checkpoint.overrides) {
if (
override.student_ids &&
ENV.current_user_id &&
override.student_ids.includes(ENV.current_user_id)
) {
return dateFormatter.format(new Date(override.due_at))
}
}
return I18n.t('No Due Date')
}
const renderCheckpointTitle = () => {
if (checkpoint.tag === REPLY_TO_TOPIC) {
return I18n.t('Reply To Topic')
} else {
// if it's not reply to topic, it must be reply to entry
const translatedReplyToEntryRequiredCount = I18n.n(
assignment.discussion_topic.reply_to_entry_required_count
)
return I18n.t('Required Replies (%{requiredReplies})', {
requiredReplies: translatedReplyToEntryRequiredCount,
})
}
}
return (
<li className="context_module_item student-view cannot-duplicate indent_1">
<div className="ig-row">
<div className="ig-row__layout">
<span className="type_icon display_icons" style={{fontSize: '1.125rem'}}>
<View as="span" margin="0 0 0 medium">
<IconArrowNestLine />
</View>
</span>
<div className="ig-info">
<span
style={{color: 'var(--ic-brand-font-color-dark)'}}
className="item_name ig-title title"
data-testid={`${assignment.id}_${checkpoint.tag}_title`}
>
{renderCheckpointTitle()}
</span>
<div className="ig-details">
<div
className="ig-details__item"
data-testid={`${assignment.id}_${checkpoint.tag}_due_date`}
>
{getCheckpointDueDate()}
</div>
</div>
</div>
</div>
</div>
</li>
)
})

View File

@ -0,0 +1,64 @@
/*
* Copyright (C) 2024 - 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 {render} from '@testing-library/react'
import {ListViewCheckpoints} from '../ListViewCheckpoints'
import {
checkpointedAssignmentNoDueDates,
checkpointedAssignmentWithDueDates,
checkpointedAssignmentWithOverrides,
} from './mocks'
describe('ListViewCheckpoints', () => {
it('renders the ListViewCheckpoints component with the correct checkpoint titles', async () => {
const {container, getByTestId} = render(
<ListViewCheckpoints {...checkpointedAssignmentNoDueDates} />
)
expect(container.querySelectorAll('li')).toHaveLength(2)
expect(getByTestId('1_reply_to_topic_title').textContent).toEqual('Reply To Topic')
expect(getByTestId('1_reply_to_entry_title').textContent).toEqual('Required Replies (4)')
})
describe('due date', () => {
it('renders the ListViewCheckpoints components with No Due Date if the checkpoint as no due date', () => {
const {getByTestId} = render(<ListViewCheckpoints {...checkpointedAssignmentNoDueDates} />)
expect(getByTestId('1_reply_to_topic_due_date').textContent).toEqual('No Due Date')
expect(getByTestId('1_reply_to_entry_due_date').textContent).toEqual('No Due Date')
})
it('renders the ListViewCheckpoints components with the formatted due dates if the checkpoint due_at field is populated', () => {
const {getByTestId} = render(<ListViewCheckpoints {...checkpointedAssignmentWithDueDates} />)
expect(getByTestId('1_reply_to_topic_due_date').textContent).toEqual('Jun 2')
expect(getByTestId('1_reply_to_entry_due_date').textContent).toEqual('Jun 4')
})
// Once VICE-4350 is completed, modify this test to reflect the new changes for finding the due date from the checkpoint overrides
it('renders the ListViewCheckpoints components with the formatted due dates if the checkpoint has overrides', () => {
ENV.current_user_id = '1'
const {getByTestId} = render(<ListViewCheckpoints {...checkpointedAssignmentWithOverrides} />)
expect(getByTestId('1_reply_to_topic_due_date').textContent).toEqual('Jun 2')
expect(getByTestId('1_reply_to_entry_due_date').textContent).toEqual('Jun 4')
})
})
})

View File

@ -0,0 +1,126 @@
/*
* Copyright (C) 2024 - 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/>.
*/
export const checkpointedAssignmentNoDueDates = {
assignment: {
id: '1',
course_id: '1',
name: 'Checkpoint Assignment',
checkpoints: [
{
due_at: null,
name: 'Checkpoint Assignment',
only_visible_to_overrides: false,
overrides: [],
points_possible: 1,
tag: 'reply_to_topic',
},
{
due_at: null,
name: 'Checkpoint Assignment',
only_visible_to_overrides: false,
overrides: [],
points_possible: 1,
tag: 'reply_to_entry',
},
],
discussion_topic: {
reply_to_entry_required_count: 4,
},
},
}
export const checkpointedAssignmentWithDueDates = {
assignment: {
id: '1',
course_id: '1',
name: 'Checkpoint Assignment',
checkpoints: [
{
due_at: '2024-06-02T17:43:13Z',
name: 'Checkpoint Assignment',
only_visible_to_overrides: false,
overrides: [],
points_possible: 1,
tag: 'reply_to_topic',
},
{
due_at: '2024-06-04T17:43:13Z',
name: 'Checkpoint Assignment',
only_visible_to_overrides: false,
overrides: [],
points_possible: 1,
tag: 'reply_to_entry',
},
],
discussion_topic: {
reply_to_entry_required_count: 4,
},
},
}
export const checkpointedAssignmentWithOverrides = {
assignment: {
id: '1',
course_id: '1',
name: 'Checkpoint Assignment',
checkpoints: [
{
due_at: null,
name: 'Checkpoint Assignment',
only_visible_to_overrides: false,
overrides: [
{
due_at: '2024-06-02T17:43:13Z',
student_ids: ['1'],
all_day: false,
all_day_date: '',
assignment_id: '1',
id: '1',
title: '',
unassign_item: false,
},
],
points_possible: 1,
tag: 'reply_to_topic',
},
{
due_at: null,
name: 'Checkpoint Assignment',
only_visible_to_overrides: false,
overrides: [
{
due_at: '2024-06-04T17:43:13Z',
student_ids: ['1'],
all_day: false,
all_day_date: '',
assignment_id: '1',
id: '1',
title: '',
unassign_item: false,
},
],
points_possible: 1,
tag: 'reply_to_entry',
},
],
discussion_topic: {
reply_to_entry_required_count: 4,
},
},
}