UNPKG

grading

Version:

Grading of student submissions, in particular programming tests.

204 lines (176 loc) 7.97 kB
import { type } from "os"; import { GradingSchema, gradingText } from "./gradingschema"; import { GradingResult, Results, StudentResult } from "./results"; export type ItemType = 'exam' | 'task' | 'grading' | 'test'; function subItemType(itemType: ItemType) { const ITEM_TYPE_NAMES = ['exam', 'task', 'grading', 'test']; const current = ITEM_TYPE_NAMES.indexOf(itemType); if (current < 0) { return "unknown"; } if (current == ITEM_TYPE_NAMES.length - 1) { return "subtest(?!)" } return ITEM_TYPE_NAMES[current + 1]; } export function computeStatistics(gradingSchema: GradingSchema, results: Results) { const examStatistics = new ItemStatistics(gradingSchema.exam, gradingSchema.exam, gradingSchema.points, 'exam'); const statistics = new Statistics(examStatistics); statistics.appliedCorrections = results.appliedCorrections; // Generate overall scheme gradingSchema.tasks.forEach((task, taskIndex) => { const taskLabel = (taskIndex + 1) + ". " + task.name; const taskStatistic = examStatistics.addItemStatistics(new ItemStatistics(taskLabel, taskLabel, task.points, 'task')) task.grading.forEach((grading, gradingIndex) => { const gradingLabel = (taskIndex + 1) + "." + (gradingIndex + 1) + "."; const gradingTitle = (taskIndex + 1) + "." + (gradingIndex + 1) + ". " + gradingText(grading); const gradingStatistics = taskStatistic.addItemStatistics(new ItemStatistics(gradingLabel, gradingTitle, grading.points, 'grading')); grading.tests.forEach((test, testIndex) => { const testLabel = gradingLabel + (testIndex + 1) + "."; const testTitle = gradingLabel + (testIndex + 1) + ". " + test; gradingStatistics.addItemStatistics(new ItemStatistics(testLabel, testTitle, 1, 'test')) }) }) }) // Fill data results.studentResults.forEach((studentResult, studentIndex) => { statistics.registerSubmission(studentResult.coverageResult.stmtCoverage, studentResult.userTestsCount); examStatistics.registerSubmission(studentResult.pointsRaw, studentResult.skipped()); studentResult.taskResults.forEach((taskResult, taskIndex) => { const taskLabel = (taskIndex + 1) + ". " + taskResult.name; const taskStatistic = examStatistics.getItemStatistics(taskIndex, taskLabel, studentResult); taskStatistic.registerSubmission(taskResult.points, taskResult.skipped()); taskResult.gradingResults.forEach((gradingResult, gradingIndex) => { const gradingLabel = (taskIndex + 1) + "." + (gradingIndex + 1) + "."; const gradingStatistics = taskStatistic.getItemStatistics(gradingIndex, gradingLabel); gradingStatistics.registerSubmission(gradingResult.points, gradingResult.skipped()); gradingResult.testResults.forEach((testResult, testIndex) => { const testLabel = gradingLabel + (testIndex + 1) + "."; const testStatistic = gradingStatistics.getItemStatistics(testIndex, testLabel); testStatistic.registerSubmission(testResult.testStatus === 'success' ? 1 : 0, testResult.testStatus === 'notFound'); }); }); }); }); return statistics; } export class Statistics { readonly examStatistics: ItemStatistics | undefined; /** * All submissions, should be similar to number of submitters. * Includes skipped items. */ submissions: number = 0; sumCoverage: number = 0; sumStudentTests: number = 0; appliedCorrections: number = 0; constructor(examStatistics: ItemStatistics) { this.examStatistics = examStatistics; } registerSubmission(coverage: number, studentTests: number) { this.submissions++; this.sumCoverage += coverage; this.sumStudentTests += studentTests; } get averageCoverage() { if (this.submissions == 0) { throw new Error("Cannot compute average coverage, no submissions registered"); } return this.sumCoverage / this.submissions; } get averageStudentTests() { if (this.submissions == 0) { throw new Error("Cannot compute average student tests, no submissions registered"); } return this.sumStudentTests / this.submissions; } *[Symbol.iterator]() { const stack: ItemStatistics[] = []; if (this.examStatistics) { stack.unshift(this.examStatistics); } while (stack.length > 0) { const current = stack.shift()!; yield current; if (current.itemStatistics.length != 0) { stack.unshift(...current.itemStatistics); } } } } export class ItemStatistics { readonly label: string; readonly title: string; readonly pointsMax: number; readonly itemType: ItemType; /** * Subitems, i.e. exam->tasks, tasks->tests, tests->none */ readonly itemStatistics: ItemStatistics[] = [] /** * All submissions, should be similar to number of submitters. * Includes skipped items. */ submissions: number = 0; sumGainedPoints: number = 0; skippedCount: number = 0; constructor(label: string, title: string, pointsMax: number, itemType: ItemType) { this.label = label; this.title = title; this.pointsMax = pointsMax; this.itemType = itemType; if (this.pointsMax < 0) { throw new Error(`Inconsistency for ${this.description}, maximum points must be greater or equal 0.`) } } addItemStatistics(itemStatistics: ItemStatistics) { this.itemStatistics.push(itemStatistics); return itemStatistics; } /** * Returns the statistics for the given subitem by index. * Since the item is added for the first student, errors here means that a student has something no one else had before. */ getItemStatistics(index: number, label: string, studentResult?: StudentResult) { const itemStatistics = this.itemStatistics[index]; if (!itemStatistics) { throw new Error(`Inconsistency in ${this.description} found: No ${subItemType(this.itemType)} at index ${index} ('${label}')${studentResult ? " for " + studentResult.userName : ""}.`) } if (itemStatistics.label !== label) { throw new Error(`Inconsistency in ${this.description} found: ${itemStatistics.description} with different name (${label}) at index ${index}${studentResult ? " for " + studentResult.userName : ""}.`); } return itemStatistics; } registerSubmission(points: number, skipped: boolean) { this.submissions++; this.sumGainedPoints += points; if (skipped) { this.skippedCount++; } } get successRate() { if (this.submissions == 0) { if (this.itemType === 'exam') { throw new Error(`Cannot compute success rate for ${this.description}, no submissions registered`); } else { // may happen if no student has submitted this item return 0; } } const averagePoints = this.sumGainedPoints / this.submissions; const successRate = averagePoints / this.pointsMax; return successRate; } get successRateNonSkipped() { if (this.submissions == 0) { throw new Error(`Cannot compute success rate (non skipped) for ${this.itemType} ${this.label}, no submissions registered`); } const nonSkipped = this.submissions - this.skippedCount; if (nonSkipped == 0) { return 0; } return this.sumGainedPoints / nonSkipped; } get description() { return `${this.itemType} '${this.label}'`; } }