UNPKG

grading

Version:

Grading of student submissions, in particular programming tests.

414 lines 15.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.substituteVariables = exports.parseJsonWithComments = exports.JSONSrcMap = exports.isSubmissionSelected = exports.retrieveSelectedFilter = exports.retrieveOriginalMoodleFile = exports.retrieveZipFile = exports.quickFormat = exports.withDecimals = exports.generateFileName = exports.timeDiff = exports.createSubmitterDir = exports.parseSubmitter = exports.warn = exports.error = exports.log = exports.debug = exports.verb = exports.verbosity = void 0; const comment_json_1 = require("comment-json"); const fs_1 = __importDefault(require("fs")); const fsUtil_1 = require("../fsUtil"); const path_1 = __importDefault(require("path")); let verbose = false; let _debug = false; let quiet = false; let colored = false; let colorError = null; function verbosity(options) { (0, fsUtil_1.fsUtilVerbose)(options.verbose); verbose = options.verbose; _debug = options.debug; quiet = options.quite; colored = options.colored; colorError = options.colorError; } exports.verbosity = verbosity; function verb(msg) { if (verbose) { console.log(msg); } } exports.verb = verb; function debug(msg) { if (_debug) { console.log(msg); } } exports.debug = debug; function log(msg, color) { if (!quiet) { if (color && colored) { msg = fsUtil_1.COLOR_START + color + fsUtil_1.COLOR_END + msg + fsUtil_1.COLOR_RESET; } console.log(msg); } } exports.log = log; function error(msg) { if (colorError && colored) { msg = fsUtil_1.COLOR_START + colorError + fsUtil_1.COLOR_END + msg + fsUtil_1.COLOR_RESET; } console.error(msg); if (msg instanceof Error && verbose) { console.error(msg.stack); } } exports.error = error; function warn(msg) { console.log(msg); } exports.warn = warn; const weirdUmlauts = ['ü', 'Ö', 'ä', 'Ü', 'Ä', 'ö']; const fixedUmlauts = ['ü', 'Ö', 'ä', 'Ü', 'Ä', 'ö']; function fixUmlauts(s) { let t = s; for (let i = 0; i < weirdUmlauts.length; i++) { t = t.replaceAll(weirdUmlauts[i], fixedUmlauts[i]); } if (t !== s) { console.log(`Fixed umlauts in ${s} (${s.length}) to ${t} (${t.length}), filename is weird`); } return t; } /** * Parses name of a folder in submission directory for submitter name and submission ID. * Note that the ID is a Moodle specific ID, not used anywhere else. */ function parseSubmitter(subDir) { const match = subDir.match(/\/?([^_]+)_([0-9a-z]+).*/); if (match) { const name = fixUmlauts(match[1]); const submissionId = match[2]; return { name, submissionId }; } else { program.error(`User name and submission id not found in submission folder ${subDir}`); } } exports.parseSubmitter = parseSubmitter; /** * Creates a folder name which could be parsed by parseSubmitter. */ function createSubmitterDir(submitter) { return submitter.name + "_" + submitter.submissionId; } exports.createSubmitterDir = createSubmitterDir; function timeDiff(startTime, endTime) { const secs = Math.floor(Number((endTime - startTime) / BigInt(1000000000))); const min = Math.floor(secs / 60); const sec = secs % 60; return `${min}:${sec < 10 ? "0" + sec : sec}`; } exports.timeDiff = timeDiff; /** * Returns the name with variables (exam, date, time, moodleFile, stdInFolder or others) replaced. * The moodleFile is the "Bewertungsdatei". */ function generateFileName(template, vars, replaceUnresolvedVarsWithEmptyString = false) { if (!template) { throw new Error("No template given, cannot generate file name"); } const dateObj = vars.dateTime ? new Date(vars.dateTime) : undefined; const date = dateObj ? dateObj.getFullYear() + "-" + twoDigits(dateObj.getMonth() + 1) + "-" + twoDigits(dateObj.getDate()) : undefined; const time = dateObj ? twoDigits(dateObj.getHours()) + twoDigits(dateObj.getMinutes()) : undefined; let file = template; file = replaceAll(file, "${examName}", vars.examName, replaceUnresolvedVarsWithEmptyString); if (vars.selected && vars.selected?.length > 0) { const selectedString = vars.selected.join("_"); file = file.replaceAll("${selected}", "." + selectedString); } else if (replaceUnresolvedVarsWithEmptyString) { file = file.replaceAll("${selected}", ""); } file = replaceAll(file, "${date}", date, replaceUnresolvedVarsWithEmptyString); file = replaceAll(file, "${time}", time, replaceUnresolvedVarsWithEmptyString); file = replaceAll(file, "${moodleFile}", vars.moodleFile, replaceUnresolvedVarsWithEmptyString); file = replaceAll(file, "${stdInFolder}", vars.stdInFolder, replaceUnresolvedVarsWithEmptyString); file = replaceAll(file, "${resultsDir}", vars.resultsDir, replaceUnresolvedVarsWithEmptyString); file = replaceAll(file, "${firstExam}", vars.firstExam, replaceUnresolvedVarsWithEmptyString); file = replaceAll(file, "${lastExam}", vars.lastExam, replaceUnresolvedVarsWithEmptyString); return file; } exports.generateFileName = generateFileName; function replaceAll(str, search, replace, replaceUnresolvedVarsWithEmptyString) { if (replace || replaceUnresolvedVarsWithEmptyString) { return str.replaceAll(search, replace ?? ""); } return str; } function twoDigits(v) { if (v >= 10) { return String(v); } return "0" + v; } function withDecimals(n) { return String((Math.round(n * 1000) / 1000)).replace('.', ','); } exports.withDecimals = withDecimals; function quickFormat(v, pad) { if (typeof v === 'string' && /[0-9]+,[0-9]+/.test(v)) { try { const n = Number.parseFloat(v.replace(',', '.')); if (n >= 0 && n <= 1) { let s = String(n * 100); if (n * 100 < 10) { s = ' ' + s; } if (s.indexOf('.') < 0) { s += '.0'; } return (s + '%').padEnd(pad, ' '); } } catch (err) { // ignore } } else if (typeof v === 'number') { return String(v).padEnd(pad, ' '); } return String(v).padEnd(pad, " "); ; } exports.quickFormat = quickFormat; /** * Either returns given zip file or searches for a zip file in the standard in folder. */ async function retrieveZipFile(zipFile, options) { if (!zipFile) { const res = await (0, fsUtil_1.findInFolder)(options.stdInFolder, ".zip", "stdInFolder does not exist"); if (res.length == 0) { program.error(`No zip file found in standard in folder '${options.stdInFolder}'.`); } if (res.length !== 1) { program.error(`Found ${res.length} zip files in standard in folder '${options.stdInFolder}'.`); } zipFile = String(res[0]); log(`Use submission zip file '${zipFile}'.`); } return zipFile; } exports.retrieveZipFile = retrieveZipFile; async function retrieveOriginalMoodleFile(moodleFile, options) { if (!moodleFile) { const res = await (0, fsUtil_1.findInFolder)(options.stdInFolder, ".csv", "stdInFolder does not exist"); if (res.length == 0) { program.error(`No csv file found in standard in folder '${options.stdInFolder}'.`); } if (res.length !== 1) { program.error(`Found ${res.length} csv files in standard in folder '${options.stdInFolder}'.`); } moodleFile = String(res[0]); log(`Use moodle file '${moodleFile}'.`); } return moodleFile; } exports.retrieveOriginalMoodleFile = retrieveOriginalMoodleFile; /** * Returns the selected filter (Names, IDs) or an empty array if no filter is selected. */ function retrieveSelectedFilter(options) { const selected = (!options.selected || options.selected.length == 0) ? [] : options.selected; if (selected instanceof Array) { if (selected[0] === "ilterIDs") { // typical typo program.error("Weird selected, did you wrote '-selected' instead of '--selected'?"); } // verb("Filter IDs: " + selected.join(", ")); return selected; } program.error(`Weird selected: ${selected}, type ${typeof selected}`); } exports.retrieveSelectedFilter = retrieveSelectedFilter; /** * * Returns true if either no selection string `selected` is given or if the name or ID of the submission matches any selection line. * A submission matchtes if the name starts with the selection line or if the ID are equal. */ function isSubmissionSelected(selected, patchedSubmissions, name, submissionID) { if (!selected || selected.length === 0) { if (patchedSubmissions.length === 0) { return true; } return patchedSubmissions.some(sub => sub.name === name && sub.submissionId === submissionID); } return selected.some(selectedEntry => { if (name.startsWith(selectedEntry)) { return true; } if (submissionID === selectedEntry) { return true; } return false; }); } exports.isSubmissionSelected = isSubmissionSelected; const srcMap = Symbol("srcMap"); /** * Hack, we would need a better way to handle this. */ class JSONSrcMap { static getLocation(obj, s) { if (obj[srcMap] instanceof JSONSrcMap) { return obj[srcMap].getLocation(s) + " "; } return ""; } static hasMap(obj) { return obj[srcMap] instanceof JSONSrcMap; } constructor(path, src) { this.path = path; this.src = src; this.newLines = []; this.newLines.push(0); for (let i = 0; i < src.length; i++) { if (src[i] === '\n') { this.newLines.push(i); } } this.newLines.push(src.length); } getLocation(s) { let i = this.src.indexOf(s); if (i < 0) { s = s.replaceAll('\"', '\\\"'); i = this.src.indexOf(s); if (i < 0) { return this.path; } } let line = 0; while (line <= this.newLines.length && this.newLines[line] < i) { line++; } if (line >= this.newLines.length) { return `${this.path}:${line}`; } let column = i - (line > 0 ? this.newLines[line - 1] : 0); return `${this.path}:${line}:${column}`; } } exports.JSONSrcMap = JSONSrcMap; async function parseJsonWithComments(path) { try { const content = await fs_1.default.promises.readFile(path, "utf-8"); const map = new JSONSrcMap(path.toString(), content); const object = await (0, comment_json_1.parse)(content); object[srcMap] = map; return object; } catch (err) { if (err instanceof Object && "line" in err && "column" in err) { throw new Error(`Invalid JSON file ${path}:${err.line}:${err.column} - ${err}`); } throw new Error(`Invalid JSON file ${path} - ${err}`); } } exports.parseJsonWithComments = parseJsonWithComments; /** * Returns the name with variables provided by means of properties of `vars` replaced. * Variables are recognized by the pattern `${variableName}`. * Additionally, the following variables are replaced: * - `${date}` with the date of the exam in the format YYYY-MM-DD * - `${time}` with the time of the exam in the format HHMM * - `${cwd}` with the current working directory * - `${cwdName}` with the simple name of the current working directory * * Inside a variable, a regular expression can be provided to extract a part of the variable value. * This regular expression must be preceded by a '/'. * It either contains exactly one capturing group, the group is then used as the value of the variable. * Or it contains another '/' after which a replacement string can be provided (with $1 etc as placeholders capturing groups). * * Note that slashes in the regular expression must be escaped by a backslash. * * @param template the template string * @param vars the variables; if it contains date, time or cwd, these values override the default ones. * @param defaultValue the default value to use if the variable is not defined. If no default value is provided, an error is thrown if variable is to be substituted. */ function substituteVariables(template, vars, defaultValue) { if (!template) { throw new Error("No template given, cannot substitute variables"); } vars = vars ?? {}; const dateObj = new Date(); const date = vars.date ?? dateObj.getFullYear() + "-" + twoDigits(dateObj.getMonth() + 1) + "-" + twoDigits(dateObj.getDate()); const time = vars.time ?? twoDigits(dateObj.getHours()) + twoDigits(dateObj.getMinutes()); const cwd = vars.cwd ?? process.cwd(); const cwdName = vars.cwdName ?? path_1.default.basename(process.cwd()); let out = ""; for (let i = 0; i < template.length; i++) { if (template[i] === "$" && template[i + 1] === "{") { const end = indexOfNotEscaped(template, "}", i + 2); if (end > 0) { const re = template.indexOf("/", i + 2); const variableName = template.substring(i + 2, (re > 0 && re < end) ? re : end); let val = ""; switch (variableName) { case "date": val = date; break; case "time": val = time; break; case "cwd": val = cwd; break; case "cwdName": val = cwdName; break; default: val = vars[variableName]; if (!val) { if (defaultValue) { val = defaultValue; } else { throw new Error(`Variable ${variableName} not defined`); } } } if (re > 0 && re < end) { const regExString = template.substring(re + 1, end); const replaceIndex = indexOfNotEscaped(regExString, '/'); if (replaceIndex > 0) { const replaceString = regExString.substring(replaceIndex + 1); const regEx = new RegExp(regExString.substring(0, replaceIndex)); val = val.replace(regEx, replaceString); } else { const regEx = new RegExp(regExString); const match = val.match(regEx); if (match && match.length > 0) { val = match[1]; } } } out += val; i = end; } else { out += template[i]; } } else { out += template[i]; } } return out; } exports.substituteVariables = substituteVariables; function indexOfNotEscaped(str, search, start = 0) { do { const index = str.indexOf(search, start); if (index < 0) { return index; } if (index === 0 || str[index - 1] !== "\\") { return index; } start = index + 1; } while (start < str.length); return -1; } //# sourceMappingURL=cliUtil.js.map