grading
Version:
Grading of student submissions, in particular programming tests.
339 lines (338 loc) • 15.4 kB
JavaScript
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
;