Skip to content

Commit

Permalink
Add button to sync grades
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed Sep 13, 2024
1 parent 1e9aa48 commit 3c6876b
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 35 deletions.
7 changes: 7 additions & 0 deletions lms/static/scripts/frontend_apps/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,10 @@ export type StudentsResponse = {
students: Student[];
pagination: Pagination;
};

/**
* Response for `/api/dashboard/assignments/{assignment_id}/grading/sync`
*/
export type GradingSync = {
status: 'scheduled' | 'in_progress' | 'finished' | 'failed';
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import FormattedDate from './FormattedDate';
import GradeIndicator from './GradeIndicator';
import type { OrderableActivityTableColumn } from './OrderableActivityTable';
import OrderableActivityTable from './OrderableActivityTable';
import SyncGradesButton from './SyncGradesButton';

type StudentsTableRow = {
lms_id: string;
Expand All @@ -33,7 +34,7 @@ type StudentsTableRow = {
*/
export default function AssignmentActivity() {
const { dashboard } = useConfig(['dashboard']);
const { routes } = dashboard;
const { routes, user } = dashboard;
const { assignmentId, organizationPublicId } = useParams<{
assignmentId: string;
organizationPublicId?: string;
Expand All @@ -58,13 +59,32 @@ export default function AssignmentActivity() {
},
);

const studentsToSync = useMemo(() => {
if (!autoGradingEnabled) {
return [];
}

return (students.data?.students ?? []).map(
({ h_userid, auto_grading_grade = 0 }) => ({
h_userid,
grade: auto_grading_grade,
}),
);
}, [autoGradingEnabled, students.data?.students]);
const rows: StudentsTableRow[] = useMemo(
() =>
(students.data?.students ?? []).map(
({ lms_id, display_name, auto_grading_grade, annotation_metrics }) => ({
({
lms_id,
display_name,
auto_grading_grade,
h_userid,
annotation_metrics,
}) => ({
lms_id,
display_name,
auto_grading_grade,
h_userid,
...annotation_metrics,
}),
),
Expand Down Expand Up @@ -133,40 +153,45 @@ export default function AssignmentActivity() {
{assignment.data && title}
</h2>
</div>
{assignment.data && (
<DashboardActivityFilters
courses={{
activeItem: assignment.data.course,
// When the active course is cleared, navigate to home, but keep
// active assignment and students
onClear: () =>
navigate(
urlWithFilters(
{ studentIds, assignmentIds: [assignmentId] },
{ path: '' },
<div className="flex justify-between items-end gap-x-4">
{assignment.data && (
<DashboardActivityFilters
courses={{
activeItem: assignment.data.course,
// When the active course is cleared, navigate to home, but keep
// active assignment and students
onClear: () =>
navigate(
urlWithFilters(
{ studentIds, assignmentIds: [assignmentId] },
{ path: '' },
),
),
),
}}
assignments={{
activeItem: assignment.data,
// When active assignment is cleared, navigate to its course page,
// but keep other query params intact
onClear: () => {
const query = search.length === 0 ? '' : `?${search}`;
navigate(`${courseURL(assignment.data!.course.id)}${query}`);
},
}}
students={{
selectedIds: studentIds,
onChange: studentIds => updateFilters({ studentIds }),
}}
onClearSelection={
studentIds.length > 0
? () => updateFilters({ studentIds: [] })
: undefined
}
/>
)}
}}
assignments={{
activeItem: assignment.data,
// When active assignment is cleared, navigate to its course page,
// but keep other query params intact
onClear: () => {
const query = search.length === 0 ? '' : `?${search}`;
navigate(`${courseURL(assignment.data!.course.id)}${query}`);
},
}}
students={{
selectedIds: studentIds,
onChange: studentIds => updateFilters({ studentIds }),
}}
onClearSelection={
studentIds.length > 0
? () => updateFilters({ studentIds: [] })
: undefined
}
/>
)}
{autoGradingEnabled && !user.is_staff && (
<SyncGradesButton studentsToSync={studentsToSync} />
)}
</div>
<OrderableActivityTable
loading={students.isLoading}
title={assignment.isLoading ? 'Loading...' : title}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Button } from '@hypothesis/frontend-shared';
import { useCallback, useMemo } from 'preact/hooks';
import { useParams } from 'wouter-preact';

import type { GradingSync } from '../../api-types';
import { useConfig } from '../../config';
import { useAPIFetch } from '../../utils/api';
import { replaceURLParams } from '../../utils/url';

export type SyncGradesButtonProps = {
/**
* List of students and their grades, which should be synced when the button
* is clicked.
*/
studentsToSync: Array<{ h_userid: string; grade: number }>;
};

export default function SyncGradesButton({
studentsToSync,
}: SyncGradesButtonProps) {
const { assignmentId } = useParams<{ assignmentId: string }>();
const { dashboard } = useConfig(['dashboard']);
const { routes } = dashboard;

const syncStatus = useAPIFetch<GradingSync>(
replaceURLParams(routes.assignment_grades_sync, {
assignment_id: assignmentId,
}),
);
const buttonContent = useMemo(() => {
if (syncStatus.isLoading || !syncStatus.data) {
return 'Loading...';
}

const { status } = syncStatus.data;
if (['scheduled', 'in_progress'].includes(status)) {
return 'Syncing grades';
}

if (status === 'failed') {
return 'Error syncing';
}

if (studentsToSync.length > 0) {
return `Sync ${studentsToSync.length} students`;
}

return 'Grades synced';
}, [studentsToSync.length, syncStatus.data, syncStatus.isLoading]);
const buttonDisabled = useMemo(() => {
// TODO This needs more checks, and there are more reasons for the button
// to be disabled
return syncStatus.isLoading || studentsToSync.length === 0;
}, [studentsToSync.length, syncStatus.isLoading]);

const syncGrades = useCallback(() => {
console.log(studentsToSync);
}, [studentsToSync]);

return (
<Button variant="primary" onClick={syncGrades} disabled={buttonDisabled}>
{buttonContent}
</Button>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ describe('AssignmentActivity', () => {
assignment: '/api/assignments/:assignment_id',
students_metrics: '/api/students/metrics',
},
user: {
is_staff: true,
},
},
};

Expand Down Expand Up @@ -390,6 +393,25 @@ describe('AssignmentActivity', () => {
});
},
);

[
{ isStaff: true, autoGradingEnabled: true, shouldShowButton: false },
{ isStaff: false, autoGradingEnabled: true, shouldShowButton: true },
{ isStaff: true, autoGradingEnabled: false, shouldShowButton: false },
{ isStaff: false, autoGradingEnabled: false, shouldShowButton: false },
].forEach(({ autoGradingEnabled, isStaff, shouldShowButton }) => {
it('shows sync button for non-staff users only', () => {
setUpFakeUseAPIFetch({
...activeAssignment,
auto_grading_config: autoGradingEnabled ? {} : null,
});
fakeConfig.dashboard.user.is_staff = isStaff;

const wrapper = createComponent();

assert.equal(wrapper.exists('SyncGradesButton'), shouldShowButton);
});
});
});

it(
Expand Down
3 changes: 3 additions & 0 deletions lms/static/scripts/frontend_apps/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,9 @@ export type DashboardRoutes = {
assignments: string;
/** Fetch list of students */
students: string;

/** Sync grades (POST) or check sync status (GET) */
assignment_grades_sync: string;
};

export type DashboardUser = {
Expand Down

0 comments on commit 3c6876b

Please sign in to comment.