UNPKG

grading

Version:

Grading of student submissions, in particular programming tests.

277 lines 13.2 kB
"use strict"; 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