Export the grader report to CSV
A teacher saves everyone's grades in a course to a CSV file: one row per student, one column per grade item named as the gradebook names it, with the category and course totals.
Uses: Grades and gradebook, Participants.
The program
import { writeFileSync } from "node:fs";
import { createMoodle, GradebookError, NotLoggedInError } from "@falkenslab/moodle-driver";
// The course's id, from its address (course/view.php?id=3) or moodle.listCourses().
const COURSE_ID = 3;
// One group's students only (an id from moodle.groups(COURSE_ID)), or undefined for the report as it opens.
const GROUP_ID: number | undefined = undefined;
const OUTPUT = "/home/teacher/grades/course-3.csv";
// RFC 4180: a cell with a comma, a quote or a line break goes in quotes, its quotes doubled.
function cell(value: string | number): string {
const text = String(value);
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
}
await using moodle = createMoodle({ site: "https://campus.example.org/", profileDir: "/home/teacher/.moodle-profile", headless: true, downloadBrowser: true });
try {
await moodle.logIn();
// Every page of the grader report, read until no new students come.
const report = await moodle.graderReport(COURSE_ID, { group: GROUP_ID });
// Columns come in the report's order: items, then each category's total, then the course total.
const header = ["User id", "Full name", ...report.columns.map((c) => c.name)];
const lines = [header.map(cell).join(",")];
for (const row of report.rows) {
// A grade refers to its column by itemId; a cell without a grade ("-") has no grade field.
const byItem = new Map(row.grades.map((g) => [g.itemId, g.grade]));
const grades = report.columns.map((c) => byItem.get(c.id) ?? "");
lines.push([row.userId, row.fullName, ...grades].map(cell).join(","));
}
// CRLF line ends, as RFC 4180 and spreadsheets expect.
writeFileSync(OUTPUT, lines.join("\r\n") + "\r\n", "utf8");
console.log(`${report.rows.length} students, ${report.columns.length} grade columns -> ${OUTPUT}`);
} catch (error) {
// GradebookError carries Moodle's reason: no permission, no such course, or grades being recalculated.
if (error instanceof GradebookError) console.error(error.message);
else if (error instanceof NotLoggedInError) console.error("No session: run again to log in.");
else throw error;
}
Step by step
COURSE_IDis the course's id;GROUP_ID, when given, is a group's id fromgroups()orparticipantFilters().logIn()opens a window the first time for the teacher to log in; later runs reuse the session without showing anything.graderReportopens the course's grader report (grade/report/grader/index.php) and reads every page of it. It reads the table by its structure, so the site's language doesn't matter, and gives grades as the text Moodle shows ("8.00", "80.00 %", "B+"): the course's grade display settings decide their format.- The header takes each column's
nameas the report shows it ("Recursive factorial", "Labs total", "Course total"). A column'skindtells an item ("item") from a category total ("category") and the course total ("course"). - Each student's grades are matched to the columns by
itemId, so a cell with no grade becomes an empty CSV cell instead of shifting the row. cellquotes what needs quoting: names with commas ("Ruiz, Ana"), quotes, or grades formatted with a decimal comma ("8,50") in a site in Spanish.- The file is written with
node:fs; the browser closes when the program ends, thanks toawait using.
What it prints
An example run:
24 students, 7 grade columns -> /home/teacher/grades/course-3.csv
And the start of the file:
User id,Full name,Quiz 1,Recursive factorial,Lab 1,Lab 2,Labs total,Course total
41,Ada Byron,8.00,9.50,10.00,7.00,17.00,34.50
42,"Ruiz, Ana",6.50,,9.00,8.00,17.00,23.50
Variations
- Only the items, without totals:
report.columns.filter((c) => c.kind === "item")before building the header and the rows. - One file per group, for each group's tutor: loop over
await moodle.groups(COURSE_ID)and callgraderReport(COURSE_ID, { group: group.id })with a file named aftergroup.name. - For Excel, which reads a plain UTF-8 file as the system's code page, start the file with a byte order mark:
writeFileSync(OUTPUT, "" + lines.join("\r\n"), "utf8"). - Add the activity's id to the header (
${c.name} (${c.activityId})) when two items share a name, or to join the file with other data by activity.
When something goes wrong
GradebookError: Moodle refused the report (the person can't view the grader report, or the course doesn't exist), with Moodle's reason in the message. It also comes while Moodle is recalculating the course's grades after a change (the report shows a progress bar): wait a moment and run again.NotLoggedInError: the session expired; run again and log in.- A Node.js error from
writeFileSync(ENOENT) when the output folder doesn't exist: create it first withmkdirSync(dir, { recursive: true }).
Data it touches
graderReport is others-personal: everyone's names and grades, which the CSV file then holds too; so is groups, where a group's id comes from. See data categories.