UNPKG

grading

Version:

Grading of student submissions, in particular programming tests.

307 lines 11.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestResult = exports.GradingResult = exports.TaskResult = exports.CoverageResult = exports.StudentResult = exports.ManualCorrection = exports.Results = exports.gradeFromPoints = exports.GRADE_VALUES = void 0; exports.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% ]; function gradeFromPoints(points, schema) { for (let note = exports.GRADE_VALUES.length - 1; note > 0; note--) { if (points >= schema[note]) { return exports.GRADE_VALUES[note]; } } return exports.GRADE_VALUES[0]; } exports.gradeFromPoints = gradeFromPoints; function gradingTable(pointsMax, requiredPoints) { if (!requiredPoints) { requiredPoints = Math.floor(pointsMax / 2); } const schema = []; const stepsBestanden = (pointsMax - (requiredPoints - 1)) / (exports.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 < exports.GRADE_VALUES.length; i++) { schema[i] = requiredPoints + Math.floor(stepsBestanden * (i - 1)); } return schema; } class Results { constructor(course, term, examName, maxPoints, minCoverageStatements = 0, penaltiesCoverageMax = 0, extraCoverageMax = 0, /** * How many points are necessary to pass the exam. */ requiredPoints) { this.course = course; this.term = term; this.examName = examName; this.maxPoints = maxPoints; this.minCoverageStatements = minCoverageStatements; this.penaltiesCoverageMax = penaltiesCoverageMax; this.extraCoverageMax = extraCoverageMax; this.requiredPoints = requiredPoints; this.studentResults = []; this.appliedCorrections = 0; 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, stmtCoverage) { 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; } } exports.Results = Results; class Skippable { constructor() { this.skipMessage = null; } skipped() { return this.skipMessage ? true : false; } } /** * Points may be 0 if only a comment is added. */ class ManualCorrection { constructor(points, reason, absolute) { if (!points) { this.points = 0; } else { this.points = points; } this.reason = reason; this.absolute = absolute; } } exports.ManualCorrection = ManualCorrection; class StudentResult extends Skippable { constructor(submissionId, userName) { super(); this.submissionId = submissionId; this.userName = userName; this.taskResults = []; this.coverageResult = new CoverageResult(); this.noSubmission = false; this.checkReportFound = false; this.studentReportFound = false; this.gradingDone = false; this.penaltyResults = []; this.manualCorrection = []; this.errorDetails = ""; /** * sum of points of tasks including manual corrections, computed in computeAndSetRawPunkte */ this.pointsRaw = 0; /** * actual coverage points, can only computed when rawPoints have been set */ this.pointsCoverage = 0; /** * points raw + point coverage */ this.points = 0; /** * grade (1-5, no 4,3 and 4,7), can only be computed when points have been set. */ this.grade = ""; /** * Set to true if manual correction changed the points using absolute points. */ this.absolutePoints = false; this.userTests = false; this.userTestsSuccess = 0; this.userTestsFailureOrError = 0; this.userTestsCount = 0; this.comment = ""; this.patched = false; } addComment(comment) { if (this.comment.length > 0) { this.comment += " "; } this.comment += comment; } addPenalty(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); } } exports.StudentResult = StudentResult; class CoverageResult extends Skippable { constructor() { super(); this.stmtCoverage = 0; } } exports.CoverageResult = CoverageResult; class TaskResult extends Skippable { constructor(name, pointsMax) { super(); this.gradingResults = []; this.penaltyResults = []; this.points = 0; this.manualCorrections = []; this.images = []; this.message = null; /** for statistics */ this.precondition = "none"; this.name = name; this.pointsMax = pointsMax; } addGradingResult(gradingResult) { this.gradingResults.push(gradingResult); } addPenalty(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; } } } exports.TaskResult = TaskResult; class GradingResult extends Skippable { /** * If the grading is negative (i.e., {@link pointsMax} is less 0), the grading is a penalty. */ constructor(text, pointsMax, isBonus) { super(); this.text = text; this.pointsMax = pointsMax; this.isBonus = isBonus; this.testResults = []; this.points = 0; this.images = []; this.message = null; } addTestResult(text, testStatus, message) { 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); } } } } exports.GradingResult = GradingResult; class TestResult { constructor(text, testStatus, message) { this.text = text; this.testStatus = testStatus; this.message = message; } } exports.TestResult = TestResult; //# sourceMappingURL=results.js.map