diff --git a/lms/static/scripts/frontend_apps/api-types.ts b/lms/static/scripts/frontend_apps/api-types.ts index ab8d82b1d6..726503bdeb 100644 --- a/lms/static/scripts/frontend_apps/api-types.ts +++ b/lms/static/scripts/frontend_apps/api-types.ts @@ -192,6 +192,7 @@ export type Student = { export type StudentWithMetrics = Student & { annotation_metrics: AnnotationMetrics; + auto_grading_grade?: number; }; /** diff --git a/lms/static/scripts/frontend_apps/components/dashboard/AssignmentActivity.tsx b/lms/static/scripts/frontend_apps/components/dashboard/AssignmentActivity.tsx index 141709db91..cf27d2b372 100644 --- a/lms/static/scripts/frontend_apps/components/dashboard/AssignmentActivity.tsx +++ b/lms/static/scripts/frontend_apps/components/dashboard/AssignmentActivity.tsx @@ -14,6 +14,8 @@ import { replaceURLParams } from '../../utils/url'; import DashboardActivityFilters from './DashboardActivityFilters'; import DashboardBreadcrumbs from './DashboardBreadcrumbs'; import FormattedDate from './FormattedDate'; +import GradeStatusChip from './GradeStatusChip'; +import type { OrderableActivityTableColumn } from './OrderableActivityTable'; import OrderableActivityTable from './OrderableActivityTable'; type StudentsTableRow = { @@ -22,6 +24,7 @@ type StudentsTableRow = { last_activity: string | null; annotations: number; replies: number; + auto_grading_grade?: number; }; /** @@ -43,6 +46,7 @@ export default function AssignmentActivity() { const assignment = useAPIFetch( replaceURLParams(routes.assignment, { assignment_id: assignmentId }), ); + const autoGradingEnabled = !!assignment.data?.auto_grading_config; const students = useAPIFetch( routes.students_metrics, @@ -56,14 +60,49 @@ export default function AssignmentActivity() { const rows: StudentsTableRow[] = useMemo( () => (students.data?.students ?? []).map( - ({ lms_id, display_name, annotation_metrics }) => ({ + ({ lms_id, display_name, auto_grading_grade, annotation_metrics }) => ({ lms_id, display_name, + auto_grading_grade, ...annotation_metrics, }), ), [students.data], ); + const columns = useMemo(() => { + const firstColumns: OrderableActivityTableColumn[] = [ + { + field: 'display_name', + label: 'Student', + }, + ]; + const lastColumns: OrderableActivityTableColumn[] = [ + { + field: 'annotations', + label: 'Annotations', + initialOrderDirection: 'descending', + }, + { + field: 'replies', + label: 'Replies', + initialOrderDirection: 'descending', + }, + { + field: 'last_activity', + label: 'Last Activity', + initialOrderDirection: 'descending', + }, + ]; + + if (autoGradingEnabled) { + firstColumns.push({ + field: 'auto_grading_grade', + label: 'Grade', + }); + } + + return [...firstColumns, ...lastColumns]; + }, [autoGradingEnabled]); const title = assignment.data?.title ?? 'Untitled assignment'; useDocumentTitle(title); @@ -134,27 +173,7 @@ export default function AssignmentActivity() { students.error ? 'Could not load students' : 'No students found' } rows={rows} - columns={[ - { - field: 'display_name', - label: 'Student', - }, - { - field: 'annotations', - label: 'Annotations', - initialOrderDirection: 'descending', - }, - { - field: 'replies', - label: 'Replies', - initialOrderDirection: 'descending', - }, - { - field: 'last_activity', - label: 'Last Activity', - initialOrderDirection: 'descending', - }, - ]} + columns={columns} defaultOrderField="display_name" renderItem={(stats, field) => { switch (field) { @@ -179,6 +198,8 @@ export default function AssignmentActivity() { ) ); + case 'auto_grading_grade': + return ; default: return ''; } diff --git a/lms/static/scripts/frontend_apps/components/dashboard/OrderableActivityTable.tsx b/lms/static/scripts/frontend_apps/components/dashboard/OrderableActivityTable.tsx index f03e444620..22cfb03d23 100644 --- a/lms/static/scripts/frontend_apps/components/dashboard/OrderableActivityTable.tsx +++ b/lms/static/scripts/frontend_apps/components/dashboard/OrderableActivityTable.tsx @@ -2,6 +2,7 @@ import type { DataTableProps, Order } from '@hypothesis/frontend-shared'; import { DataTable } from '@hypothesis/frontend-shared'; import { useOrderedRows } from '@hypothesis/frontend-shared'; import type { OrderDirection } from '@hypothesis/frontend-shared/lib/types'; +import classnames from 'classnames'; import { useMemo, useState } from 'preact/hooks'; import { useLocation } from 'wouter-preact'; @@ -46,7 +47,13 @@ export default function OrderableActivityTable({ columns.map(({ field, label }, index) => ({ field, label, - classes: index === 0 ? 'lg:w-[60%] md:w-[45%]' : undefined, + classes: classnames({ + // For assignments with auto-grading, a fifth column is displayed. + // In that case, we need to reserve less space for the first column, + // otherwise the rest overflow. + 'lg:w-[60%] md:w-[45%]': index === 0 && columns.length < 5, + 'lg:w-[45%] md:w-[30%]': index === 0 && columns.length >= 5, + }), })), [columns], ); diff --git a/lms/static/scripts/frontend_apps/components/dashboard/test/AssignmentActivity-test.js b/lms/static/scripts/frontend_apps/components/dashboard/test/AssignmentActivity-test.js index 8a05735832..0472e43fb4 100644 --- a/lms/static/scripts/frontend_apps/components/dashboard/test/AssignmentActivity-test.js +++ b/lms/static/scripts/frontend_apps/components/dashboard/test/AssignmentActivity-test.js @@ -52,11 +52,17 @@ describe('AssignmentActivity', () => { let fakeConfig; let wrappers; - beforeEach(() => { - fakeUseAPIFetch = sinon.stub().callsFake(url => ({ + function setUpFakeUseAPIFetch(assignment = activeAssignment) { + fakeUseAPIFetch.callsFake(url => ({ isLoading: false, - data: url.endsWith('metrics') ? { students } : activeAssignment, + data: url.endsWith('metrics') ? { students } : assignment, })); + } + + beforeEach(() => { + fakeUseAPIFetch = sinon.stub(); + setUpFakeUseAPIFetch(); + fakeNavigate = sinon.stub(); fakeUseSearch = sinon.stub().returns('current=query'); fakeConfig = { @@ -297,6 +303,94 @@ describe('AssignmentActivity', () => { }); }); + context('when auto-grading is enabled', () => { + [ + { + autoGradingEnabled: false, + expectedColumns: [ + { + field: 'display_name', + label: 'Student', + }, + { + field: 'annotations', + label: 'Annotations', + initialOrderDirection: 'descending', + }, + { + field: 'replies', + label: 'Replies', + initialOrderDirection: 'descending', + }, + { + field: 'last_activity', + label: 'Last Activity', + initialOrderDirection: 'descending', + }, + ], + }, + { + autoGradingEnabled: true, + expectedColumns: [ + { + field: 'display_name', + label: 'Student', + }, + { + field: 'auto_grading_grade', + label: 'Grade', + }, + { + field: 'annotations', + label: 'Annotations', + initialOrderDirection: 'descending', + }, + { + field: 'replies', + label: 'Replies', + initialOrderDirection: 'descending', + }, + { + field: 'last_activity', + label: 'Last Activity', + initialOrderDirection: 'descending', + }, + ], + }, + ].forEach(({ autoGradingEnabled, expectedColumns }) => { + it('shows one more column in the metrics table', () => { + setUpFakeUseAPIFetch({ + ...activeAssignment, + auto_grading_config: autoGradingEnabled ? {} : null, + }); + + const wrapper = createComponent(); + const tableElement = wrapper.find('OrderableActivityTable'); + + assert.deepEqual(tableElement.prop('columns'), expectedColumns); + }); + }); + + [{ auto_grading_grade: undefined }, { auto_grading_grade: 25 }].forEach( + ({ auto_grading_grade }) => { + it('shows the grade for every student', () => { + setUpFakeUseAPIFetch({ + ...activeAssignment, + auto_grading_config: {}, + }); + + const wrapper = createComponent(); + const item = wrapper + .find('OrderableActivityTable') + .props() + .renderItem({ auto_grading_grade }, 'auto_grading_grade'); + + assert.equal(mount(item).prop('grade'), auto_grading_grade ?? 0); + }); + }, + ); + }); + it( 'should pass a11y checks', checkAccessibility({