grading
Version:
Grading of student submissions, in particular programming tests.
277 lines • 13.2 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.cmdSummary = void 0;
const fs_1 = __importDefault(require("fs"));
const csv_1 = require("../csv");
const fsUtil_1 = require("../fsUtil");
const gradingschema_1 = require("../grade/gradingschema");
const cliUtil_1 = require("./cliUtil");
const cmdConclude_1 = require("./cmdConclude");
const cmdPDF_1 = require("./cmdPDF");
const compareNumberAware_1 = require("./compareNumberAware");
async function cmdSummary(options, runInternally = false) {
(0, cliUtil_1.verbosity)(options);
const resultsDir = options.resultsDir;
const encoding = options.encoding;
const resultCSVDelimiter = options.resultCSVDelimiter;
const selectBest = options.selectBest;
const totalExams = options.totalExams;
const maxFailed = options.maxFailed;
const noFinalGradeBefore = options.noFinalGradeBefore;
const thisPartDescription = options.thisPartDescription;
const latex = options.latex;
const schemaFile = options.gradingSchemaFile;
const summariesLatexFragmentFile = options.summariesLatexFragmentFile;
const summaryLatexMainFile = options.summaryLatexMainFile;
const latexMainFolderCopy = options.latexMainFolderCopy;
const pdfSummariesDir = options.pdfSummariesDir;
const workDir = options.workDir;
const dry = options.dry;
const latexConsole = options.latexConsole;
const noSummaryPDFZip = !!options.noSummaryPDFZip;
const summaryPDFZipFile = options.summaryPDFZipFile;
const clean = !!options.clean;
let max = Number.parseInt(options.max);
try {
// Some prequisits
const moodleFile = await (0, cliUtil_1.retrieveOriginalMoodleFile)("", options);
await (0, fsUtil_1.ensureFileExists)(moodleFile, "Check MoodleCSV setting.");
const readable = fs_1.default.createReadStream(moodleFile, "utf-8");
const moodleGradingTable = await (0, csv_1.readCSV)(readable);
const nameToLatestSubmissionID = new Map();
for (let row = 2; row <= moodleGradingTable.rowsCount; row++) { // row 1 is header
const longID = moodleGradingTable.at("ID", row);
if (!longID.startsWith("Teilnehmer/in")) {
throw new Error(`Invalid ID in Moodle file: ${longID}, row ${row}`);
}
const submissionID = longID.slice("Teilnehmer/in".length);
const studentName = moodleGradingTable.at("Vollständiger Name", row);
nameToLatestSubmissionID.set(studentName, submissionID);
}
const gradingSchema = await (0, gradingschema_1.readGradingSchema)(schemaFile);
// Create Conclusion
const allResultCSVs = await (0, cmdConclude_1.retrieveResultCSVFilenames)(resultsDir);
if (allResultCSVs.length == 0) {
(0, cliUtil_1.error)(`No result CSV files found in ${resultsDir}`);
return;
}
const manualConclusion = await (0, cmdConclude_1.readManualConclusion)(options);
(0, cmdConclude_1.checkManualConclusion)(manualConclusion);
const allExamResults = new cmdConclude_1.AllExamResults();
(0, cliUtil_1.verb)('Reading result CSVs...');
await (0, cmdConclude_1.readAllResults)(allResultCSVs, resultsDir, encoding, resultCSVDelimiter, allExamResults);
(0, cmdConclude_1.normalizeAliases)(manualConclusion, allExamResults);
(0, cliUtil_1.verb)('Calculating conclusions...');
// Collect all submissions
const { conclusions } = (0, cmdConclude_1.calculateConclusions)(allExamResults, totalExams, selectBest, maxFailed, noFinalGradeBefore, manualConclusion);
// Write Latex Fragments
await createSummaryLatexFragment({
courseInfo: {
date: new Date().toLocaleDateString("de-DE"),
course: gradingSchema.course, term: gradingSchema.term,
totalExams, selectBest, minPassed: totalExams - maxFailed
},
thisPartDescription, conclusions,
nameToLatestSubmissionID, summariesLatexFragmentFile,
dry
});
// Create PDFs
if (!dry) {
const { latexMainLatex, latexMainPDF, latexMainWOExt } = (0, cmdPDF_1.prepareLatexFiles)(summaryLatexMainFile, workDir, summariesLatexFragmentFile, latexMainFolderCopy);
let count = 0;
for (let row = 2; row <= moodleGradingTable.rowsCount; row++) { // row 1 is header
const longID = moodleGradingTable.at("ID", row);
if (!longID.startsWith("Teilnehmer/in")) {
throw new Error(`Invalid ID in Moodle file: ${longID}, row ${row}`);
}
const submissionID = longID.slice("Teilnehmer/in".length);
const studentName = moodleGradingTable.at("Vollständiger Name", row);
// const studentEmail = moodleGradingTable.at("E-Mail-Adresse", row);
const conclusion = conclusions.find(c => c.name === studentName);
if (conclusion?.finalGrade === "-") {
continue;
}
if (conclusion && conclusion.examGrades.some(eg => (0, compareNumberAware_1.compareNumberAware)(eg.exam, noFinalGradeBefore) >= 0)) {
count++;
const subDir = `${studentName}_${submissionID}_assignsubmission_file`;
await (0, cmdPDF_1.runLatex)(subDir, latexMainLatex, pdfSummariesDir, latex, workDir, latexMainPDF, latexMainWOExt, latexConsole, options);
if (count >= max && max > 0) {
break;
}
}
}
(0, cliUtil_1.log)(`Created ${count} PDF summaries`);
}
else {
(0, cliUtil_1.log)(`Would create PDF summaries, skipped in dry mode.`);
}
if (!noSummaryPDFZip && !dry) {
const fileName = (0, cliUtil_1.generateFileName)(summaryPDFZipFile, { dateTime: Date.now() });
await (0, fsUtil_1.zip)(pdfSummariesDir, fileName);
(0, cliUtil_1.log)(`Created zip file to upload ${fileName}`);
if (clean) {
await (0, fsUtil_1.rmDir)(pdfSummariesDir);
}
}
}
catch (err) {
(0, cliUtil_1.error)(err);
program.error(String(err));
}
if (!runInternally) {
(0, cliUtil_1.log)(`${SEP}\nDone`);
}
}
exports.cmdSummary = cmdSummary;
;
function numberToGrading(grading) {
return grading.toFixed(1).replace('.', ',');
}
function weightToPercentage(weight) {
return (weight * 100).toFixed(0);
}
async function createSummaryLatexFragment({ courseInfo, thisPartDescription, conclusions, nameToLatestSubmissionID, summariesLatexFragmentFile, dry }) {
const latexString = toLatex({
courseInfo,
thisPartDescription,
conclusions,
nameToLatestSubmissionID
});
if (dry) {
(0, cliUtil_1.log)(`Would write latex fragment "${summariesLatexFragmentFile}" (${(latexString.length / 1024).toFixed(2)} kB), skipped in dry mode.`);
return;
}
else {
(0, cliUtil_1.log)(`Write latex fragment "${summariesLatexFragmentFile}".`);
await fs_1.default.promises.writeFile(summariesLatexFragmentFile, latexString);
}
}
function toLatex({ courseInfo, thisPartDescription, conclusions, nameToLatestSubmissionID }) {
let outstr = "";
let indent = 0;
const def = (name, value) => {
outstr += `${" ".repeat(indent)}\\def\\${name}{${value}}\n`;
};
const outLn = (...text) => {
outstr += ind();
outstr += text.map(t => "" + t).map(s => s.split('\n').join("\n" + ind())).join("");
outstr += '\n';
};
const ind = () => {
return "".padStart(indent * 4, " ");
};
const block = (start, content, end) => {
outLn(start);
indent++;
content();
indent--;
outLn(end);
};
const examNames = [...new Set(conclusions.map(c => c.examGrades).flat().map(examGrade => examGrade.exam))].sort((a, b) => (0, compareNumberAware_1.compareNumberAware)(a, b));
if (examNames.length !== courseInfo.totalExams) {
(0, cliUtil_1.warn)(`Number of exams in conclusions does not match total exams: ${examNames.length} vs. ${courseInfo.totalExams}`);
}
const examPrefix = commonPrefix(examNames);
outLn('% LaTeX fragment, generated by grading tool, cf. grading summary');
outLn('% --------------------');
outLn('\\newcommand{\\ifequals}[3]{\\ifthenelse{\\equal{#1}{#2}}{#3}{}}');
outLn('%--------------------------------------');
outLn('% Course data');
outLn('%--------------------------------------');
def("summaryDate", courseInfo.date);
def("summaryCourse", courseInfo.course);
def("summaryTerm", courseInfo.term);
def("summaryTotalExams", courseInfo.totalExams);
def("summarySelectBest", courseInfo.selectBest);
def("summaryMinPassed", courseInfo.minPassed);
def("summaryThisTeilleistung", thisPartDescription);
outLn('%--------------------------------------');
outLn('% Student data');
outLn('%--------------------------------------');
for (const conclusion of conclusions) {
const submissionID = nameToLatestSubmissionID.get(conclusion.name);
if (!submissionID) {
throw new Error(`No submission ID found for ${conclusion.name}`);
}
block(`\\ifequals{\\submissionid}{${submissionID}}{`, () => {
const gradingForThisPart = conclusion.finalGrade;
if (!gradingForThisPart) {
throw new Error(`No grading for ${conclusion.name} for this part (${thisPartDescription}) found`);
}
def("ergName", conclusion.name);
def("ergSuccessful", conclusion.successful);
def("ergGradingThisPart", gradingForThisPart);
block("\\def\\ergConclusion{", () => {
const examColumns = "|c".repeat(examNames.length);
const examGrades = conclusion.examGrades;
block(`\\begin{center}\\begin{tabular}{l${examColumns}}`, () => {
outLn(`\\textbf{${examPrefix}} ${examNames.map(name => " &" + name.slice(examPrefix.length)).join('')} \\\\`);
outLn(`\\textbf{Abgegeben} ${examNames.map(examName => examGrades.some(eg => eg.exam === examName) ? " & \\ding{52}" : " & \\ding{56}").join('')} \\\\ \\hline`);
let countedMissing = 0;
outLn(`\\textbf{Note} ${examNames.map(examName => {
const examGrade = examGrades.find(eg => eg.exam === examName);
if (!examGrade) {
countedMissing++;
if (countedMissing <= conclusion.missing) {
return ` & 5,0`;
}
else {
return ` & {\\color{gray}5,0}`;
}
}
else {
if (examGrade.selected) {
return ` & ${numberToGrading(examGrade.grade)}`;
}
else {
return ` & {\\color{gray}${numberToGrading(examGrade.grade)}}`;
}
}
}).join('')} \\\\ `);
countedMissing = 0;
outLn(`\\textbf{Gezählt} ${examNames.map(examName => {
const examGrade = examGrades.find(eg => eg.exam === examName);
if (!examGrade) {
countedMissing++;
if (countedMissing <= conclusion.missing) {
return ` & \\ding{52}`;
}
else {
return ` & -- `;
}
}
else {
if (examGrade.selected) {
return ` & \\ding{52}`;
}
else {
return ` & --`;
}
}
}).join('')} \\\\ `);
}, `\\end{tabular}\\end{center}`);
}, "}");
}, '}');
}
return outstr;
}
function commonPrefix(names) {
let prefix = names[0];
for (let i = 1; i < names.length; i++) {
const name = names[i];
while (prefix.length > 0) {
if (name.startsWith(prefix)) {
break;
}
prefix = prefix.slice(0, -1);
}
if (prefix.length === 0) {
return "";
}
}
return prefix;
}
//# sourceMappingURL=cmdSummary.js.map
;