Answer a survey and a choice
A program for a student that answers a course's feedback survey and a choice activity at the terminal: it shows each question and its options, sends what the person types, and prints what Moodle says afterwards.
Uses: surveys and choices.
The program
import { createInterface } from "node:readline/promises";
import { createMoodle, SurveyError, type SurveyField } from "@falkenslab/moodle-driver";
// Course module ids, from courseStructure(): a feedback activity (type "feedback") and a choice (type "choice").
const SURVEY_ID = 34;
const CHOICE_ID = 40;
await using moodle = createMoodle({
site: "https://campus.example.org/",
profileDir: "/home/ada/.moodle-profile",
headless: true,
downloadBrowser: true,
});
await moodle.logIn();
const prompt = createInterface({ input: process.stdin, output: process.stdout });
// One field's answer: an option's number, a tick or a text; empty leaves it unanswered.
async function ask(field: SurveyField): Promise<string | undefined> {
if (field.kind === "text") return (await prompt.question(" > ")) || undefined;
if (field.kind === "checkbox") return /^y/i.test(await prompt.question(` ${field.label ?? ""} (y/n) `)) ? "1" : undefined;
if (field.label) console.log(` ${field.label}`);
field.options?.forEach((option, i) => console.log(` ${i + 1}) ${option.label}`));
return field.options?.[Number(await prompt.question(" Number: ")) - 1]?.value;
}
// The module saves finding it from the page; "questionnaire" is the other survey module supported.
const survey = await moodle.getSurvey(SURVEY_ID, "feedback");
if (!survey.canAnswer) {
console.log(`Survey: ${survey.messages.join(" ")}`);
} else {
console.log(survey.intro);
// Every page's questions, read ahead: responses go by form name, whatever page they're on.
const responses: Record<string, string> = {};
for (const question of survey.questions) {
console.log(`\n${question.number ?? ""} ${question.text}${question.required ? " (required)" : ""}`);
for (const field of question.fields) {
const value = await ask(field);
if (value !== undefined) responses[field.name] = value;
}
}
try {
// Fills each page with its responses and submits on the last one, as the person would.
const { changed, messages } = await moodle.submitSurvey(SURVEY_ID, responses, { type: "feedback" });
console.log(`${changed} ${messages.join(" ")}`);
} catch (error) {
// Moodle sent a page back (a required question unanswered): its messages are in the error.
if (error instanceof SurveyError) console.log(`Not submitted: ${error.message}`);
else throw error;
}
}
const choice = await moodle.getChoice(CHOICE_ID);
console.log(`\n${choice.name}\n${choice.intro}`);
if (!choice.canAnswer) {
console.log(`${choice.messages.join(" ")} Your answer: ${choice.answer.join(", ") || "none"}`);
} else {
// Full options (a limit of answers reached) can't be chosen.
const open = choice.options.filter((o) => !o.full);
open.forEach((option, i) => console.log(` ${i + 1}) ${option.text}${option.selected ? " *" : ""}`));
const typed = await prompt.question(choice.multiple ? "Numbers, separated by commas: " : "Number: ");
const ids = typed.split(",").flatMap((n) => open[Number(n) - 1]?.id ?? []);
// In a multiple choice the options left out are unticked: ids is the whole answer.
if (ids.length > 0) console.log((await moodle.answerChoice(CHOICE_ID, ids)).changed);
}
prompt.close();
Step by step
logIn()opens a window the first time for the person to log in.getSurvey(SURVEY_ID, "feedback")reads the activity's page and, when the person can answer (canAnswer), every page of its form. Each question has itstext, whether it'srequired, and itsfieldsby form name. When the person has answered already, or the survey is closed,messagessays so.- The program asks for each field at the terminal and keeps the answers by field name.
submitSurveychecks every name against the form first (a wrong one sends nothing), then fills the pages with their "Next page" button and submits on the last. getChoice(CHOICE_ID)reads the choice's page: its options with their ids, whether several can be chosen (multiple), and the person's current answer (selected,answer).answerChoice(CHOICE_ID, ids)saves the options as "Save my choice" does and returnschangedand the choice afterwards.
What it prints
An example; the texts are the activities'.
Tell us how the first unit went.
1 How clear were the explanations? (required)
1) Not at all
2) Somewhat
3) Very
Number: 3
2 What would you change?
> More exercises on recursion.
Submitted feedback 34. Thank you. Your answers have been saved.
Lab group
Choose the lab session you'll attend.
1) Monday 10:00
2) Wednesday 16:00 *
Number: 1
Answered choice 40: Monday 10:00.
Variations
- A questionnaire (mod_questionnaire) instead: give
"questionnaire"to both calls, or no type and let the driver find it at the cost of a page or two. Its rating grids come as onechoicefield per row. - Prepared answers instead of the terminal: fill
responsesby field name (await moodle.submitSurvey(34, { "multichoice_12": "3" })), taking the names fromgetSurveyonce. - Check the answer without changing it:
(await moodle.getChoice(40)).answerlists the chosen options by text, also once the choice is closed.
When something goes wrong
SurveyErrorfromgetSurvey: the id isn't an activity of that module the person can open; check its type in the course structure.SurveyErrorfromsubmitSurvey: unknown field names (nothing is sent), the survey can't be answered now, or Moodle sent a page back with messages by field. After a server error (5xx) Moodle may or may not have saved the answers: read the survey again before retrying.ChoiceErrorfromgetChoiceoranswerChoice: not a choice the person can open, closed or not open yet, answered already where changes aren't allowed, several options in a single choice, or an option that's full. Nothing is saved.NotLoggedInError: the session expired while the person was typing. Callmoodle.logIn()and run again. See Errors.
Data it touches
getSurvey and getChoice are course-content and own; submitSurvey and answerChoice are own. See data categories.