Files
classroomio/apps/dashboard/src/lib/features/course/utils/exercise-progression-utils.ts
T

107 lines
4.1 KiB
TypeScript
Raw Normal View History

import { isAutoGradableQuestionTypeId } from '@cio/question-types';
import type { Question } from '$features/course/types';
import type { SubmissionListItem } from '$features/course/utils/types';
export type ExerciseCompletionPolicy = 'submitted' | 'passed';
export type ExerciseAttempt = {
attemptNumber: number;
submission: SubmissionListItem;
};
export type ExerciseSubmissionStudentGroup = {
studentKey: string;
studentName: string;
studentAvatarUrl: string;
attempts: ExerciseAttempt[];
};
export function getActiveExerciseQuestions(questions: Question[]): Question[] {
return questions.filter((question) => !question.deletedAt);
}
export function getTotalPossibleExercisePoints(questions: Question[]): number {
return getActiveExerciseQuestions(questions).reduce((total, question) => total + Number(question.points ?? 0), 0);
}
export function getManualGradedExerciseQuestions(questions: Question[]): Question[] {
return getActiveExerciseQuestions(questions).filter((question) => {
const questionTypeId = Number(question.questionTypeId ?? question.questionType?.id);
if (!Number.isFinite(questionTypeId)) return true;
return !isAutoGradableQuestionTypeId(questionTypeId);
});
}
function getSubmissionScore(submission: SubmissionListItem): number {
const submissionTotal = Number((submission as SubmissionListItem & { total?: number | string | null }).total ?? NaN);
if (Number.isFinite(submissionTotal)) return submissionTotal;
return (submission.answers ?? []).reduce((total, answer) => total + Number(answer.point ?? 0), 0);
}
export function shouldCompleteExerciseFromSubmission(params: {
completionPolicy: ExerciseCompletionPolicy | null | undefined;
passThreshold: number | null | undefined;
totalPossiblePoints: number;
submission: SubmissionListItem & { gradingState?: string | null };
}): boolean {
if ((params.completionPolicy ?? 'submitted') === 'submitted') return true;
if (params.submission.gradingState !== 'completed') return false;
if (params.totalPossiblePoints <= 0) return false;
const threshold = Number(params.passThreshold ?? 100);
const scorePercent = (getSubmissionScore(params.submission) / params.totalPossiblePoints) * 100;
return scorePercent >= threshold;
}
function getSubmissionTimestamp(submission: SubmissionListItem): number {
const createdAt = (submission as SubmissionListItem & { createdAt?: string | Date | null }).createdAt;
const timestamp = createdAt ? new Date(createdAt).getTime() : NaN;
return Number.isFinite(timestamp) ? timestamp : 0;
}
function getStudentKey(submission: SubmissionListItem): string {
const profile = submission.groupmember?.profile;
const firstAnswerGroupMemberId = submission.answers?.find((answer) => answer.groupMemberId)?.groupMemberId;
return String(profile?.id ?? firstAnswerGroupMemberId ?? submission.id);
}
export function groupSubmissionsByStudentAndAttempt(
submissionItems: SubmissionListItem[]
): ExerciseSubmissionStudentGroup[] {
const groupByStudent = new Map<string, SubmissionListItem[]>();
for (const submission of submissionItems) {
const studentKey = getStudentKey(submission);
const studentSubmissions = groupByStudent.get(studentKey) ?? [];
studentSubmissions.push(submission);
groupByStudent.set(studentKey, studentSubmissions);
}
return [...groupByStudent.entries()]
.map(([studentKey, studentSubmissions]) => {
const orderedSubmissions = [...studentSubmissions].sort(
(leftSubmission, rightSubmission) =>
getSubmissionTimestamp(leftSubmission) - getSubmissionTimestamp(rightSubmission)
);
const latestSubmission = orderedSubmissions[orderedSubmissions.length - 1];
const profile = latestSubmission?.groupmember?.profile;
return {
studentKey,
studentName: profile?.fullname ?? '',
studentAvatarUrl: profile?.avatarUrl ?? '',
attempts: orderedSubmissions.map((submission, index) => ({
attemptNumber: index + 1,
submission
}))
};
})
.sort((leftGroup, rightGroup) => leftGroup.studentName.localeCompare(rightGroup.studentName));
}