UNPKG

grading

Version:

Grading of student submissions, in particular programming tests.

175 lines (149 loc) 8.07 kB
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 }; }