Submit an assignment
A program for a student that checks an assignment, saves online text and a file in their submission, and submits it for grading, asking the person first when the site wants them to accept a submission statement.
Uses: assignments, files.
The program
import { createInterface } from "node:readline/promises";
import { createMoodle, SubmissionError } from "@falkenslab/moodle-driver";
// The assignment's course module id: an activity of type "assign" in courseStructure(), or listAssignments().
const ASSIGNMENT_ID = 45;
// Local files go as absolute paths.
const FILE = "/home/ada/work/factorial.py";
await using moodle = createMoodle({
site: "https://campus.example.org/",
profileDir: "/home/ada/.moodle-profile",
headless: true,
downloadBrowser: true,
});
await moodle.logIn();
const assignment = await moodle.studentAssignment(ASSIGNMENT_ID);
for (const row of [...assignment.dates, ...assignment.status]) console.log(`${row.label}: ${row.value}`);
// canEdit: the page offers to add or edit the submission now (not submitted yet, not past its cut-off).
if (!assignment.canEdit) {
console.log("\nThe submission can't be changed now.");
} else {
// Saves a draft: the online text replaces what was there, the file is added to the ones already there.
const saved = await moodle.saveSubmission(ASSIGNMENT_ID, {
onlineText: "<p>My factorial is recursive, with 0 as the base case. The file has the tests.</p>",
files: [FILE],
});
console.log(`\n${saved.changed} Files: ${saved.assignment.submittedFiles.map((f) => f.name).join(", ")}`);
// canSubmit: the assignment asks students to click "Submit assignment", and there's a draft to submit.
// Where it doesn't ask, Moodle took the saved draft as submitted already.
if (saved.assignment.canSubmit) {
try {
console.log((await moodle.submitForGrading(ASSIGNMENT_ID)).changed);
} catch (error) {
// With canSubmit true, a SubmissionError here is the site's statement; its message carries the text.
if (!(error instanceof SubmissionError)) throw error;
console.log(`\n${error.message}`);
const prompt = createInterface({ input: process.stdin, output: process.stdout });
const answer = await prompt.question("Do you accept this statement? (yes/no) ");
prompt.close();
if (/^y(es)?$/i.test(answer.trim())) {
const submitted = await moodle.submitForGrading(ASSIGNMENT_ID, { acceptStatement: true });
console.log(`${submitted.changed} State: ${submitted.assignment.submissionState}`);
} else {
console.log("Not submitted: the draft stays saved, and can be submitted later.");
}
}
}
}
Step by step
logIn()opens a window the first time for the person to log in, and answers at once afterwards.studentAssignment(ASSIGNMENT_ID)reads the assignment as the student's page shows it.datesandstatusare label and value pairs in the site's language;submissionState,canEditandcanSubmitdon't depend on it.saveSubmissionopens the edit page, uploads the file as the file picker does, sets the online text through its editor and saves. It returnschangedand the assignment afterwards, so there's no need to read it again.submitForGradingopens the confirmation page. When the site asks to accept a statement ("This submission is my own work…") it submits nothing and throwsSubmissionErrorwith the statement's text: the program shows it and asks the person at the terminal.- Only if the person answers yes does it call again with
{ acceptStatement: true }, which ticks the statement and submits. Once submitted, the submission can't be edited unless the teacher reverts it to draft.
What it prints
An example on a site with a submission statement; the labels and values come from the site.
Due: Friday, 16 October 2026, 11:59 PM
Submission status: No attempt
Grading status: Not graded
Time remaining: 6 days 12 hours
Saved online text and 1 file in assignment 45's submission. Files: factorial.py
Submitting asks to accept this statement; call again with acceptStatement once the person agrees: This submission is my own work, except where I have acknowledged the use of the works of other people.
Do you accept this statement? (yes/no) yes
Submitted assignment 45 for grading. State: submitted
Variations
- Only a file, or only text: give
filesoronlineTextalone; an assignment that doesn't take one of them throwsSubmissionErrorbefore saving anything. - Check the files before anything opens, to tell the person early:
await checkLocalFiles([FILE])(exported). - Save a draft and stop: leave out
submitForGrading; the teacher sees it as a draft. - Download the files attached to the description first:
for (const f of assignment.descriptionFiles) await moodle.downloadFile(f.url, "/home/ada/work").
When something goes wrong
SubmissionErrorfromstudentAssignment: the id isn't an assignment the person can open. Take it from the course structure (module"assign").SubmissionErrorfromsaveSubmission: nothing given, the submission can't be edited now (submitted already, past the cut-off date), or the assignment doesn't take files or online text.SubmissionErrorfromsubmitForGrading: the statement to accept, as above, or nothing to submit (no draft, submitted already). Thecodeissubmissionfor all of them; checkingcanSubmitfirst is what leaves the statement as the one to expect.MoodleErrorwith codeinvalid-inputornot-found: the file's path isn't absolute or there's no file there, checked before opening anything.FormError: the upload failed (too big, a type the assignment doesn't accept, no upload repository on the site) or Moodle refused the form;error.fieldshas Moodle's messages. See Errors.
Data it touches
studentAssignment is course-content and own; saveSubmission and submitForGrading are own. See data categories.