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