UNPKG

grading

Version:

Grading of student submissions, in particular programming tests.

359 lines (297 loc) 11.2 kB
import { Image } from "./gradingschema"; export const GRADE_VALUES = [ // 0 1 2 3 4 5 6 7 8 9 10 "5,0", "4,0", "3,7", "3,3", "3,0", "2,7", "2,3", "2,0", "1,7", "1,3", "1,0" // 50% ] export function gradeFromPoints(points: number, schema: number[]) { for (let note = GRADE_VALUES.length - 1; note > 0; note--) { if (points >= schema[note]) { return GRADE_VALUES[note]; } } return GRADE_VALUES[0]; } function gradingTable(pointsMax: number, requiredPoints?: number): number[] { if (!requiredPoints) { requiredPoints = Math.floor(pointsMax / 2); } const schema: number[] = []; const stepsBestanden = (pointsMax - (requiredPoints - 1)) / (GRADE_VALUES.length - 1); schema[0] = 0; // falls schlechter als 4 (4,3, 4,7 etc); nicht an der BHT // const stepsDurchgefallen = bestandenAb / 5; // for (let i = 0; i < 5; i++) { // schema[i] = Math.floor(stepsDurchgefallen * i); // } schema[1] = requiredPoints; for (let i = 2; i < GRADE_VALUES.length; i++) { schema[i] = requiredPoints + Math.floor(stepsBestanden * (i - 1)); } return schema; } export class Results { readonly studentResults: StudentResult[] = [] readonly gradingTable: number[]; readonly dateTime: number; appliedCorrections: number = 0; constructor( public readonly course: string, public readonly term: string, public readonly examName: string, public readonly maxPoints: number, public readonly minCoverageStatements: number = 0, public readonly penaltiesCoverageMax: number = 0, public readonly extraCoverageMax: number = 0, /** * How many points are necessary to pass the exam. */ public readonly requiredPoints?: number ) { this.gradingTable = gradingTable(maxPoints, requiredPoints); this.dateTime = Date.now(); } /** * * @param rawPoints Punkte ohne Coverage-Korrektur * @param stmtCoverage Statement-Coverage (des Studenten) * @returns Korrekturfaktor für Coverage; 0, wenn keine Coverage verlangt (minCoverageStatements undefined oder 0) */ coveragePunkte(rawPoints: number, stmtCoverage: number) { if (this.minCoverageStatements <= 0 || this.minCoverageStatements > 100 || this.minCoverageStatements == stmtCoverage) { return 0; } if (stmtCoverage < 0) { stmtCoverage = 0; } if (stmtCoverage > 100) { stmtCoverage = 100; } if (stmtCoverage < this.minCoverageStatements) { const factor = Math.max(0, stmtCoverage) / this.minCoverageStatements; // penaltiesCoverageMax = 0 means: no penalty const deductionMax = Math.min(this.penaltiesCoverageMax, this.maxPoints); const deduction = (1 - factor) * deductionMax; return Math.max(-rawPoints, -Math.floor(deduction)); } else { // stmtCoverage>this.minCoverageStatements if (this.extraCoverageMax > 0) { const factor = (stmtCoverage - this.minCoverageStatements) / (100 - this.minCoverageStatements); const extra = this.extraCoverageMax * factor; return Math.min(rawPoints, Math.ceil(extra)); } } return 0; } } class Skippable { skipMessage: string | null = null; skipped(): this is { skipMessage: string } { return this.skipMessage ? true : false; } } /** * Points may be 0 if only a comment is added. */ export class ManualCorrection { points: number; reason: string|string[]; absolute: boolean; constructor(points: number | undefined, reason: string|string[], absolute: boolean) { if (!points) { this.points = 0; } else { this.points = points; } this.reason = reason; this.absolute = absolute; } } export class StudentResult extends Skippable { readonly taskResults: TaskResult[] = []; readonly coverageResult = new CoverageResult(); noSubmission: boolean = false; checkReportFound = false; studentReportFound = false; gradingDone = false; readonly penaltyResults: GradingResult[] = [] manualCorrection: ManualCorrection[] = []; errorDetails: string = ""; /** * sum of points of tasks including manual corrections, computed in computeAndSetRawPunkte */ pointsRaw: number = 0; /** * actual coverage points, can only computed when rawPoints have been set */ pointsCoverage: number = 0; /** * points raw + point coverage */ points: number = 0; /** * grade (1-5, no 4,3 and 4,7), can only be computed when points have been set. */ grade: string = ""; /** * Set to true if manual correction changed the points using absolute points. */ absolutePoints = false; userTests: boolean = false; userTestsSuccess: number = 0; userTestsFailureOrError: number = 0; userTestsCount: number = 0; comment =""; patched = false; constructor(public readonly submissionId: string, public readonly userName: string) { super(); } addComment(comment: string) { if (this.comment.length > 0) { this.comment += " "; } this.comment += comment; } addPenalty(gradingResult: GradingResult) { this.penaltyResults.push(gradingResult); } /** * @returns Returns true, if any penalities exist (i.e. taking actual results into account). */ hasPenalties() { let penaltyPoints = this.penaltyResults.reduce((sum, bew) => sum + bew.points, 0); return penaltyPoints != 0 } computeAndSetRawPunkte() { this.pointsRaw = this.taskResults.reduce( (sum, taskResult) => { // compute pointsRaw for each task even if it skipped as it may contain manual corrections taskResult.computeAndSetRawPoints(); return sum + taskResult.points; }, 0) if (this.penaltyResults.length > 0) { this.penaltyResults.forEach(gradingResult => gradingResult.computeAndSetRawPoints()); this.pointsRaw += this.penaltyResults.reduce((sum, gradingResult) => sum + gradingResult.points, 0) } this.manualCorrection.forEach(manualCorrection => this.pointsRaw += manualCorrection.points); if (this.pointsRaw < 0) { this.pointsRaw = 0; } } get pointsMax() { return this.taskResults.reduce( (sum, task) => sum + task.pointsMax, 0 ) } } export class CoverageResult extends Skippable { public stmtCoverage: number = 0; constructor() { super(); } } type ConditionStatus = "none" | "failed" | "success" | "notFound"; export class TaskResult extends Skippable { readonly name: string; readonly pointsMax: number readonly gradingResults: GradingResult[] = [] readonly penaltyResults: GradingResult[] = [] points = 0; manualCorrections: ManualCorrection[] = []; images: Image[] = []; message: string | null = null; /** for statistics */ precondition: ConditionStatus = "none"; constructor(name: string, pointsMax: number) { super(); this.name = name; this.pointsMax = pointsMax; } addGradingResult(gradingResult: GradingResult) { this.gradingResults.push(gradingResult); } addPenalty(gradingResult: GradingResult) { this.penaltyResults.push(gradingResult); } /** * @returns Returns true, if any penalities exist (i.e. taking actual results into account). */ hasPenalties() { let penaltyPoints = this.penaltyResults.reduce((sum, bew) => sum + bew.points, 0); return penaltyPoints != 0 } computeAndSetRawPoints() { this.points = 0; if (!this.skipped()) { if (this.gradingResults.length > 0) { this.gradingResults.forEach(gradingResult => gradingResult.computeAndSetRawPoints()); this.points = this.gradingResults.reduce((sum, gradingResult) => sum + gradingResult.points, 0); } if (this.penaltyResults.length > 0) { this.penaltyResults.forEach(gradingResult => gradingResult.computeAndSetRawPoints()); this.points += this.penaltyResults.reduce((sum, gradingResult) => sum + gradingResult.points, 0) } } this.manualCorrections.forEach(correction => this.points += correction.points); if (this.points < 0) { // minimum 0 points per task this.points = 0; } } } export class GradingResult extends Skippable { readonly testResults: TestResult[] = [] points = 0; precondition?: string; images: Image[] = [] message: string | null = null; /** * If the grading is negative (i.e., {@link pointsMax} is less 0), the grading is a penalty. */ constructor(public readonly text: string, public readonly pointsMax: number, public isBonus?: boolean) { super(); } addTestResult(text: string, testStatus: TestStatus, message: string | undefined) { this.testResults.push(new TestResult(text, testStatus, message)); } /** * If the grading is negative (i.e., {@link pointsMax} is less 0), the grading is a penalty. */ get isPenalty() { return this.pointsMax < 0; } /** * Computes raw points (and sets them internally to field {@link points}). * * If the grading is positive, the number of successfully passed tests is used to compute the raw points. * If the grading is a penalty (i.e. grading is negative) the number of failed tests are used instead. */ computeAndSetRawPoints() { this.points = 0; if (!this.isPenalty) { if (this.skipped()) { return; } const successCount = this.testResults.filter(test => test.testStatus === "success").length; if (this.testResults.length == 0) { console.error("Keine Testergebnisse verzeichnet, setze Punkte für Bewertung " + this.text + " auf 0.") this.points = 0; } else { this.points = Math.ceil(this.pointsMax * successCount / this.testResults.length); } } else { const failureCount = this.testResults.filter(test => test.testStatus !== "success").length; if (this.testResults.length == 0) { this.points = 0; } else { this.points = Math.floor(this.pointsMax * failureCount / this.testResults.length); } } } } export type TestStatus = "success" | "failure" | "error" | "skipped" | "notFound"; export class TestResult { constructor( public text: string, public testStatus: TestStatus, public message: string | undefined) { } }