grading
Version:
Grading of student submissions, in particular programming tests.
695 lines • 33.2 kB
JavaScript
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
;