UNPKG

grading

Version:

Grading of student submissions, in particular programming tests.

339 lines (338 loc) 15.4 kB
"use strict"; 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.cmdInit = void 0; const fs_1 = require("fs"); const fs = __importStar(require("fs/promises")); const promises_1 = require("fs/promises"); const path_1 = __importDefault(require("path")); const csv_1 = require("../csv"); const fsUtil_1 = require("../fsUtil"); const gradingschema_1 = require("../grade/gradingschema"); const cliUtil_1 = require("./cliUtil"); const settings_1 = require("./settings"); const constants_1 = require("../constants"); /** * Location of default files in this package. */ const DEFAULT_FILES = "defaultFiles"; async function cmdInit(options) { (0, cliUtil_1.verbosity)(options); try { const schemaFile = options.gradingSchemaFile; let gradingSchema = undefined; if (await (0, fsUtil_1.fileExists)(schemaFile)) { gradingSchema = await (0, gradingschema_1.readGradingSchema)(schemaFile); } if (options.generateCorrectionsFile) { const latestResultFile = await findLatestResultFile(options.resultFile, { examName: gradingSchema?.exam }); let results = undefined; if (latestResultFile) { results = await (0, csv_1.loadTableWithEncoding)(latestResultFile, options.encoding, options.resultCSVDelimiter); } else { (0, cliUtil_1.log)(`No result file found for exam ${gradingSchema?.exam}, cannot prefill users in corrections file. Tip: generate a result file first (by running the grade command)!`); } gradingSchema = await (0, gradingschema_1.readGradingSchema)(schemaFile); await writeCorrectionsFile(options.manualCorrectionsFile, options, gradingSchema, results); if (gradingSchema) { (0, cliUtil_1.verb)("Corrections file created, skip other steps since grading schema is already available."); return; } } if (options.generateManualConclusionFile) { const latestResultFile = await findLatestResultFile(options.resultFile, { examName: gradingSchema?.exam }); let results = undefined; if (latestResultFile) { results = await (0, csv_1.loadTableWithEncoding)(latestResultFile, options.encoding, options.resultCSVDelimiter); } await writeConclusionFile(options.manualConclusionFile, options, gradingSchema, results); if (gradingSchema) { (0, cliUtil_1.verb)("Conclusion file created, skip other steps."); return; } } const checkDir = options.checkDir; if (!await (0, fsUtil_1.folderExists)(checkDir, 'checkDir')) { await (0, fsUtil_1.createDir)(checkDir); } else { (0, cliUtil_1.log)(`Folder ${checkDir} already exists.`); } const packageName = "grading"; const baseStart = __dirname.lastIndexOf(packageName); if (baseStart < 0) { program.error("Unable to find grading package folder."); } const packageFile = __dirname.substring(0, baseStart + packageName.length); const defaultFiles = await (0, fsUtil_1.readDir)(path_1.default.join(packageFile, DEFAULT_FILES), 'file', true); for (const defaultFile of defaultFiles) { const srcFile = path_1.default.join(packageFile, DEFAULT_FILES, defaultFile); const targetFile = path_1.default.join(checkDir, defaultFile); const targetDir = path_1.default.dirname(targetFile); if (targetDir != '.') { await (0, fsUtil_1.createDir)(targetDir); } if (await (0, fsUtil_1.fileExists)(targetFile)) { (0, cliUtil_1.log)(`File ${targetFile} already exists, skipped.`); } else { (0, cliUtil_1.verb)(`Copy ${defaultFile}`); (0, fs_1.copyFileSync)(srcFile, targetFile); } } (0, cliUtil_1.log)(`Default files created in ${checkDir}`); // .option('--stdInFolder', 'Name of folder to be created for input files (and added to .gitignore).', 'gradingIn') // .option('--stdOutFolder', 'Name of folder to be created for output files (and added to .gitignore).', 'gradingOut') if (!options.noFolders) { if (await (0, fsUtil_1.folderExists)(options.stdInFolder, 'stdInFolder')) { (0, cliUtil_1.log)(`${options.stdInFolder} already exists.`); } else { await (0, fsUtil_1.createDir)(options.stdInFolder); (0, cliUtil_1.log)(`Created ${options.stdInFolder} where you should place files downloaded from Moodle.`); } } if (!options.noGitignore) { if (!await (0, fsUtil_1.fileExists)('.gitignore')) { (0, cliUtil_1.log)(`.gitignore does not exists, please ensure to add ${options.stdInFolder} and ${options.stdOutFolder} manually.`); } else { let gitignore = (await (0, promises_1.readFile)('.gitignore')).toString(); const addIn = !new RegExp(`^${options.stdInFolder}$`, 'm').test(gitignore); const addOut = !new RegExp(`^${options.stdOutFolder}$`, 'm').test(gitignore); if (addIn || addOut) { if (gitignore.charAt(gitignore.length - 1) != '\n') { gitignore += "\n"; } gitignore += "# Grading input and output folders\n"; if (addIn) gitignore += `${options.stdInFolder}\n`; if (addOut) gitignore += `${options.stdOutFolder}\n`; await (0, promises_1.writeFile)('.gitignore', gitignore); if (addIn && addOut) { (0, cliUtil_1.log)(`Added ${options.stdInFolder} and${options.stdOutFolder} to .gitignore`); } else if (addIn) { (0, cliUtil_1.log)(`Added ${options.stdInFolder} to .gitignore, ${options.stdOutFolder} already present`); } else if (addOut) { (0, cliUtil_1.log)(`Added ${options.stdOutFolder} to .gitignore, ${options.stdInFolder} already present`); } } else { (0, cliUtil_1.log)(`${options.stdInFolder} and ${options.stdOutFolder} already found in .gitignore`); } } } if (options.generateSettings) { (0, settings_1.writeSettings)(); } (0, cliUtil_1.log)(` In order to not run into problems in your (solution) project, add the following entries to your configuration settings (besides .gitignore): - tsconfig.ts: add "include": ["src/**/*"] at the end if not already present - jest.config.js: add gradingOut to ignored folders in order to prevent testing of submissions in your daily work, i.e. add the following lines: testPathIgnorePatterns: ["<rootDir>/dist/", "<rootDir>/node_modules/", "<rootDir>/gradingOut/"], modulePathIgnorePatterns: ["<rootDir>/gradingOut/"], coveragePathIgnorePatterns: ["<rootDir>/gradingOut/"] `); } catch (err) { (0, cliUtil_1.error)(`${SEP}`); // error(`${SEP}\nError: ${err}`); program.error(String(err)); } } exports.cmdInit = cmdInit; async function writeCorrectionsFile(manualCorrectionsFile, options, gradingSchema, results) { if (!manualCorrectionsFile) { (0, cliUtil_1.error)("No manualCorrectionsFile provided, cannot generate initial correction file"); return; } const correctionFileName = (0, cliUtil_1.generateFileName)(manualCorrectionsFile, options); if (await (0, fsUtil_1.fileExists)(correctionFileName)) { (0, cliUtil_1.log)(`Corrections file '${correctionFileName}' already exists, do not overwrite.`); return; } const folder = path_1.default.dirname(correctionFileName); if (!await (0, fsUtil_1.folderExists)(folder)) { await (0, fsUtil_1.createDir)(folder, true); } const taskInGeneratedCorrectionsFile = Boolean(options.taskInGeneratedCorrectionsFile); let sampleTasks; if (gradingSchema) { sampleTasks = gradingSchema.tasks.map(task => `{ "name": "${task.name}", "points": 0, "reason": "" }`).join(","); } else { sampleTasks = ` { "name": "Task name", "points": 0, "reason": "" }`; } let userList = ""; if (results) { userList = results.rawArray.map(row => "\n" + ` // { "submissionID": "${row[3]}", "userName": "${row[4]}", /* ${row[7]} */ "general": [{"points": 0, "reason": "" }]` + (taskInGeneratedCorrectionsFile ? `, "tasks": [${sampleTasks}]` : "") + "}") .filter((_, index) => index > 0).join(","); } const content = `{ "$schema": "${constants_1.MANUAL_CORRECTIONS_SCHEMA_URL}", "exam": "${gradingSchema?.exam || "Blatt x"}", "course": "${gradingSchema?.course || "Course"}", "term": "${gradingSchema?.term || computeTerm()}", "corrections": [${userList} /* { "submissionID": "xxx", "userName": "first last", "general": [ { "points": 0, "reason": "General remarks or points." } ], "tasks": [${sampleTasks} ] }, */ ] }`; (0, cliUtil_1.log)(`Write manual correction file ${correctionFileName}`); await fs.writeFile(correctionFileName, content, 'utf8'); } function findEndOfBlock(s, offset) { let counter = 0; for (let i = offset + 1; i < s.length; i++) { if (s[i] == '}') { counter--; if (counter < 0) { return i; } } else if (s[i] == '{') { counter++; } } return -1; } async function findLatestResultFile(resultFileTemplate, templateVars) { const resultFiles = await findResultFiles(resultFileTemplate, templateVars); if (resultFiles.length == 0) { return undefined; } return resultFiles[resultFiles.length - 1]; } async function findResultFiles(resultFileTemplate, templateVars) { const resultFileName = (0, cliUtil_1.generateFileName)(resultFileTemplate, templateVars, false); (0, cliUtil_1.verb)(`Search for latest result file ${resultFileName}`); const ext = path_1.default.extname(resultFileName); if (!ext || ext.includes('${')) { (0, cliUtil_1.log)(`Cannot find latest result file, extension not found or it contains unresolved template variables: ${ext}`); return []; } const resultsDir = path_1.default.dirname(resultFileName); const base = path_1.default.basename(resultFileName, ext); if (resultsDir.includes('${')) { (0, cliUtil_1.log)(`Cannot find latest result file, directory name contains unresolved template variables: ${resultsDir}`); return []; } const prefixIndex = base.indexOf('${'); const namePrefix = (prefixIndex >= 0) ? base.substring(0, prefixIndex) : base; const resultFiles = (await (0, fsUtil_1.readDir)(resultsDir, 'file', false, 'All results folder.')) .filter(entry => { return (path_1.default.basename(entry).startsWith(namePrefix) && path_1.default.extname(entry) === ext); }) .sort((a, b) => a.localeCompare(b)) .map(entry => path_1.default.join(resultsDir, entry)); if (resultFiles.length == 0) { (0, cliUtil_1.verb)(`No result files found matching ${resultsDir}/${namePrefix}.${ext}`); } return resultFiles; } function computeTerm() { const now = new Date(); const year = now.getFullYear() - 2000; const month = now.getMonth(); if (month >= 4 && month <= 9) { // Summer term return `SS 20${year}`; } else { // Winter term if (month < 4) { // Winter term of previous year return `WS 20${year - 1}/${year}`; } else { // Winter term of current year return `WS 20${year}/${year + 1}`; } } } async function writeConclusionFile(manualConclusionFile, options, gradingSchema, results) { if (!manualConclusionFile) { (0, cliUtil_1.error)("No manualConclusionFile provided, cannot generate initial conclusion file"); return; } const conclusionFileName = (0, cliUtil_1.generateFileName)(manualConclusionFile, { resultsDir: options.resultsDir, stdInFolder: options.stdInFolder }); if (await (0, fsUtil_1.fileExists)(conclusionFileName)) { (0, cliUtil_1.warn)(`Conclusion file '${conclusionFileName}' already exists, do not overwrite.`); return; } const folder = path_1.default.dirname(conclusionFileName); if (!await (0, fsUtil_1.folderExists)(folder)) { (0, cliUtil_1.error)(`Conclusion file path '${folder}' does not exists (and it is not automatically created).`); return; } let userList = ""; if (results) { userList = results.rawArray.map(row => ` // {"userName": "${row[4]}", "totalGrading": 0.0, "gradingReason": "", "generalRemark": "" }`).filter((_, index) => index > 0).join(","); } const content = `{ "$schema": "${constants_1.MANUAL_CONCLUSION_SCHEMA_URL}", "course": "${gradingSchema?.course || "Course"}", "term": "${gradingSchema?.term || computeTerm()}", "conclusions": [${userList} /* { "userName": "first last", "totalGrading": 0.0, "gradingReason": "Reason for total grading.", "generalRemark": "General remark." }, */ ], "aliases": [ // for renamed students (or names changed otherwise, e.g., typos) // ["currentFirst currentLast", "oldFirst oldLast"],]] ] }`; (0, cliUtil_1.log)(`Write manual conclusion file ${conclusionFileName}`); await fs.writeFile(conclusionFileName, content, 'utf8'); } //# sourceMappingURL=cmdInit.js.map