grading
Version:
Grading of student submissions, in particular programming tests.
175 lines (149 loc) • 8.07 kB
text/typescript
import { OptionValues } from "commander";
import { error, generateFileName, isSubmissionSelected, log, parseSubmitter, retrieveSelectedFilter, verb, verbosity, warn } from "./cliUtil";
import { createDir, ensureFileExists, ensureFolderExists, execShell, fileExists, folderExists, fsUtilVerbose, readDir, rmDir, rmFile, zip } from "../fsUtil";
import { copyFileSync, existsSync, readdirSync, renameSync, rmdir, rmSync } from 'fs';
import path from "path";
import { DESTRUCTION } from "dns";
import { Submitter } from "./submitter";
// .description('Create PDF reports using LaTeX based on results previously created via grade.')
// .argument('<pdfDir>', 'The folder into which the PDF reports are generated (using Moodle\'s folder structure)')
// .addOption(optSubmissionsDir)
// .option('-p --pdfZipFile <pdfZipFile>', 'File name of created zip file to be uploaded to Moodle (Alle Abgaben Anzeigen/Bewertungsvorgang: Mehrere Feedbackdateien in einer Zip-Datei hochladen); with variables in ${..} (examName, date, time).', "${moodleFile}_graded_${date}-${time}.csv")
// .option('--latexMainFile <latexMainFile>', 'Name of LaTeX main file (importing fragment via input)', "check/latex/grading.tex")
// .option('--latexFragment <latexFragmentFile>', 'Name of LaTeX fragment (previously created via grade)', "reports/results.tex")
// .option('--workDir <workDir>', 'Name of working folder, this folder is cleaned before and after)', "_working")
// .option('-l --latex <latex>', 'Name of LaTeX tool', 'xelatex')
// .option('-c --clean', 'Automatically clean folder with PDFs', false)
// .addOption(optQuiet)
// .addOption(optVerbose)
export async function cmdPDF(options: OptionValues, runInternally = false) {
verbosity(options);
try {
const submissionsDir = options.submissionsDir;
await ensureFolderExists(submissionsDir, "Check submissions folder.");
const latexMainFile = options.latexMainFile;
const latexMainFolderCopy = options.latexMainFolderCopy;
await ensureFileExists(latexMainFile, "Check latex main file.");
const latexFragmentFile = options.latexFragmentFile;
await ensureFileExists(latexFragmentFile, "Check latex fragment file.");
const selected = retrieveSelectedFilter(options);
const selectPatched = options.selectPatched;
const patchedSubmissions: Submitter[] = [];
const patchFolder: string = options.patchFolder;
const clean = selected.length === 0;
const pdfDir = options.pdfDir;
const workDir = options.workDir;
if (clean && await folderExists(pdfDir, 'pdfDir')) await rmDir(pdfDir); // do not clean if only one pdf is regenerated
if (await folderExists(workDir, 'workDir')) await rmDir(workDir);
await createDir(pdfDir)
await createDir(workDir)
const { latexMainLatex, latexMainPDF, latexMainWOExt } = prepareLatexFiles(latexMainFile, workDir, latexFragmentFile, latexMainFolderCopy);
const submissionDirs = await readDir(submissionsDir, 'dir', false, 'Check submissions folder.');
//const submitters = submissionDirs.map(subDir => parseSubmitter(subDir));
if (selectPatched) {
if (selected.length > 0) {
warn(`--selectPatched and --selected are mutually exclusive, ignoring --selectPatched.`);
} else {
const patchFiles = await readDir(patchFolder, 'file', false, 'Check patch folder.');
patchedSubmissions.push(...patchFiles.map(f => {
const n_s = f.substring(0, f.lastIndexOf('.')).split('_');
return { name: n_s[0], submissionId: n_s[1] };
}));
verb(`Found ${patchFiles.length} patch files: ${patchedSubmissions.map(s => s.name).join(', ')}.`)
}
}
let count = 0;
let max = Number.parseInt(options.max);
const latex = options.latex;
for (const subDir of submissionDirs) {
if (max > 0 && count >= max) {
log(`Maximal submissions reached, skipping ${submissionDirs.length - count} submissions.`)
break;
}
const { name, submissionId: submissionId } = parseSubmitter(subDir);
if (!isSubmissionSelected(selected, patchedSubmissions, name, submissionId)) {
verb(`Skip user ${name}, id ${submissionId}`);
continue;
}
count++;
await runLatex(subDir, latexMainLatex, pdfDir, latex, workDir, latexMainPDF, latexMainWOExt, options.latexConsole, options);
}
log(`Created ${count} PDF reports`)
if (!options.noPDFZip) {
const fileName = generateFileName(options.pdfZipFile, { dateTime: Date.now() });
await zip(pdfDir, fileName);
log(`Created zip file to upload ${fileName}`)
if (options.clean) {
await rmDir(pdfDir);
}
}
} catch (err) {
error(`${SEP}\nError: ${err}`);
program.error(String(err));
}
if (!runInternally) {
log(`${SEP}\nDone`);
}
}
/**
*
* @param subDir directory to which to write to inside pdfDir
* @param latexMainLatex
* @param pdfDir the directory where the pdfs are written to
* @param latex
* @param workDir
* @param latexMainPDF
* @param latexMainWOExt
* @param latexConsole
* @param options
*/
export async function runLatex(subDir: string, latexMainLatex: string, pdfDir: string,
latex: string, workDir: string, latexMainPDF: string, latexMainWOExt: string, latexConsole: boolean, options: OptionValues) {
const submitter = parseSubmitter(subDir);
const latexSnippet = `\\def\\submissionid{${submitter.submissionId}}\\input{${latexMainLatex}}`;
const subPDFDir = path.join(pdfDir, subDir);
await createDir(subPDFDir);
const args = ['-halt-on-error'];
if (!latexConsole) {
args.push('-interaction=batchmode');
}
args.push(latexSnippet);
log(`Create PDFs: ${submitter.name}: ${latex} ${args.join(' ')}`);
await execShell(latex, args, { ...options, colored: options.colored || false }, { indent: true, prefix: '>', cwd: workDir });
const generatedPDF = path.join(workDir, latexMainPDF);
if (await fileExists(generatedPDF)) {
const targetPDF = path.join(subPDFDir, latexMainWOExt + "-" + submitter.submissionId + ".pdf");
if (await fileExists(targetPDF)) {
await rmFile(targetPDF);
}
renameSync(generatedPDF, targetPDF);
} else {
program.error(`${generatedPDF} not generated, run with --latexConsole to show LaTeX output.`);
}
}
export function prepareLatexFiles(latexMainFile: any, workDir: any, latexFragmentFile: any, latexMainFolderCopy: boolean) {
const latexMainLatex = path.basename(latexMainFile);
const latexMainWOExt = latexMainLatex.substring(0, latexMainLatex.length - path.extname(latexMainLatex).length);
const latexMainPDF = latexMainWOExt + ".pdf";
if (!folderExists(workDir)) {
createDir(workDir);
}
if (latexMainFolderCopy) {
const latexMainFolder = path.dirname(latexMainFile);
readdirSync(latexMainFolder, {withFileTypes: true}).forEach(dirent => {
const ext = path.extname(dirent.name);
if (dirent.isFile() && ! ["aux", "bbl", "blg", "log", "out", "gz", "thm", "toc", "json"].includes(ext)) {
const srcFile = path.join(latexMainFolder, dirent.name);
const targetFile = path.join(workDir, dirent.name);
if (existsSync(targetFile)) {
rmSync(targetFile);
}
copyFileSync(srcFile, targetFile);
}
});
} else {
copyFileSync(latexMainFile, path.join(workDir, latexMainLatex));
}
copyFileSync(latexFragmentFile, path.join(workDir, path.basename(latexFragmentFile)));
return { latexMainLatex, latexMainPDF, latexMainWOExt };
}