UNPKG

grading

Version:

Grading of student submissions, in particular programming tests.

695 lines 33.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.checkManualConclusion = exports.readManualConclusion = exports.computeConclusionValues = exports.retrieveResultCSVFilenames = exports.readAllResults = exports.normalizeAliases = exports.calculateConclusions = exports.cmdConclude = exports.Conclusion = exports.AllExamResults = exports.ExamResult = void 0; const comment_json_1 = require("comment-json"); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const csv_1 = require("../csv"); const excel_1 = require("../excel"); const fsUtil_1 = require("../fsUtil"); const table_1 = require("../table"); const cliUtil_1 = require("./cliUtil"); const compareNumberAware_1 = require("./compareNumberAware"); const promises_1 = require("fs/promises"); class ExamResult { constructor(submissionID, name, exam, submitted, grade, points) { if (!submissionID || !name || !exam || grade < 0 || points < 0) { throw new Error(`Invalid exam result: SubmissionID ${submissionID}, name ${name}, exam ${exam}, submitted ${submitted}, grade ${grade}, points ${points}`); } this.submissionID = submissionID; this.name = name; this.exam = exam; this.submitted = submitted; this.grade = grade; this.points = points; } } exports.ExamResult = ExamResult; class AllExamResults { constructor() { this.examsByStudent = new Map(); } addExamResult(examResult) { let examResults = this.examsByStudent.get(examResult.name); if (!examResults) { examResults = []; this.examsByStudent.set(examResult.name, examResults); } if (examResults.some(er => er.exam === examResult.exam)) { throw new Error(`Duplicate exam result found for ${examResult.name} in ${examResult.exam} -- does the folder contain multiple results for same exam?`); } examResults.push(examResult); } } exports.AllExamResults = AllExamResults; // export for tests class Conclusion { constructor(src) { this.name = src.name; this.submissions = src.submissions; this.successful = src.successful; this.failed = src.failed; this.missing = src.missing; this.examGrades = src.examGrades; this.averageGrade = src.averageGrade; this.finalGrade = src.finalGrade; this.minGrade = src.minGrade; this.maxGrade = src.maxGrade; this.remark = src.remark; } getFinalGradeAsNumber() { return (0, csv_1.parseGrading)(this.finalGrade); } getGradeOrNA() { } } exports.Conclusion = Conclusion; async function cmdConclude(options, runInternally = false) { (0, cliUtil_1.verbosity)(options); const resultsDir = options.resultsDir; const resultFile = options.resultFile; const encoding = options.encoding; const resultCSVDelimiter = options.resultCSVDelimiter; const conclusionFile = options.conclusionFile; const selectBest = options.selectBest; const totalExams = options.totalExams; const maxFailed = options.maxFailed; const deprecatedResultsDir = options.deprecatedResultsDir; const autoCopyResult = options.autoCopyResult; const autoCopyConclusion = options.autoCopyConclusion; const keepOldResult = options.keepOldResult; const noFinalGradeBefore = options.noFinalGradeBefore; try { const allResultCSVs = await retrieveResultCSVFilenames(resultsDir); if (autoCopyResult) { await doAutoCopyResult(resultFile, resultsDir, allResultCSVs, keepOldResult, deprecatedResultsDir); } else { (0, cliUtil_1.verb)(`Auto copy results disabled.`); } if (allResultCSVs.length == 0) { (0, cliUtil_1.error)(`No result CSV files found in ${resultsDir}`); return; } const manualConclusion = await readManualConclusion(options); checkManualConclusion(manualConclusion); const allExamResults = new AllExamResults(); (0, cliUtil_1.verb)('Reading result CSVs...'); await readAllResults(allResultCSVs, resultsDir, encoding, resultCSVDelimiter, allExamResults); normalizeAliases(manualConclusion, allExamResults); (0, cliUtil_1.verb)('Calculating conclusions...'); // Collect all submissions const { conclusions, examTitles, examTitlesWithUsage } = calculateConclusions(allExamResults, totalExams, selectBest, maxFailed, noFinalGradeBefore, manualConclusion); // Create table const conclusionFileName = await createAndWriteConclusionTable(examTitlesWithUsage, conclusions, examTitles, conclusionFile, selectBest, maxFailed, totalExams, encoding, resultCSVDelimiter); if (autoCopyConclusion) { const resultWithConclusion = path_1.default.join(resultsDir, path_1.default.basename(conclusionFileName)); (0, cliUtil_1.log)(`Copy ${conclusionFileName} to ${resultWithConclusion}`); await (0, promises_1.copyFile)(conclusionFileName, resultWithConclusion); } else { (0, cliUtil_1.verb)(`Auto copy conclusion disabled.`); } } catch (err) { (0, cliUtil_1.error)(err); program.error(String(err)); } if (!runInternally) { (0, cliUtil_1.log)(`${SEP}\nDone`); } } exports.cmdConclude = cmdConclude; async function createAndWriteConclusionTable(examTitlesWithUsage, conclusions, examTitles, conclusionFile, selectBest, maxFailed, totalExams, encoding, resultCSVDelimiter) { const conclusionTable = new table_1.Table(); conclusionTable.addRow('Name', 'Final Grade', 'Submissions', 'Success', 'Missing', 'Failed', 'Avg', 'Min', 'Max', ...examTitlesWithUsage, "Calc", "Kommentar"); for (const conclusion of conclusions) { conclusionTable.addRow(conclusion.name, conclusion.finalGrade, conclusion.submissions, conclusion.successful, conclusion.missing, conclusion.failed, conclusion.averageGrade, conclusion.minGrade, conclusion.maxGrade, ...usedExams(examTitles, conclusion), '', conclusion.remark || ''); } const firstExam = examTitles[0]; const lastExam = examTitles.length > 0 ? examTitles[examTitles.length - 1] : undefined; const conclusionFileName = (0, cliUtil_1.generateFileName)(conclusionFile, { dateTime: new Date().getTime(), firstExam, lastExam }); if (conclusionFileName.endsWith(fsUtil_1.EXT_EXCEL)) { await writeTableAsExcel(conclusionFileName, conclusionTable, selectBest, maxFailed, totalExams); } else { await (0, csv_1.writeTableWithEncoding)(conclusionFileName, conclusionTable, encoding, resultCSVDelimiter); } (0, cliUtil_1.log)(`Wrote conclusion to ${conclusionFileName}`); return conclusionFileName; } function calculateConclusions(allExamResults, totalExams, selectBest, maxFailed, noFinalGradeBefore, manualConclusion) { const examTitlesSet = new Set(); const conclusions = []; let maxSubmissions = 0; // 1. Creates a conclusion with exam grades for each student. Only submitted exames are added here and no grade is selected yet. for (const examResults of allExamResults.examsByStudent.values()) { const conclusion = new Conclusion({ name: examResults[0].name, submissions: 0, successful: 0, missing: 0, failed: 0, examGrades: [], averageGrade: "", finalGrade: "", minGrade: "", maxGrade: "", }); for (const examResult of examResults) { if (examResult.submitted) { examTitlesSet.add(examResult.exam); conclusion.submissions++; maxSubmissions = Math.max(maxSubmissions, conclusion.submissions); conclusion.examGrades.push({ exam: examResult.exam, grade: examResult.grade, selected: false }); } } conclusions.push(conclusion); } // Actually compute the conclusion values: for (const conclusion of conclusions) { computeConclusionValues(conclusion, maxSubmissions, totalExams, selectBest, maxFailed, noFinalGradeBefore); } const examTitles = Array.from(examTitlesSet).sort(compareNumberAware_1.compareNumberAware); const examTitlesWithUsage = examTitles.map(title => [title, "x"]).flat(); conclusions.sort((a, b) => a.name.localeCompare(b.name)); const appliedManualConclusions = []; if (manualConclusion) { manualConclusion.conclusions.forEach(manualConclusion => { let conclusion = conclusions.find(conclusion => conclusion.name === manualConclusion.userName); if (!conclusion) { (0, cliUtil_1.verb)(`Manual conclusion for ${manualConclusion.userName} not found in computed results, create new one (probably grading from previous term).`); conclusion = new Conclusion({ name: manualConclusion.userName, submissions: 0, successful: 0, missing: 0, failed: 0, examGrades: [], averageGrade: "", finalGrade: "", minGrade: "", maxGrade: "", }); conclusions.push(conclusion); } if (manualConclusion.totalGrading) { conclusion.finalGrade = toNumWithComma(manualConclusion.totalGrading); conclusion.remark = manualConclusion.gradingReason; appliedManualConclusions.push(`${manualConclusion.userName} (${conclusion.finalGrade})`); } if (manualConclusion.generalRemark) { if (conclusion.remark) { conclusion.remark += ". " + manualConclusion.generalRemark; } else { conclusion.remark = manualConclusion.generalRemark; } } }); } (0, cliUtil_1.verb)(`Found ${conclusions.length} students.`); if (appliedManualConclusions.length > 0) { (0, cliUtil_1.log)('Applied manual grading for: ' + appliedManualConclusions.join(', ')); } (0, cliUtil_1.verb)("Max. number of submissions per student: " + maxSubmissions); (0, cliUtil_1.verb)("Number of different exams found: " + examTitles.length); (0, cliUtil_1.verb)("Number of total exams: " + totalExams); if (maxSubmissions !== examTitles.length) { (0, cliUtil_1.warn)(`Number of different exams (${examTitles.length}) differs from max. number of submissions (${maxSubmissions}).`); } return { examTitlesWithUsage, conclusions, examTitles }; } exports.calculateConclusions = calculateConclusions; function normalizeAliases(manualConclusion, allExamResults) { if (manualConclusion?.aliases) { for (const alias of manualConclusion.aliases) { const currentName = alias[0]; const currentResults = allExamResults.examsByStudent.get(currentName); for (let i = 1; i < alias.length; i++) { const aliasName = alias[i]; const aliasResults = allExamResults.examsByStudent.get(aliasName); if (aliasResults) { for (const examResult of aliasResults) { examResult.name = currentName; } allExamResults.examsByStudent.delete(aliasName); if (!currentResults) { allExamResults.examsByStudent.set(currentName, aliasResults); } else { for (const aliasResult of aliasResults) { const currentResult = currentResults.find(cr => cr.exam === aliasResult.exam); if (currentResult) { if (currentResult.grade === aliasResult.grade) { (0, cliUtil_1.warn)(`Alias ${aliasName} has same grade for ${aliasResult.exam} as ${currentName}, skipping.`); } else { throw new Error(`Alias ${aliasName} has different grade for ${aliasResult.exam} as ${currentName}.`); } } else { currentResults.push(aliasResult); (0, cliUtil_1.log)(`${aliasResult.exam} submitted as ${aliasName}, merged to ${currentName}.`); } } } } } } } } exports.normalizeAliases = normalizeAliases; async function doAutoCopyResult(resultFile, resultsDir, allResultCSVs, keepOldResult, deprecatedResultsDir) { const currentResultDir = path_1.default.dirname((0, cliUtil_1.generateFileName)(resultFile, {})); // may not be complete, we only need parent if (resultsDir === currentResultDir) { (0, cliUtil_1.verb)(`Auto copy results disabled, resultsDir and currentResultDir are the same.`); } else { const currentResultCSVs = (await (0, fsUtil_1.readDir)(currentResultDir, 'file', false, 'Current results folder.')) .filter(entry => { const lower = entry.toLowerCase(); return lower.startsWith('results') && lower.endsWith('csv'); }) .sort((a, b) => a.localeCompare(b)); const latestResultCSVs = currentResultCSVs[currentResultCSVs.length - 1]; if (latestResultCSVs) { const latestResultCSVBase = path_1.default.basename(latestResultCSVs); if (allResultCSVs.some(resultFile => resultFile === latestResultCSVBase)) { (0, cliUtil_1.log)(`Latest result CSV ${latestResultCSVBase} already in ${resultsDir}, skipping auto copy.`); } else { if (keepOldResult) { const latestExamFromFilename = parseExamFromFilename(latestResultCSVBase); const similarExamResultFile = allResultCSVs.find(resultFile => { const examFromFilename = parseExamFromFilename(resultFile); return examFromFilename === latestExamFromFilename; }); if (similarExamResultFile) { const similarExamResultCSV = path_1.default.join(resultsDir, similarExamResultFile); const deprecatedResultsFolder = (0, cliUtil_1.generateFileName)(deprecatedResultsDir, { resultsDir }); const target = path_1.default.join(deprecatedResultsFolder, similarExamResultFile); (0, cliUtil_1.log)(`Move old result file for exam ${latestExamFromFilename} from '${similarExamResultCSV}' to '${target}'`); if (!await (0, fsUtil_1.folderExists)(deprecatedResultsFolder, "Deprecated results folder.")) { await (0, fsUtil_1.createDir)(deprecatedResultsFolder); } await (0, promises_1.copyFile)(similarExamResultCSV, target); await (0, promises_1.rm)(similarExamResultCSV); const index = allResultCSVs.findIndex(resultFile => resultFile === similarExamResultFile); allResultCSVs.splice(index, 1); } else { (0, cliUtil_1.verb)(`No other exam file for ${latestExamFromFilename} found in ${resultsDir}`); } } const absCurrentResult = path_1.default.join(currentResultDir, latestResultCSVs); const absAllResultCurrent = path_1.default.join(resultsDir, latestResultCSVs); (0, cliUtil_1.log)(`Copy ${absCurrentResult} to ${absAllResultCurrent}`); await (0, promises_1.copyFile)(absCurrentResult, absAllResultCurrent); allResultCSVs.push(latestResultCSVs); allResultCSVs.sort((a, b) => a.localeCompare(b)); } } else { (0, cliUtil_1.verb)(`No latest result CSV found in ${currentResultDir}`); } } } async function readAllResults(allResultCSVs, resultsDir, encoding, resultCSVDelimiter, allExamResults) { const resultsForExam = new Map(); for (const resultCSV of allResultCSVs) { try { const fullName = path_1.default.join(resultsDir, resultCSV); (0, cliUtil_1.verb)(` Reading result CSV '${fullName}'...`); const results = await (0, csv_1.loadTableWithEncoding)(fullName, encoding, resultCSVDelimiter); const colSubmissionID = results.getColForTitles('submissionID', 'userID'); const colName = results.getColForTitles('name'); const colSubmissionFlag = results.getColForTitles('abgabe', "submitted"); const colGrade = results.getColForTitles('note', "grade"); const colPoints = results.getColForTitles('punkte', "points"); let colExam = -1; const examFromFilename = parseExamFromFilename(resultCSV); try { colExam = results.getColForTitles('exam'); } catch (err) { if (!examFromFilename) { throw Error(`No exam column found in ${resultCSV} and no exam could be parsed from filename.`); } } if (examFromFilename !== undefined && resultsForExam.has(examFromFilename)) { throw new Error(`Duplicate result file for exam '${examFromFilename}': ${resultCSV} and ${resultsForExam.get(examFromFilename)}.`); } if (colSubmissionID < 0) throw new Error(`No column 'submissionID' found in ${resultCSV}`); if (colName < 0) throw new Error(`No column 'name' found in ${resultCSV}`); if (colSubmissionFlag < 0) throw new Error(`No column 'abgabe' found in ${resultCSV}`); if (colGrade < 0) throw new Error(`No column 'note' found in ${resultCSV}`); if (colPoints < 0) throw new Error(`No column 'punkte' found in ${resultCSV}`); for (let row = 2; row <= results.rowsCount; row++) { const submissionID = results.getText(colSubmissionID, row); const name = results.getText(colName, row); const submitted = results.getText(colSubmissionFlag, row) == '1'; const grade = (0, csv_1.parseGrading)(results.getText(colGrade, row)); const points = (0, csv_1.parseGrading)(results.getText(colPoints, row)); const exam = colExam > 0 ? results.getText(colExam, row) : examFromFilename; const examResult = new ExamResult(submissionID, name, exam, submitted, grade, points); allExamResults.addExamResult(examResult); } } catch (err) { (0, cliUtil_1.error)(`Error reading result CSV '${resultCSV}'`); (0, cliUtil_1.error)(err); } } ; } exports.readAllResults = readAllResults; async function retrieveResultCSVFilenames(resultsDir) { return (await (0, fsUtil_1.readDir)(resultsDir, 'file', false, 'All results folder.')) .filter(entry => { const lower = entry.toLowerCase(); return lower.startsWith('results') && lower.endsWith('csv'); }) .sort((a, b) => a.localeCompare(b)); } exports.retrieveResultCSVFilenames = retrieveResultCSVFilenames; /** * Computes conclusion values for a student. * * @param conclusion The current conclusion to fill * @param maxSubmissions the max submissions for any student, i.e. the currently processed exams * @param totalExames the number of exams in total * @param selectBest number of submissions to select for final grade * @param maxFailed number of submissions failed (or not submitted) before to set final grade to 5,0 */ function computeConclusionValues(conclusion, maxSubmissions, totalExames, selectBest, maxFailed, noFinalGradeBefore) { // similar for all students, computed here for better readability (instead of passing args) const openSubmissions = totalExames - maxSubmissions; const surlyCounted = Math.max(0, selectBest - openSubmissions); // sort ascending conclusion.examGrades.sort((a, b) => a.grade - b.grade); let used = 0; let subSum = 0; let minSum = 0; let doFinalGrade = false; conclusion.examGrades.forEach(grade => { if (grade.exam >= noFinalGradeBefore) { doFinalGrade = true; } if (used < selectBest) { // we still need grades grade.selected = true; subSum += grade.grade; used++; if (used <= surlyCounted) { minSum += grade.grade; } } ; if (grade.grade > 4) { conclusion.failed++; } else { conclusion.successful++; } }); const submitted = conclusion.examGrades.length; conclusion.missing = selectBest - used; // missing submissions for this student, this may include past submissions conclusion.failed += maxSubmissions - submitted; // see above: surlyCountedInCourse = selectBest - openSubmissionsInCourse; const averageGrade = used == 0 ? 0 : subSum / used; const maxGrade = (subSum + (5 * conclusion.missing)) / selectBest; const finalGrade = (totalExames <= maxSubmissions) ? maxGrade : 0; const notSubmittedButCounting = Math.max(0, surlyCounted - submitted); const minGrade = (minSum + (5 * notSubmittedButCounting) // surlyCountedInCourse! + (1 * Math.min(openSubmissions, selectBest - surlyCounted))) / selectBest; conclusion.averageGrade = averageGrade > 0 ? roundToGrade(averageGrade) : ""; conclusion.maxGrade = roundToGrade(maxGrade); conclusion.minGrade = roundToGrade(minGrade); if (!doFinalGrade) { conclusion.finalGrade = "-"; } else { conclusion.finalGrade = finalGrade > 0 ? roundToGrade(finalGrade) : ""; if (conclusion.finalGrade && conclusion.minGrade !== conclusion.maxGrade) { (0, cliUtil_1.warn)(`Final grade is set, but min and max grade differ for ${conclusion.name}`); } if (!conclusion.finalGrade && conclusion.minGrade === conclusion.maxGrade) { conclusion.finalGrade = conclusion.minGrade; } if (maxFailed >= 0 && conclusion.failed > maxFailed) { conclusion.finalGrade = "5,0"; } ; } } exports.computeConclusionValues = computeConclusionValues; function parseExamFromFilename(filename) { const match = filename.match(/results_(.+)_.*\.csv/i); if (!match) { return undefined; } return match[1]; } /** * RSPO 2016: * Bei der Mittelung von Noten erfolgt nach einer arithmetischen Berechnung eine Rundung, * indem die nächstgelegene Note vergeben wird. Ergibt sich bei der Mittelung ein Zahlenwert, * der genau zwischen zwei Notenstufen liegt, so ist die bessere Note zu vergeben. * */ function roundToGrade(averageGrade) { // 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% // ] if (averageGrade > (4.3 + 4.0) / 2) return "5,0"; if (averageGrade > (4.0 + 3.7) / 2) return "4,0"; if (averageGrade > (3.7 + 3.3) / 2) return "3,7"; if (averageGrade > (3.3 + 3.0) / 2) return "3,3"; if (averageGrade > (3.0 + 2.7) / 2) return "3,0"; if (averageGrade > (2.7 + 2.3) / 2) return "2,7"; if (averageGrade > (2.3 + 2.0) / 2) return "2,3"; if (averageGrade > (2.0 + 1.7) / 2) return "2,0"; if (averageGrade > (1.7 + 1.3) / 2) return "1,7"; if (averageGrade > (1.3 + 1.0) / 2) return "1,3"; return "1,0"; } function usedExams(examTitles, conclusion) { let usedExams = 0; let selectedGrades = new Set(); const examsWithUsage = []; for (const examTitle of examTitles) { const grade = conclusion.examGrades.find(grade => grade.exam === examTitle); if (grade) { examsWithUsage.push(toNumWithComma(grade.grade)); examsWithUsage.push(grade.selected ? 1 : 0); } else { examsWithUsage.push(''); examsWithUsage.push(''); } } return examsWithUsage; } function toNumWithComma(num) { return num.toString().replace('.', ','); } const nameWidth = 29.6; const midWidth = 10; const examGradeWidth = 6.7; const examSelectedWidth = 1.8; function colLtr(col) { const A = 'A'.charCodeAt(0); return String.fromCharCode(A + col - 1); } async function writeTableAsExcel(conclusionFileName, conclusionTable, selectBest, maxFailed, totalExams) { const COL_FINAL = 2; // column B const COL_SUB = 3; // column C const COL_SUCCESS = 4; // column D const COL_MISSING = 5; // column E const COL_FAILED = 6; // column F const COL_AVG = 7; // column G const COL_MIN = 8; // column H const COL_MAX = 9; // column I const COL_GRADING = 10; // column J await (0, excel_1.writeExcel)(conclusionTable, conclusionFileName, (ws) => { ws.name = 'Conclusion'; ws.getColumn(1).width = nameWidth; [COL_FINAL, COL_SUB, COL_SUCCESS, COL_MISSING, COL_FAILED].forEach(col => ws.getColumn(col).width = midWidth); (0, excel_1.forEachCellInRow)(ws, COL_FINAL, 2, (cell) => cell.font = { bold: true }); (0, excel_1.forEachCellInRow)(ws, COL_FINAL, 2, (cell) => cell.alignment = { horizontal: 'center' }); [COL_AVG, COL_MIN, COL_MAX].forEach(col => ws.getColumn(col).width = examGradeWidth); [COL_FINAL, COL_AVG, COL_MIN, COL_MAX].forEach(col => (0, excel_1.forEachCellInRow)(ws, col, 2, (cell) => cell.numFmt = '0.0')); [COL_SUB, COL_SUCCESS, COL_MISSING, COL_FAILED].forEach(col => (0, excel_1.forEachCellInRow)(ws, col, 2, (cell) => cell.numFmt = '0')); (0, excel_1.headerRow)(ws, 1); let sqRef = []; for (let col = COL_GRADING; col < conclusionTable.columnsCount - 2; col += 2) { ws.getColumn(col).width = examGradeWidth; (0, excel_1.forEachCellInRow)(ws, col, 2, (cell) => { cell.numFmt = '0.0'; }); const colStr = (0, excel_1.toCol)(col); sqRef.push(`${colStr}2:${colStr}${conclusionTable.rowsCount}`); ws.getColumn(col + 1).width = examSelectedWidth; (0, excel_1.forEachCellInRow)(ws, col + 1, 2, (cell) => { cell.fill = excel_1.bgLightGray; cell.numFmt = '0'; }); } (0, excel_1.forEachCellInRow)(ws, conclusionTable.columnsCount - 1, 2, (cell) => { const row = cell.row; const sumRef = []; const countRef = []; for (let col = COL_GRADING; col < conclusionTable.columnsCount - 2; col += 2) { sumRef.push(`${(0, excel_1.toCol)(col)}${row}*${(0, excel_1.toCol)(col + 1)}${row}`); countRef.push(`${(0, excel_1.toCol)(col + 1)}${row}`); } const sumUsed = sumRef.join('+'); const sumCount = countRef.join("+"); cell.value = { formula: `((${sumUsed})+(${selectBest}-(${sumCount}))*5)/${selectBest}`, result: (0, excel_1.asNumber)(conclusionTable.getText(2, parseInt(row))) }; cell.numFmt = '0.00'; cell.fill = excel_1.bgLightYellow; }); ws.addConditionalFormatting({ ref: sqRef.join(" "), rules: [ { type: 'expression', formulae: [`${colLtr(COL_GRADING + 1)}2`], style: { fill: excel_1.bgLightGreen }, } ] }); ws.addConditionalFormatting({ ref: sqRef.join(" "), rules: [ { type: 'cellIs', operator: 'greaterThan', formulae: [4], style: { font: { color: { argb: 'FF9C0000' }, bold: true } }, priority: 2 } ] }); ws.addConditionalFormatting({ ref: `${colLtr(COL_FINAL)}2:${colLtr(COL_FINAL)}${conclusionTable.rowsCount} ${colLtr(COL_MIN)}2:${colLtr(COL_MIN)}${conclusionTable.rowsCount} ${colLtr(COL_MAX)}2:${colLtr(COL_MAX)}${conclusionTable.rowsCount}`, rules: [ { type: 'cellIs', operator: 'equal', formulae: ['"-"'], style: { fill: excel_1.bgVeryLightGray }, // priority: 2 }, { type: 'cellIs', operator: 'greaterThan', formulae: [4], style: { fill: excel_1.bgLightRed }, // priority: 1 }, ] }); if (maxFailed > 0) { ws.addConditionalFormatting({ ref: `${colLtr(COL_FAILED)}2:${colLtr(COL_FAILED)}${conclusionTable.rowsCount}`, rules: [ { type: 'cellIs', operator: 'greaterThan', formulae: [maxFailed], style: { fill: excel_1.bgLightRed }, } ] }); ws.addConditionalFormatting({ ref: `${colLtr(COL_FAILED)}2:${colLtr(COL_FAILED)}${conclusionTable.rowsCount}`, rules: [ { type: 'cellIs', operator: 'equal', formulae: [maxFailed], style: { fill: excel_1.bgLightYellow }, } ] }); if (totalExams) { const minSuccess = (totalExams - maxFailed); ws.addConditionalFormatting({ ref: `${colLtr(COL_SUCCESS)}2:${colLtr(COL_SUCCESS)}${conclusionTable.rowsCount}`, rules: [ { type: 'cellIs', operator: 'greaterThan', formulae: [minSuccess - 1], style: { fill: excel_1.bgLightGreen }, } ] }); } } ws.views = [{ state: 'frozen', xSplit: 1, ySplit: 1, topLeftCell: 'B2', activeCell: 'B2' }]; }); } async function readManualConclusion(options) { if (!options.manualConclusionFile) { (0, cliUtil_1.verb)(`No manual conclusion file specified, skipped.`); return; } const conclusionFile = (0, cliUtil_1.generateFileName)(options.manualConclusionFile, { stdInFolder: options.stdInFolder, resultsDir: options.resultsDir }); if (!await (0, fsUtil_1.fileExists)(conclusionFile, 'conclusionFile')) { (0, cliUtil_1.verb)(`No manual conclusion file "${conclusionFile}" found, skipped.`); return undefined; } (0, cliUtil_1.log)(`Reading manual conclusion from ${conclusionFile}`); const conclusionFileContent = await fs_1.default.promises.readFile(conclusionFile, "utf-8"); // TODO: PATH const conclusionSchema = (0, comment_json_1.parse)(conclusionFileContent); return conclusionSchema; } exports.readManualConclusion = readManualConclusion; function checkManualConclusion(manualConclusion) { if (!manualConclusion) { return; } manualConclusion.conclusions.forEach(conclusion => { if (!conclusion.userName) { throw new Error(`Error in manual conclusion: No userName found in conclusion.`); } if (conclusion.totalGrading > 5 || conclusion.totalGrading < 0) { throw new Error(`Error in manual conclusion: totalGrading > 5 (or less 0) for ${conclusion.userName}.`); } if (conclusion.totalGrading >= 1 && !conclusion.gradingReason) { throw new Error(`Error in manual conclusion: totalGrading defined but gradingReason missing for ${conclusion.userName}.`); } if (!conclusion.totalGrading && conclusion.gradingReason) { throw new Error(`Error in manual conclusion: totalGrading not defined but gradingReason found for ${conclusion.userName}.`); } }); } exports.checkManualConclusion = checkManualConclusion; ; //# sourceMappingURL=cmdConclude.js.map