grading
Version:
Grading of student submissions, in particular programming tests.
486 lines • 25.8 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.toLatex = exports.san = void 0;
const cliUtil_1 = require("../cli/cliUtil");
const results_1 = require("./results");
const fs = __importStar(require("fs"));
const path_1 = __importDefault(require("path"));
const gitCommands_1 = require("../gitCommands");
/**
* Default image width
*/
const LATEX_IMG_WIDTH = "10cm";
/**
* Computes the path to the image, images are supposed to be found in a subfolder `_${submissionid}_img` of the report directory.
*
* @param reportsDir The directory where the report is written to,
* either from current directory (for testing whether file exists)
* or relative to latex working directory for link in latex
* @param submissionid the submission id
* @param imageFileName the file name of the image
*/
function getImagePath(reportsDir, submissionid, imageFileName) {
return path_1.default.join(reportsDir, `_${submissionid}_img`, imageFileName);
}
function imageNameWithoutSuffix(imageFileName) {
return imageFileName.split('.')[0];
}
function firstUpper(s) {
if (!s) {
return "";
}
if (s.length == 1) {
return s.toUpperCase();
}
return s.charAt(0).toUpperCase() + s.slice(1);
}
const CONTROL_CHARACTERS = [
'NUL', 'SOH', 'STX', 'ETX', 'EOT', 'ENQ', 'ACK', 'BEL',
'BS', 'HT', 'LF', 'VT', 'FF', 'CR', 'SO', 'SI',
'DLE', 'DC1', 'DC2', 'DC3', 'DC4', 'NAK', 'SYN',
'ETB', 'CAN', 'EM', 'SUB', 'ESC', 'FS', 'GS', 'RS', 'US'
];
/**
* Export for testing only
*/
function san(s) {
if (s == undefined || s == null) { // 0 needs to be printed out
return "";
}
if (typeof s === "string") {
s = s
.replace(/\\/mg, "<<§§<<\\textbackslash>>§§>>")
.replace(/([${}])/mg, "\\$1")
.replace(/(<<§§<<)/mg, "{")
.replace(/(>>§§>>)/mg, "}")
.replace(/(#)/mg, "\\#")
.replace(/(\^)/mg, "\\^{}")
.replace(/([\[\]])/mg, "{$1}")
.replace(/_/mg, "{\\_}");
let copy = "";
for (let i = 0; i < s.length; i++) {
const ch = s.charAt(i);
const c = ch.charCodeAt(0);
if (c == 9 || c == 10 || c == 13) {
copy += " ";
}
else if (c < 32) {
copy += "«" + CONTROL_CHARACTERS[c] + "»";
}
else if (c == 127) {
copy += "«DEL»";
}
else {
copy += ch;
}
}
return copy;
}
return String(s);
}
exports.san = san;
function toLatex(results, workDir, reportsDir, patchFolder) {
let outstr = "";
let indent = 0;
const ind = () => {
return "".padStart(indent * 4, " ");
};
const wurdenPunkte = (p) => {
if (p === 1) {
return "wurde \\textbf{" + p + " Punkt}";
}
else {
return "wurden \\textbf{" + p + " Punkte}";
}
};
const sentence = function sentence(text) {
if (Array.isArray(text)) {
return text.map(t => sentence(t)).join(" ");
}
if (text == undefined || text == null) {
return "";
}
const textAsString = String(text);
if (textAsString.length == 0) {
return "";
}
const lastChar = textAsString.charAt(textAsString.length - 1);
if ("\\\n}]-.,;:!?".indexOf(lastChar) >= 0) {
return textAsString;
}
return textAsString + ".";
};
const bonusPrefix = (gradingResult) => {
if (!gradingResult.isBonus || gradingResult.text.includes("Bonus")) {
return "";
}
return "\\textbf{Bonus: }";
};
const outLn = (...text) => {
outstr += ind();
outstr += text.map(t => "" + t).map(s => s.split('\n').join("\n" + ind())).join("");
outstr += '\n';
};
const outDef = (def, text) => {
outLn("\\def\\", "erg", firstUpper(def), "{", san(text), "}");
};
const def = (def, content) => {
block("\\def\\" + "erg" + firstUpper(def) + "{%", content, "} % end of erg" + firstUpper(def));
};
const block = (start, content, end) => {
outLn(start);
indent++;
content();
indent--;
outLn(end);
};
const outImage = (submissionID, images) => {
for (const image of images) {
const projectRelativeImgPath = getImagePath(reportsDir, submissionID, image.src);
const workToReportRelPath = path_1.default.relative(workDir, reportsDir);
const simpleName = imageNameWithoutSuffix(image.src);
const latexRelImagePath = getImagePath(workToReportRelPath, submissionID, simpleName);
outLn(`%
% workDir: ${workDir}
% reportsDir: ${reportsDir}
% projectRelativeImgPath: ${projectRelativeImgPath}
% latexRelImagePath: ${latexRelImagePath}
%`);
if (fs.existsSync(projectRelativeImgPath)) {
const width = image.width ? image.width : LATEX_IMG_WIDTH;
outLn(`
\\begin{center}\\setlength{\\fboxsep}{2pt}\\setlength{\\fboxrule}{0.5pt}%
\\fbox{\\includegraphics[width=${width}]{${latexRelImagePath}}}\\\\
{\\tiny ${image.text}}
\\end{center}
`);
}
else if (image.missing) {
outLn(`
\\begin{center}\\setlength{\\fboxsep}{2pt}\\setlength{\\fboxrule}{0.5pt}%
\\fbox{${image.missing}} % ${latexRelImagePath}
\\end{center
`);
}
else {
outLn(`
\\begin{center}\\setlength{\\fboxsep}{2pt}\\setlength{\\fboxrule}{0.5pt}%
\\fbox{Das Bild konnte nicht erstellt werden.}\\\\
{\\tiny ${image.text}} % ${latexRelImagePath}
\\end{center}
`);
}
}
};
outLn("% LaTeX fragment, generated by grading tool");
outLn("% Used by command 'grading pdf' to create PDF reports");
outLn("\\newcommand\\gradingDate{" + formatResultDate(results.dateTime) + "}");
outLn("\\newcommand\\gradingCourse{" + results.course + "}");
outLn("\\newcommand\\gradingTerm{" + results.term + "}");
outLn("\\newcommand\\punkteVon[2]{\\textbf{#1}{\\small/#2}}");
outLn("\\newcommand\\ergSuccess{\\text{\\color{bhtDGreen}\\ding{52}}}");
outLn("\\newcommand\\ergFailure{\\text{\\color{bhtDRed}\\ding{56}}}");
outLn("\\newcommand\\ergError{\\text{\\color{bhtDRed}\\ding{56}}}");
outLn("\\newcommand\\ergSkipped{\\text{\\color{gray}\\ding{56}}}");
outLn("\\newcommand\\ergNotFound{--}");
outLn("\\newcommand{\\ifequals}[3]{\\ifthenelse{\\equal{#1}{#2}}{#3}{}}");
outDef("blatt", results.examName);
outDef("maxPunkte", results.maxPoints);
outDef("minCoverageStatements", results.minCoverageStatements ?? "keine Coverage-Anforderungen");
outDef("coverageBeachten", results.minCoverageStatements ? true : false);
def("notenSchema", () => {
let blockBeginn = "\\begin{center}{\\scriptsize\\begin{tabular}{|c";
blockBeginn += "|c".repeat(results_1.GRADE_VALUES.length);
blockBeginn += "|}\\hline";
block(blockBeginn, () => {
let strNote = "\\textbf{Note} ";
let strPunkte = "\\textbf{Punkte ab} ";
for (let note = 0; note < results_1.GRADE_VALUES.length; note++) {
strNote += "& " + results_1.GRADE_VALUES[note];
strPunkte += "& " + results.gradingTable[note];
}
outLn(strNote, " \\\\ \\hline");
outLn(strPunkte, " \\\\ \\hline");
}, "\\end{tabular}}\\end{center}");
});
const latexParts = results.studentResults.forEach(studentResult => {
if (studentResult.noSubmission) {
return;
}
outLn("% ".padEnd(80, "-"));
block("\\ifequals{\\submissionid}{" + studentResult.submissionId + "}{", () => {
outDef("name", studentResult.userName);
outDef("rawPunkte", studentResult.pointsRaw);
outDef("coveragePunkte", studentResult.pointsCoverage);
outDef("punkte", studentResult.points);
outDef("note", studentResult.grade);
if (studentResult.hasPenalties()) {
def("penalties", () => {
outLn("\\textbf{Allgemeine Abzüge}");
studentResult.penaltyResults.forEach(gradingResult => {
if (gradingResult.points !== 0) {
block("\\begin{flushright}", () => {
block("\\begin{tabular}{p{\\textwidth-3cm}p{1cm}}", () => {
outLn(sentence(gradingResult.text), " & \\textbf{", gradingResult.points, "} \\\\");
}, "\\end{tabular}");
if (gradingResult.skipped()) {
block("\\begin{tabular}{p{\\textwidth-3cm}}", () => {
outLn("\\small{", san(sentence(gradingResult.skipMessage)), "} \\\\");
}, "\\end{tabular}");
}
else {
gradingResult.testResults.forEach(testResult => {
let testName = testResult.text;
if (gradingResult.testResults.length === 1 && gradingResult.text === testName) { // avoid repeat of name
testName = "Überprüfung";
}
block("\\begin{tabular}{p{\\textwidth-5cm}p{2cm}}", () => {
outLn("\\scriptsize{", san(testName), san(sentence(testMessage(testResult.message))), '} & \\erg', san(firstUpper(testResult.testStatus)), " \\\\");
}, "\\end{tabular}");
});
}
}, "\\end{flushright}");
}
});
});
}
else {
def("penalties", () => { });
}
let absolute = false;
def("correction", () => {
let generalCorrections = "";
if (studentResult.manualCorrection.length > 0) {
studentResult.manualCorrection.forEach(correction => {
if (correction.absolute) {
outLn(sentence(san(correction.reason)) + " \\textbf{Die Abgabe wird daher mit " + correction.points + " Punkten bewertet.}");
absolute = true;
}
else {
const pointsStr = correction.points == 0
? ""
: Math.abs(correction.points) != 1
? ` (\\textbf{${correction.points}} Punkte)`
: ` (\\textbf{${correction.points}} Punkt)`;
outLn(sentence(san(correction.reason)) + pointsStr);
}
outLn("\\\\");
});
}
});
def("coverageDetails", () => {
if (results.minCoverageStatements) {
if (studentResult.coverageResult.skipped()) {
outLn(studentResult.coverageResult.skipMessage);
if (!absolute) {
outLn("Daher ", wurdenPunkte(Math.abs(studentResult.pointsCoverage)), " abgezogen.");
}
}
else {
outLn("Die Statement-Coverage betrug ", studentResult.coverageResult.stmtCoverage, "\\%, verlangt waren mindestens ", results.minCoverageStatements, "\\%.");
if (!absolute) {
const diff = studentResult.coverageResult.stmtCoverage - results.minCoverageStatements;
if (diff > 0) {
if (studentResult.pointsCoverage == 0) {
if (studentResult.pointsRaw < results.extraCoverageMax) {
outLn("Dabei werden die ansonsten erreichten Punkte allerdings maximal verdoppelt!");
}
else {
outLn("Die Differenz war so gering, dass die Punkte unverändert bleiben!");
}
}
else { // ergStudent.coveragePunkte > 0
if (studentResult.pointsRaw < results.extraCoverageMax) {
outLn("Dabei werden die ansonsten erreichten Punkte allerdings maximal verdoppelt!");
}
if (studentResult.pointsRaw != 0) {
if (studentResult.points == results.maxPoints) {
outLn("Daher ", wurdenPunkte(studentResult.pointsCoverage), " addiert, wobei die maximale Punktzahl jedoch nicht überschritten werden kann.");
}
else {
outLn("Daher ", wurdenPunkte(studentResult.pointsCoverage), " addiert.");
}
}
}
}
else if (diff < 0) {
if (studentResult.pointsCoverage != 0) {
outLn("Daher ", wurdenPunkte(Math.abs(studentResult.pointsCoverage)), " abgezogen.");
}
else {
outLn("Die Differenz war so gering, dass die Punkte unverändert bleiben.");
}
}
else { // diff == 0
outLn("Die Punkte bleiben daher unverändert.");
}
}
}
}
});
let rawPunkteGesamt = 0;
def("details", () => {
if (studentResult.comment.length > 0) {
outLn("\\textit{" + studentResult.comment + "}\n\\\\");
}
if (studentResult.skipped()) {
outLn(sentence(studentResult.skipMessage));
}
else {
outLn("\\needspace{3\\baselineskip}");
block("\\begin{description}", () => {
studentResult.taskResults.forEach(taskResult => {
outLn("\\filbreak");
outLn("\\item[Aufgabe ", taskResult.name, "]\\hfill\\punkteVon{", taskResult.points, "}{", taskResult.pointsMax, "}");
if (taskResult.skipped()) {
outLn("\\\\");
outLn(sentence(san(taskResult.skipMessage)));
if (taskResult.message) {
block("\\begin{tabular}{p{\\textwidth-3cm}}", () => {
outLn("{\\scriptsize \\textbf{Probleme bei Korrektur:} ", san(sentence(taskResult.message)), "} \\\\");
}, "\\end{tabular}");
}
}
else {
if (taskResult.message) {
block("\\begin{tabular}{p{\\textwidth-3cm}}", () => {
outLn("{\\scriptsize \\textbf{Probleme bei Korrektur:} ", san(sentence(taskResult.message)), "} \\\\");
}, "\\end{tabular}");
}
taskResult.gradingResults.forEach(gradingResult => {
outLn("\\filbreak");
block("\\begin{flushright}", () => {
outLn("\\filbreak");
block("\\begin{tabular}{p{\\textwidth-3cm}p{1cm}}", () => {
outLn(bonusPrefix(gradingResult) + sentence(gradingResult.text), " & \\punkteVon{", gradingResult.points, "}{", gradingResult.pointsMax, "} \\\\");
}, "\\end{tabular}");
if (gradingResult.skipped()) {
block("\\begin{tabular}{p{\\textwidth-3cm}}", () => {
outLn("\\small{", san(sentence(gradingResult.skipMessage)), "} \\\\");
}, "\\end{tabular}");
}
else {
if (gradingResult.message && gradingResult.message !== taskResult.message) {
block("\\begin{tabular}{p{\\textwidth-3cm}}", () => {
outLn("{\\scriptsize \\textbf{Probleme bei Korrektur:} ", san(sentence(gradingResult.message)), "} \\\\");
}, "\\end{tabular}");
}
gradingResult.testResults.forEach(testResult => {
let testName = testResult.text;
if (gradingResult.testResults.length === 1 && gradingResult.text === testName) { // avoid repeat of name
testName = "Überprüfung";
}
block("\\begin{tabular}{p{\\textwidth-5cm}p{2cm}}", () => {
outLn("\\scriptsize{", san(testName), san(sentence(testMessage(testResult.message))), '} & \\erg', san(firstUpper(testResult.testStatus)), " \\\\");
}, "\\end{tabular}");
});
}
if (gradingResult.images.length > 0) {
outImage(studentResult.submissionId, gradingResult.images);
}
}, "\\end{flushright}");
});
if (taskResult.hasPenalties()) {
taskResult.penaltyResults.forEach(gradingResult => {
block("\\begin{flushright}", () => {
block("\\begin{tabular}{p{\\textwidth-3cm}p{1cm}}", () => {
outLn(sentence(gradingResult.text), " & \\textbf{", gradingResult.points, "} \\\\");
}, "\\end{tabular}");
if (gradingResult.skipped()) {
block("\\begin{tabular}{p{\\textwidth-3cm}}", () => {
outLn("\\small{", san(sentence(gradingResult.skipMessage)), "} \\\\");
}, "\\end{tabular}");
}
else {
gradingResult.testResults.forEach(testResult => {
let testName = testResult.text;
if (gradingResult.testResults.length === 1 && gradingResult.text === testName) { // avoid repeat of name
testName = "Überprüfung";
}
block("\\begin{tabular}{p{\\textwidth-5cm}p{2cm}}", () => {
outLn("\\scriptsize{", san(testName), san(sentence(testMessage(testResult.message))), '} & \\erg', san(firstUpper(testResult.testStatus)), " \\\\");
}, "\\end{tabular}");
});
}
}, "\\end{flushright}");
});
}
}
if (taskResult.manualCorrections.length > 0) {
(0, cliUtil_1.verb)(`Add manual ${taskResult.manualCorrections.length} correction(s) for ${studentResult.userName}, task '${taskResult.name}'.`);
block("\n\\begin{tabular}{p{\\textwidth-3cm}p{1cm}}", () => {
taskResult.manualCorrections.forEach(correction => {
const pointsStr = correction.points == 0
? ""
: ` \\textbf{${correction.points}}`;
outLn("\\small{", sentence(san(correction.reason)), "} & \\small{" + pointsStr + "} \\\\");
});
}, "\\end{tabular}");
}
if (taskResult.images.length > 0) {
outImage(studentResult.submissionId, taskResult.images);
}
});
}, "\\end{description}");
}
});
def("patch", () => {
if (studentResult.patched) {
const patchFileName = (0, gitCommands_1.getPatchFileName)(patchFolder, studentResult.userName, studentResult.submissionId);
const relPatchFileName = path_1.default.relative(workDir, patchFileName);
outLn("\\noindent{\\large\\textbf{Patch}}");
outLn("\\\n");
outLn("\\\n");
outLn("Die folgenden Änderungen mussten während der Korrektur vorgenommen werden, damit die Abgabe bewertet werden konnte.");
outLn(`Das Format ist ein diff-Format. Die mit \\texttt{-} markierten Zeilen sind zu löschen, die mit \\text{+} markierten Zeilen sind hinzuzufügen. An sich kann das automatisch durchgeführt werden, wird hier aber nicht funktionieren, da Sie den Text nicht ordentlich aus dem PDF kopieren können. Abgesehen davon lernen Sie bei der manuellen Anwendung, welche (gravierenden) Fehler Sie gemacht haben. Beachten Sie, dass es sich hierbei nicht um eine wirkliche Korrektur handelt, sondern nur um einen minimalen Fix.\n`);
// outLn("\\begin{lstlisting}[language=diff,numbers=none]")
// outstr += patchFileContent;
// outLn("\\end{lstlisting}")
outLn(`\\lstinputlisting[language=diff,numbers=none]{${relPatchFileName}}`);
}
else {
// do nothing
}
});
}, "} % end of " + studentResult.submissionId);
});
return outstr;
}
exports.toLatex = toLatex;
function formatResultDate(dateTime) {
const date = new Date(dateTime);
return date.getDate() + "." + (1 + date.getMonth()) + "." + date.getFullYear();
}
function testMessage(message) {
if (!message) {
return "";
}
return ": " + message;
}
//# sourceMappingURL=results2latex.js.map
;