@falkenslab/moodle-driver 0.0.0-stage → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +133 -2
- package/dist/areas/activities/build.d.ts +98 -0
- package/dist/areas/activities/build.js +246 -0
- package/dist/areas/activities/build.js.map +1 -0
- package/dist/areas/activities/content.d.ts +53 -0
- package/dist/areas/activities/content.js +113 -0
- package/dist/areas/activities/content.js.map +1 -0
- package/dist/areas/assignments/grader.d.ts +68 -0
- package/dist/areas/assignments/grader.js +169 -0
- package/dist/areas/assignments/grader.js.map +1 -0
- package/dist/areas/assignments/grading.d.ts +86 -0
- package/dist/areas/assignments/grading.js +162 -0
- package/dist/areas/assignments/grading.js.map +1 -0
- package/dist/areas/assignments/student.d.ts +57 -0
- package/dist/areas/assignments/student.js +115 -0
- package/dist/areas/assignments/student.js.map +1 -0
- package/dist/areas/calendar/calendar.d.ts +43 -0
- package/dist/areas/calendar/calendar.js +84 -0
- package/dist/areas/calendar/calendar.js.map +1 -0
- package/dist/areas/completion/completion.d.ts +50 -0
- package/dist/areas/completion/completion.js +112 -0
- package/dist/areas/completion/completion.js.map +1 -0
- package/dist/areas/courses/list.d.ts +59 -0
- package/dist/areas/courses/list.js +74 -0
- package/dist/areas/courses/list.js.map +1 -0
- package/dist/areas/courses/overview.d.ts +83 -0
- package/dist/areas/courses/overview.js +137 -0
- package/dist/areas/courses/overview.js.map +1 -0
- package/dist/areas/courses/state.d.ts +51 -0
- package/dist/areas/courses/state.js +45 -0
- package/dist/areas/courses/state.js.map +1 -0
- package/dist/areas/courses/structure.d.ts +89 -0
- package/dist/areas/courses/structure.js +104 -0
- package/dist/areas/courses/structure.js.map +1 -0
- package/dist/areas/forums/discussions.d.ts +57 -0
- package/dist/areas/forums/discussions.js +119 -0
- package/dist/areas/forums/discussions.js.map +1 -0
- package/dist/areas/forums/posts.d.ts +83 -0
- package/dist/areas/forums/posts.js +114 -0
- package/dist/areas/forums/posts.js.map +1 -0
- package/dist/areas/grades/gradebook.d.ts +104 -0
- package/dist/areas/grades/gradebook.js +250 -0
- package/dist/areas/grades/gradebook.js.map +1 -0
- package/dist/areas/grades/user-report.d.ts +26 -0
- package/dist/areas/grades/user-report.js +71 -0
- package/dist/areas/grades/user-report.js.map +1 -0
- package/dist/areas/h5p/h5p.d.ts +33 -0
- package/dist/areas/h5p/h5p.js +104 -0
- package/dist/areas/h5p/h5p.js.map +1 -0
- package/dist/areas/lessons/lessons.d.ts +60 -0
- package/dist/areas/lessons/lessons.js +145 -0
- package/dist/areas/lessons/lessons.js.map +1 -0
- package/dist/areas/messages/messages.d.ts +68 -0
- package/dist/areas/messages/messages.js +84 -0
- package/dist/areas/messages/messages.js.map +1 -0
- package/dist/areas/participants/participants.d.ts +57 -0
- package/dist/areas/participants/participants.js +135 -0
- package/dist/areas/participants/participants.js.map +1 -0
- package/dist/areas/questions/banks.d.ts +83 -0
- package/dist/areas/questions/banks.js +239 -0
- package/dist/areas/questions/banks.js.map +1 -0
- package/dist/areas/quizzes/attempts.d.ts +123 -0
- package/dist/areas/quizzes/attempts.js +300 -0
- package/dist/areas/quizzes/attempts.js.map +1 -0
- package/dist/areas/quizzes/results.d.ts +36 -0
- package/dist/areas/quizzes/results.js +93 -0
- package/dist/areas/quizzes/results.js.map +1 -0
- package/dist/areas/quizzes/structure.d.ts +42 -0
- package/dist/areas/quizzes/structure.js +100 -0
- package/dist/areas/quizzes/structure.js.map +1 -0
- package/dist/areas/site/info.d.ts +25 -0
- package/dist/areas/site/info.js +45 -0
- package/dist/areas/site/info.js.map +1 -0
- package/dist/areas/site/users.d.ts +56 -0
- package/dist/areas/site/users.js +89 -0
- package/dist/areas/site/users.js.map +1 -0
- package/dist/areas/surveys/choice.d.ts +38 -0
- package/dist/areas/surveys/choice.js +105 -0
- package/dist/areas/surveys/choice.js.map +1 -0
- package/dist/areas/surveys/surveys.d.ts +51 -0
- package/dist/areas/surveys/surveys.js +201 -0
- package/dist/areas/surveys/surveys.js.map +1 -0
- package/dist/browser/chromium.d.ts +15 -0
- package/dist/browser/chromium.js +53 -0
- package/dist/browser/chromium.js.map +1 -0
- package/dist/browser/cookies.d.ts +11 -0
- package/dist/browser/cookies.js +31 -0
- package/dist/browser/cookies.js.map +1 -0
- package/dist/browser/manager.d.ts +57 -0
- package/dist/browser/manager.js +177 -0
- package/dist/browser/manager.js.map +1 -0
- package/dist/browser.d.ts +53 -0
- package/dist/browser.js +167 -0
- package/dist/browser.js.map +1 -0
- package/dist/categories.d.ts +28 -0
- package/dist/categories.js +222 -0
- package/dist/categories.js.map +1 -0
- package/dist/chromiumInstaller.d.ts +15 -0
- package/dist/chromiumInstaller.js +52 -0
- package/dist/chromiumInstaller.js.map +1 -0
- package/dist/cli/args.d.ts +33 -0
- package/dist/cli/args.js +194 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/commands.generated.d.ts +4 -0
- package/dist/cli/commands.generated.js +2203 -0
- package/dist/cli/commands.generated.js.map +1 -0
- package/dist/cli/config.d.ts +46 -0
- package/dist/cli/config.js +107 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +19 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/output.d.ts +4 -0
- package/dist/cli/output.js +71 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/run.d.ts +15 -0
- package/dist/cli/run.js +194 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/spec.d.ts +42 -0
- package/dist/cli/spec.js +4 -0
- package/dist/cli/spec.js.map +1 -0
- package/dist/client.d.ts +217 -0
- package/dist/client.js +179 -0
- package/dist/client.js.map +1 -0
- package/dist/core/ajax.d.ts +25 -0
- package/dist/core/ajax.js +85 -0
- package/dist/core/ajax.js.map +1 -0
- package/dist/core/editor.d.ts +18 -0
- package/dist/core/editor.js +51 -0
- package/dist/core/editor.js.map +1 -0
- package/dist/core/errors.d.ts +12 -0
- package/dist/core/errors.js +10 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/files.d.ts +16 -0
- package/dist/core/files.js +58 -0
- package/dist/core/files.js.map +1 -0
- package/dist/core/forms.d.ts +45 -0
- package/dist/core/forms.js +200 -0
- package/dist/core/forms.js.map +1 -0
- package/dist/core/http.d.ts +18 -0
- package/dist/core/http.js +22 -0
- package/dist/core/http.js.map +1 -0
- package/dist/core/inPage.d.ts +8 -0
- package/dist/core/inPage.js +10 -0
- package/dist/core/inPage.js.map +1 -0
- package/dist/core/login.d.ts +40 -0
- package/dist/core/login.js +83 -0
- package/dist/core/login.js.map +1 -0
- package/dist/core/pace.d.ts +4 -0
- package/dist/core/pace.js +21 -0
- package/dist/core/pace.js.map +1 -0
- package/dist/core/session.d.ts +55 -0
- package/dist/core/session.js +157 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/site.d.ts +8 -0
- package/dist/core/site.js +36 -0
- package/dist/core/site.js.map +1 -0
- package/dist/core/text.d.ts +21 -0
- package/dist/core/text.js +113 -0
- package/dist/core/text.js.map +1 -0
- package/dist/home.d.ts +19 -0
- package/dist/home.js +54 -0
- package/dist/home.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/internals.d.ts +11 -0
- package/dist/internals.js +15 -0
- package/dist/internals.js.map +1 -0
- package/dist/moodle/activities.d.ts +94 -0
- package/dist/moodle/activities.js +240 -0
- package/dist/moodle/activities.js.map +1 -0
- package/dist/moodle/ajax.d.ts +22 -0
- package/dist/moodle/ajax.js +91 -0
- package/dist/moodle/ajax.js.map +1 -0
- package/dist/moodle/assign.d.ts +65 -0
- package/dist/moodle/assign.js +162 -0
- package/dist/moodle/assign.js.map +1 -0
- package/dist/moodle/calendar.d.ts +41 -0
- package/dist/moodle/calendar.js +80 -0
- package/dist/moodle/calendar.js.map +1 -0
- package/dist/moodle/choice.d.ts +33 -0
- package/dist/moodle/choice.js +100 -0
- package/dist/moodle/choice.js.map +1 -0
- package/dist/moodle/completion.d.ts +48 -0
- package/dist/moodle/completion.js +108 -0
- package/dist/moodle/completion.js.map +1 -0
- package/dist/moodle/content.d.ts +43 -0
- package/dist/moodle/content.js +105 -0
- package/dist/moodle/content.js.map +1 -0
- package/dist/moodle/course.d.ts +51 -0
- package/dist/moodle/course.js +45 -0
- package/dist/moodle/course.js.map +1 -0
- package/dist/moodle/courses.d.ts +57 -0
- package/dist/moodle/courses.js +70 -0
- package/dist/moodle/courses.js.map +1 -0
- package/dist/moodle/editor.d.ts +18 -0
- package/dist/moodle/editor.js +50 -0
- package/dist/moodle/editor.js.map +1 -0
- package/dist/moodle/files.d.ts +16 -0
- package/dist/moodle/files.js +57 -0
- package/dist/moodle/files.js.map +1 -0
- package/dist/moodle/forms.d.ts +44 -0
- package/dist/moodle/forms.js +199 -0
- package/dist/moodle/forms.js.map +1 -0
- package/dist/moodle/forum.d.ts +55 -0
- package/dist/moodle/forum.js +115 -0
- package/dist/moodle/forum.js.map +1 -0
- package/dist/moodle/forumPosts.d.ts +83 -0
- package/dist/moodle/forumPosts.js +113 -0
- package/dist/moodle/forumPosts.js.map +1 -0
- package/dist/moodle/gradebook.d.ts +102 -0
- package/dist/moodle/gradebook.js +246 -0
- package/dist/moodle/gradebook.js.map +1 -0
- package/dist/moodle/grades.d.ts +24 -0
- package/dist/moodle/grades.js +67 -0
- package/dist/moodle/grades.js.map +1 -0
- package/dist/moodle/grading.d.ts +92 -0
- package/dist/moodle/grading.js +161 -0
- package/dist/moodle/grading.js.map +1 -0
- package/dist/moodle/h5p.d.ts +31 -0
- package/dist/moodle/h5p.js +100 -0
- package/dist/moodle/h5p.js.map +1 -0
- package/dist/moodle/inPage.d.ts +8 -0
- package/dist/moodle/inPage.js +10 -0
- package/dist/moodle/inPage.js.map +1 -0
- package/dist/moodle/lesson.d.ts +57 -0
- package/dist/moodle/lesson.js +140 -0
- package/dist/moodle/lesson.js.map +1 -0
- package/dist/moodle/login.d.ts +37 -0
- package/dist/moodle/login.js +77 -0
- package/dist/moodle/login.js.map +1 -0
- package/dist/moodle/messages.d.ts +66 -0
- package/dist/moodle/messages.js +80 -0
- package/dist/moodle/messages.js.map +1 -0
- package/dist/moodle/overview.d.ts +83 -0
- package/dist/moodle/overview.js +137 -0
- package/dist/moodle/overview.js.map +1 -0
- package/dist/moodle/pace.d.ts +4 -0
- package/dist/moodle/pace.js +19 -0
- package/dist/moodle/pace.js.map +1 -0
- package/dist/moodle/participants.d.ts +55 -0
- package/dist/moodle/participants.js +131 -0
- package/dist/moodle/participants.js.map +1 -0
- package/dist/moodle/questionBank.d.ts +81 -0
- package/dist/moodle/questionBank.js +235 -0
- package/dist/moodle/questionBank.js.map +1 -0
- package/dist/moodle/quiz.d.ts +117 -0
- package/dist/moodle/quiz.js +291 -0
- package/dist/moodle/quiz.js.map +1 -0
- package/dist/moodle/quizResults.d.ts +36 -0
- package/dist/moodle/quizResults.js +93 -0
- package/dist/moodle/quizResults.js.map +1 -0
- package/dist/moodle/quizStructure.d.ts +42 -0
- package/dist/moodle/quizStructure.js +100 -0
- package/dist/moodle/quizStructure.js.map +1 -0
- package/dist/moodle/session.d.ts +39 -0
- package/dist/moodle/session.js +99 -0
- package/dist/moodle/session.js.map +1 -0
- package/dist/moodle/site.d.ts +8 -0
- package/dist/moodle/site.js +36 -0
- package/dist/moodle/site.js.map +1 -0
- package/dist/moodle/siteInfo.d.ts +25 -0
- package/dist/moodle/siteInfo.js +45 -0
- package/dist/moodle/siteInfo.js.map +1 -0
- package/dist/moodle/structure.d.ts +94 -0
- package/dist/moodle/structure.js +103 -0
- package/dist/moodle/structure.js.map +1 -0
- package/dist/moodle/submission.d.ts +48 -0
- package/dist/moodle/submission.js +109 -0
- package/dist/moodle/submission.js.map +1 -0
- package/dist/moodle/survey.d.ts +47 -0
- package/dist/moodle/survey.js +197 -0
- package/dist/moodle/survey.js.map +1 -0
- package/dist/moodle/text.d.ts +21 -0
- package/dist/moodle/text.js +113 -0
- package/dist/moodle/text.js.map +1 -0
- package/dist/moodle/user.d.ts +54 -0
- package/dist/moodle/user.js +78 -0
- package/dist/moodle/user.js.map +1 -0
- package/dist/options.d.ts +30 -0
- package/dist/options.js +35 -0
- package/dist/options.js.map +1 -0
- package/dist/sessionCookies.d.ts +11 -0
- package/dist/sessionCookies.js +31 -0
- package/dist/sessionCookies.js.map +1 -0
- package/dist/testing/fakeMoodle.d.ts +55 -0
- package/dist/testing/fakeMoodle.js +103 -0
- package/dist/testing/fakeMoodle.js.map +1 -0
- package/dist/testing/gradingPanel.d.ts +6 -0
- package/dist/testing/gradingPanel.js +22 -0
- package/dist/testing/gradingPanel.js.map +1 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +18 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +77 -4
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { dateSelectorValues, openForm, setFields, submitForm } from "./forms.js";
|
|
2
|
+
import { pause, readingSeconds } from "./pace.js";
|
|
3
|
+
import { inPage } from "./inPage.js";
|
|
4
|
+
import { NotLoggedInError, openSitePage } from "./session.js";
|
|
5
|
+
/** The quiz or attempt isn't there, or the person can't do what was asked. */
|
|
6
|
+
export class QuizError extends Error {
|
|
7
|
+
name = "QuizError";
|
|
8
|
+
}
|
|
9
|
+
const NOT_A_QUIZ = (id) => new QuizError(`Activity ${id} isn't a quiz you can open. Take the id of a quiz (module "quiz") from the course structure.`);
|
|
10
|
+
export function readQuizView(page) {
|
|
11
|
+
return inPage(page, () => {
|
|
12
|
+
const doc = globalThis.document;
|
|
13
|
+
const text = (el) => (el?.innerText ?? "").replace(/[ \t]+/g, " ").trim();
|
|
14
|
+
const attempts = Array.from(doc.querySelectorAll(".card")).flatMap((card) => {
|
|
15
|
+
if (!card.querySelector("table.quizreviewsummary"))
|
|
16
|
+
return [];
|
|
17
|
+
const link = card.querySelector('a[href*="review.php?attempt="], a[href*="attempt.php?attempt="]');
|
|
18
|
+
const href = link?.getAttribute("href") ?? "";
|
|
19
|
+
const id = Number(/attempt=(\d+)/.exec(href)?.[1]);
|
|
20
|
+
const details = Array.from(card.querySelectorAll("table.quizreviewsummary tr")).map((row) => ({ label: text(row.querySelector("th")), value: text(row.querySelector("td")) }));
|
|
21
|
+
return id ? [{ attemptId: id, inProgress: /\/attempt\.php/.test(href), details }] : [];
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
intro: text(doc.querySelector("#intro")),
|
|
25
|
+
info: Array.from(doc.querySelectorAll(".quizinfo p")).map((p) => text(p)),
|
|
26
|
+
dates: Array.from(doc.querySelectorAll('[data-region="activity-dates"] .date-item')).map((item) => {
|
|
27
|
+
const label = text(item.querySelector(".date-item-label"));
|
|
28
|
+
return { label: label.replace(/:$/, ""), value: text(item).slice(label.length).trim() };
|
|
29
|
+
}),
|
|
30
|
+
feedback: text(doc.querySelector("#feedback")) || undefined,
|
|
31
|
+
attempts,
|
|
32
|
+
canStart: doc.querySelector('form[action*="startattempt.php"]') !== null,
|
|
33
|
+
messages: Array.from(doc.querySelectorAll(".quizattempt p, .quizattempt .alert")).map((p) => text(p)).filter(Boolean),
|
|
34
|
+
};
|
|
35
|
+
}, null);
|
|
36
|
+
}
|
|
37
|
+
/** The questions on an attempt or review page, with their fields read from the form. */
|
|
38
|
+
export function readQuestions(page) {
|
|
39
|
+
return inPage(page, () => {
|
|
40
|
+
const doc = globalThis.document;
|
|
41
|
+
// Text as the page shows it, without what only screen readers get ("Answer Question 4").
|
|
42
|
+
const text = (el) => {
|
|
43
|
+
if (!el)
|
|
44
|
+
return "";
|
|
45
|
+
const copy = el.cloneNode(true);
|
|
46
|
+
for (const hidden of Array.from(copy.querySelectorAll(".accesshide, .visually-hidden, .sr-only")))
|
|
47
|
+
hidden.remove();
|
|
48
|
+
doc.body.appendChild(copy);
|
|
49
|
+
const shown = copy.innerText ?? "";
|
|
50
|
+
copy.remove();
|
|
51
|
+
return shown.replace(/[ \t]+/g, " ").trim();
|
|
52
|
+
};
|
|
53
|
+
return Array.from(doc.querySelectorAll(".que")).flatMap((q) => {
|
|
54
|
+
const match = /^question-\d+-(\d+)$/.exec(q.id);
|
|
55
|
+
if (!match)
|
|
56
|
+
return [];
|
|
57
|
+
const classes = q.className.split(/\s+/);
|
|
58
|
+
const fields = [];
|
|
59
|
+
const inputs = Array.from(q.querySelectorAll("input:not([type=hidden]), select, textarea")).filter((input) => !/:flagged$|_:sequencecheck$/.test(input.name) && input.name);
|
|
60
|
+
for (const input of inputs) {
|
|
61
|
+
// An option's label, without its "a." numbering: the element named after the input, or its row.
|
|
62
|
+
const labelEl = (doc.getElementById(`${input.id}_label`) ?? input.closest(".answer > div, label"))?.cloneNode(true);
|
|
63
|
+
for (const numbering of Array.from(labelEl?.querySelectorAll(".answernumber") ?? []))
|
|
64
|
+
numbering.remove();
|
|
65
|
+
const label = labelEl ? (labelEl.innerText ?? "").replace(/\s+/g, " ").trim() : undefined;
|
|
66
|
+
if (input.type === "radio") {
|
|
67
|
+
let field = fields.find((f) => f.name === input.name);
|
|
68
|
+
if (!field) {
|
|
69
|
+
field = { name: input.name, kind: "choice", options: [] };
|
|
70
|
+
fields.push(field);
|
|
71
|
+
}
|
|
72
|
+
// A single-choice question's hidden "clear my choice" radio has the value -1 and no label of its own.
|
|
73
|
+
field.options.push({ value: input.value, label: label || (input.value === "-1" ? "(clear my choice)" : input.value) });
|
|
74
|
+
if (input.checked)
|
|
75
|
+
field.value = input.value;
|
|
76
|
+
}
|
|
77
|
+
else if (input.type === "checkbox") {
|
|
78
|
+
fields.push({ name: input.name, kind: "checkbox", label, value: input.checked ? "1" : "0" });
|
|
79
|
+
}
|
|
80
|
+
else if (input.tagName === "SELECT") {
|
|
81
|
+
const options = Array.from(input.options);
|
|
82
|
+
fields.push({
|
|
83
|
+
name: input.name,
|
|
84
|
+
kind: "select",
|
|
85
|
+
label: text(input.closest("tr")?.querySelector(".text")) || undefined,
|
|
86
|
+
options: options.map((o) => ({ value: o.value, label: o.text.trim() })),
|
|
87
|
+
value: options.find((o) => o.selected)?.value,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
fields.push({ name: input.name, kind: "text", value: input.value || undefined });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return [
|
|
95
|
+
{
|
|
96
|
+
slot: Number(match[1]),
|
|
97
|
+
number: text(q.querySelector(".qno")) || undefined,
|
|
98
|
+
type: classes[1] ?? "",
|
|
99
|
+
state: classes[3] ?? "",
|
|
100
|
+
mark: text(q.querySelector(".info .grade")) || undefined,
|
|
101
|
+
text: text(q.querySelector(".qtext")),
|
|
102
|
+
images: Array.from(q.querySelectorAll(".qtext img")).map((img) => img.src),
|
|
103
|
+
fields,
|
|
104
|
+
feedback: [text(q.querySelector(".specificfeedback")), text(q.querySelector(".generalfeedback"))].filter(Boolean).join("\n") || undefined,
|
|
105
|
+
rightAnswer: text(q.querySelector(".rightanswer")) || undefined,
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
});
|
|
109
|
+
}, null);
|
|
110
|
+
}
|
|
111
|
+
/** The pages of an attempt, from its navigation: slot → page. */
|
|
112
|
+
export function readNavigation(page) {
|
|
113
|
+
return page.evaluate(() => {
|
|
114
|
+
const buttons = globalThis.document.querySelectorAll('.qnbutton[id^="quiznavbutton"]');
|
|
115
|
+
const pages = {};
|
|
116
|
+
for (const button of Array.from(buttons))
|
|
117
|
+
pages[Number(button.id.slice("quiznavbutton".length))] = Number(button.getAttribute("data-quiz-page") ?? 0);
|
|
118
|
+
return pages;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
export async function quizInfo(context, siteUrl, quizId) {
|
|
122
|
+
const page = await openSitePage(context, siteUrl, `mod/quiz/view.php?id=${quizId}`);
|
|
123
|
+
try {
|
|
124
|
+
if ((await page.locator("body#page-mod-quiz-view").count()) === 0)
|
|
125
|
+
throw NOT_A_QUIZ(quizId);
|
|
126
|
+
return await readQuizView(page);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
await page.close().catch(() => undefined);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Starts an attempt, or continues the one in progress, as the quiz page's button does; returns its id. */
|
|
133
|
+
export async function startAttempt(context, siteUrl, quizId) {
|
|
134
|
+
const page = await openSitePage(context, siteUrl, `mod/quiz/view.php?id=${quizId}`);
|
|
135
|
+
try {
|
|
136
|
+
if ((await page.locator("body#page-mod-quiz-view").count()) === 0)
|
|
137
|
+
throw NOT_A_QUIZ(quizId);
|
|
138
|
+
const start = page.locator('form[action*="startattempt.php"] [type="submit"]').first();
|
|
139
|
+
if ((await start.count()) === 0)
|
|
140
|
+
throw new QuizError(`You can't start an attempt at quiz ${quizId} now: ${(await readQuizView(page)).messages.join(" ") || "see the quiz's information"}.`);
|
|
141
|
+
await Promise.all([page.waitForNavigation({ waitUntil: "domcontentloaded" }), start.click()]);
|
|
142
|
+
// A time limit or a password first asks for confirmation on a form of its own.
|
|
143
|
+
if ((await page.locator("#mod_quiz_preflight_form, form[action*='startattempt.php'] [name='submitbutton']").count()) > 0) {
|
|
144
|
+
if ((await page.locator("#mod_quiz_preflight_form [name='quizpassword']").count()) > 0)
|
|
145
|
+
throw new QuizError(`Quiz ${quizId} asks for a password, which this server doesn't type.`);
|
|
146
|
+
await Promise.all([page.waitForNavigation({ waitUntil: "domcontentloaded" }), page.click("[name='submitbutton']")]);
|
|
147
|
+
}
|
|
148
|
+
const id = Number(new URL(page.url()).searchParams.get("attempt"));
|
|
149
|
+
if (!id)
|
|
150
|
+
throw new QuizError(`Quiz ${quizId} didn't open an attempt (it landed on ${new URL(page.url()).pathname}).`);
|
|
151
|
+
return id;
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
await page.close().catch(() => undefined);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** Every question of an attempt, page by page, with the page each is on. */
|
|
158
|
+
export async function readAttempt(context, siteUrl, attemptId) {
|
|
159
|
+
const page = await openSitePage(context, siteUrl, `mod/quiz/attempt.php?attempt=${attemptId}&page=0`);
|
|
160
|
+
try {
|
|
161
|
+
if ((await page.locator("#responseform").count()) === 0)
|
|
162
|
+
throw new QuizError(`Attempt ${attemptId} isn't open: it's finished, or not yours. Read its review instead.`);
|
|
163
|
+
const pages = await readNavigation(page);
|
|
164
|
+
const last = Math.max(0, ...Object.values(pages));
|
|
165
|
+
const questions = [];
|
|
166
|
+
for (let p = 0; p <= last; p++) {
|
|
167
|
+
if (p > 0)
|
|
168
|
+
await page.goto(new URL(`mod/quiz/attempt.php?attempt=${attemptId}&page=${p}`, siteUrl).href, { waitUntil: "domcontentloaded" });
|
|
169
|
+
for (const question of await readQuestions(page))
|
|
170
|
+
questions.push({ ...question, page: p });
|
|
171
|
+
}
|
|
172
|
+
return questions;
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
await page.close().catch(() => undefined);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Saves answers page by page, as the attempt's "next" button does; `beforeSave` runs with each
|
|
180
|
+
* page's questions (the realistic trail's reading pause). Throws, saving nothing more, on a field
|
|
181
|
+
* a page doesn't have.
|
|
182
|
+
*/
|
|
183
|
+
export async function saveAnswers(context, siteUrl, attemptId, answers, beforeSave) {
|
|
184
|
+
const page = await openSitePage(context, siteUrl, `mod/quiz/attempt.php?attempt=${attemptId}&page=0`);
|
|
185
|
+
try {
|
|
186
|
+
if ((await page.locator("#responseform").count()) === 0)
|
|
187
|
+
throw new QuizError(`Attempt ${attemptId} isn't open: it's finished, or not yours.`);
|
|
188
|
+
const pages = await readNavigation(page);
|
|
189
|
+
const byPage = new Map();
|
|
190
|
+
for (const answer of answers) {
|
|
191
|
+
const p = pages[answer.slot];
|
|
192
|
+
if (p === undefined)
|
|
193
|
+
throw new QuizError(`Attempt ${attemptId} has no question in slot ${answer.slot}.`);
|
|
194
|
+
byPage.set(p, { ...byPage.get(p), ...answer.responses });
|
|
195
|
+
}
|
|
196
|
+
for (const [p, responses] of [...byPage].sort(([a], [b]) => a - b)) {
|
|
197
|
+
await page.goto(new URL(`mod/quiz/attempt.php?attempt=${attemptId}&page=${p}`, siteUrl).href, { waitUntil: "domcontentloaded" });
|
|
198
|
+
await setFields(page, responses, "#responseform");
|
|
199
|
+
await beforeSave?.(await readQuestions(page));
|
|
200
|
+
await Promise.all([page.waitForNavigation({ waitUntil: "domcontentloaded" }), page.click('#responseform [name="next"]')]);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
await page.close().catch(() => undefined);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/** Submits the attempt for grading, as "Submit all and finish" does, without its confirm dialog. */
|
|
208
|
+
export async function finishAttempt(context, siteUrl, attemptId) {
|
|
209
|
+
const page = await openSitePage(context, siteUrl, `mod/quiz/summary.php?attempt=${attemptId}`);
|
|
210
|
+
try {
|
|
211
|
+
const form = page.locator('form[action*="processattempt.php"]:has([name="finishattempt"])');
|
|
212
|
+
if ((await form.count()) === 0)
|
|
213
|
+
throw new QuizError(`Attempt ${attemptId} can't be finished: it's already finished, or not yours.`);
|
|
214
|
+
await Promise.all([page.waitForNavigation({ waitUntil: "domcontentloaded" }), form.evaluate((f) => f.submit())]);
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
await page.close().catch(() => undefined);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** A finished attempt's review, as far as the quiz's review options let the person see it. */
|
|
221
|
+
export async function readReview(context, siteUrl, attemptId) {
|
|
222
|
+
const page = await openSitePage(context, siteUrl, `mod/quiz/review.php?attempt=${attemptId}&showall=1`);
|
|
223
|
+
try {
|
|
224
|
+
if ((await page.locator("body#page-mod-quiz-review").count()) === 0)
|
|
225
|
+
throw new QuizError(`Attempt ${attemptId}'s review isn't available to you (not finished, not yours, or the quiz doesn't show reviews yet).`);
|
|
226
|
+
const summary = await page.evaluate(() => Array.from(globalThis.document.querySelectorAll("table.quizreviewsummary tr")).map((row) => ({ label: (row.querySelector("th")?.innerText ?? "").trim(), value: (row.querySelector("td")?.innerText ?? "").replace(/\s+/g, " ").trim() })));
|
|
227
|
+
return { summary, questions: await readQuestions(page) };
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
await page.close().catch(() => undefined);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** Starts an attempt, or continues the one in progress, and gives its questions. */
|
|
234
|
+
export async function startQuizAttempt(context, siteUrl, quizId) {
|
|
235
|
+
const attemptId = await startAttempt(context, siteUrl, quizId);
|
|
236
|
+
return { attemptId, questions: await readAttempt(context, siteUrl, attemptId) };
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Saves answers in an attempt in progress without finishing it, and gives its questions afterwards.
|
|
240
|
+
* With the realistic trail, it first takes the time a reader needs for the questions and their
|
|
241
|
+
* options (ADR-005), calling `onProgress` with the seconds elapsed and the total.
|
|
242
|
+
*/
|
|
243
|
+
export async function answerQuizQuestions(context, siteUrl, attemptId, answers, { trail = "realistic", onProgress } = {}) {
|
|
244
|
+
const beforeSave = trail === "realistic"
|
|
245
|
+
? async (questions) => {
|
|
246
|
+
const seconds = readingSeconds(questions.map((q) => [q.text, ...q.fields.flatMap((f) => [f.label ?? "", ...(f.options ?? []).map((o) => o.label)])].join(" ")).join(" "));
|
|
247
|
+
await pause(seconds, (elapsed) => onProgress?.(elapsed, seconds));
|
|
248
|
+
}
|
|
249
|
+
: undefined;
|
|
250
|
+
await saveAnswers(context, siteUrl, attemptId, answers, beforeSave);
|
|
251
|
+
return { attemptId, questions: await readAttempt(context, siteUrl, attemptId) };
|
|
252
|
+
}
|
|
253
|
+
/** Submits an attempt for grading, and gives its review when the quiz shows it. */
|
|
254
|
+
export async function finishQuizAttempt(context, siteUrl, attemptId) {
|
|
255
|
+
await finishAttempt(context, siteUrl, attemptId);
|
|
256
|
+
const review = await readReview(context, siteUrl, attemptId).catch(() => undefined);
|
|
257
|
+
return { attemptId, finished: true, review };
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Sets a user override on a quiz, through its override form: other opening and closing dates, a time
|
|
261
|
+
* limit in minutes, or a number of attempts (0 unlimited); what isn't given stays the quiz's.
|
|
262
|
+
*/
|
|
263
|
+
export async function setQuizOverride(context, siteUrl, quizId, userId, { open, close, timeLimitMinutes, attempts }) {
|
|
264
|
+
const page = await openForm(context, siteUrl, `mod/quiz/overrideedit.php?action=adduser&cmid=${quizId}`).catch((error) => {
|
|
265
|
+
if (error instanceof NotLoggedInError)
|
|
266
|
+
throw error;
|
|
267
|
+
throw new QuizError(`You can't add overrides to activity ${quizId}: it isn't a quiz, or you aren't allowed.`);
|
|
268
|
+
});
|
|
269
|
+
try {
|
|
270
|
+
const values = { userid: String(userId) };
|
|
271
|
+
if (open)
|
|
272
|
+
Object.assign(values, { "timeopen[enabled]": true, ...(await dateSelectorValues(page, "timeopen", open)) });
|
|
273
|
+
if (close)
|
|
274
|
+
Object.assign(values, { "timeclose[enabled]": true, ...(await dateSelectorValues(page, "timeclose", close)) });
|
|
275
|
+
if (timeLimitMinutes !== undefined)
|
|
276
|
+
Object.assign(values, { "timelimit[enabled]": true, "timelimit[number]": timeLimitMinutes, "timelimit[timeunit]": 60 });
|
|
277
|
+
if (attempts !== undefined)
|
|
278
|
+
values.attempts = String(attempts);
|
|
279
|
+
const offered = await page.evaluate((user) => globalThis.document.querySelector(`form.mform select[name="userid"] option[value="${user}"]`) !== null, userId);
|
|
280
|
+
if (!offered)
|
|
281
|
+
throw new QuizError(`User ${userId} can't get an override in quiz ${quizId}: not a participant, or already overridden (change that override in Moodle).`);
|
|
282
|
+
await setFields(page, values);
|
|
283
|
+
await submitForm(page, "submitbutton");
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
await page.close().catch(() => undefined);
|
|
287
|
+
}
|
|
288
|
+
const what = [open && `opens ${open.toISOString()}`, close && `closes ${close.toISOString()}`, timeLimitMinutes !== undefined && `${timeLimitMinutes} minutes`, attempts !== undefined && `${attempts || "unlimited"} attempts`].filter(Boolean);
|
|
289
|
+
return { changed: `Override for user ${userId} in quiz ${quizId}: ${what.join(", ") || "the quiz's settings"}.` };
|
|
290
|
+
}
|
|
291
|
+
//# sourceMappingURL=quiz.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quiz.js","sourceRoot":"","sources":["../../src/moodle/quiz.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from "playwright-core";
|
|
2
|
+
export interface QuizAttemptResult {
|
|
3
|
+
attemptId: number;
|
|
4
|
+
userId: number;
|
|
5
|
+
fullName: string;
|
|
6
|
+
/** As the report shows them, in the site's language. */
|
|
7
|
+
state?: string;
|
|
8
|
+
started?: string;
|
|
9
|
+
completed?: string;
|
|
10
|
+
duration?: string;
|
|
11
|
+
grade?: string;
|
|
12
|
+
questions: {
|
|
13
|
+
slot: number;
|
|
14
|
+
/** The mark (overview) as shown; "-" left out. */
|
|
15
|
+
mark?: string;
|
|
16
|
+
/** The answer given, with `responses` (the responses report). */
|
|
17
|
+
response?: string;
|
|
18
|
+
/** How it was marked: "correct", "incorrect", "partiallycorrect", "notanswered", "requiresgrading"… */
|
|
19
|
+
result?: string;
|
|
20
|
+
}[];
|
|
21
|
+
}
|
|
22
|
+
/** One report page's rows: ids from inputs and links, the grade column by its class, the rest by position from it. */
|
|
23
|
+
export declare function readQuizReport(page: Page, mode: "overview" | "responses"): Promise<QuizAttemptResult[]>;
|
|
24
|
+
/**
|
|
25
|
+
* A quiz's attempts as its grades report shows them: per attempt the user, state, dates, grade and
|
|
26
|
+
* the mark per question; with `responses`, also the answer per question from the responses report.
|
|
27
|
+
* Every page of the report.
|
|
28
|
+
*/
|
|
29
|
+
export declare function quizResults(context: BrowserContext, siteUrl: string, quizId: number, { responses }?: {
|
|
30
|
+
responses?: boolean;
|
|
31
|
+
}): Promise<{
|
|
32
|
+
quiz: {
|
|
33
|
+
id: number;
|
|
34
|
+
};
|
|
35
|
+
attempts: QuizAttemptResult[];
|
|
36
|
+
}>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { inPage } from "./inPage.js";
|
|
2
|
+
import { QuizError } from "./quiz.js";
|
|
3
|
+
import { moodleErrorMessage, openSitePage } from "./session.js";
|
|
4
|
+
/** One report page's rows: ids from inputs and links, the grade column by its class, the rest by position from it. */
|
|
5
|
+
export function readQuizReport(page, mode) {
|
|
6
|
+
return inPage(page, (mode) => {
|
|
7
|
+
const doc = globalThis.document;
|
|
8
|
+
const table = doc.querySelector("#attempts") ?? doc.querySelector("#region-main table.generaltable");
|
|
9
|
+
if (!table)
|
|
10
|
+
return [];
|
|
11
|
+
const clean = (s) => (s ?? "").replace(/\s+/g, " ").trim();
|
|
12
|
+
// The grade column is the bold one that isn't the name's (sticky).
|
|
13
|
+
const headers = Array.from(table.querySelectorAll("thead th"));
|
|
14
|
+
const gradeAt = headers.findIndex((th) => /\bbold\b/.test(th.className) && !/\bsticky-column\b/.test(th.className));
|
|
15
|
+
return Array.from(table.querySelectorAll("tbody tr")).flatMap((row) => {
|
|
16
|
+
const attemptId = Number(row.querySelector('input[name="attemptid[]"]')?.value);
|
|
17
|
+
const user = row.querySelector('td:not(.picture) a[href*="/user/view.php?id="]');
|
|
18
|
+
if (!attemptId || !user)
|
|
19
|
+
return [];
|
|
20
|
+
const cells = Array.from(row.querySelectorAll("td"));
|
|
21
|
+
const at = (i) => (i >= 0 && cells[i] ? clean(cells[i].textContent) || undefined : undefined);
|
|
22
|
+
const questions = cells.flatMap((cell) => {
|
|
23
|
+
const link = cell.querySelector('a[href*="reviewquestion.php"]');
|
|
24
|
+
const slot = Number(new URL(link?.getAttribute("href") ?? "", "http://x/").searchParams.get("slot"));
|
|
25
|
+
if (!link || !slot)
|
|
26
|
+
return [];
|
|
27
|
+
const shown = link.querySelector(".que span:last-child") ?? link;
|
|
28
|
+
const value = clean(shown.textContent);
|
|
29
|
+
const result = (shown.className.split(/\s+/).find(Boolean) ?? "") || undefined;
|
|
30
|
+
return [{ slot, ...(value && value !== "-" ? (mode === "responses" ? { response: value } : { mark: value }) : {}), ...(result ? { result } : {}) }];
|
|
31
|
+
});
|
|
32
|
+
// Overview: state, started, completed and duration come right before the grade; responses: the state alone.
|
|
33
|
+
const before = mode === "overview" ? [4, 3, 2, 1] : [1];
|
|
34
|
+
const [state, started, completed, duration] = before.map((n) => at(gradeAt - n));
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
attemptId,
|
|
38
|
+
userId: Number(new URL(user.getAttribute("href") ?? "", "http://x/").searchParams.get("id")),
|
|
39
|
+
fullName: clean(user.textContent),
|
|
40
|
+
...(state ? { state } : {}),
|
|
41
|
+
...(started ? { started } : {}),
|
|
42
|
+
...(completed ? { completed } : {}),
|
|
43
|
+
...(duration ? { duration } : {}),
|
|
44
|
+
...(at(gradeAt) && at(gradeAt) !== "-" ? { grade: at(gradeAt) } : {}),
|
|
45
|
+
questions,
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
});
|
|
49
|
+
}, mode);
|
|
50
|
+
}
|
|
51
|
+
const MAX_PAGES = 100;
|
|
52
|
+
async function readReport(context, siteUrl, quizId, mode) {
|
|
53
|
+
const all = new Map();
|
|
54
|
+
for (let p = 0; p < MAX_PAGES; p++) {
|
|
55
|
+
const page = await openSitePage(context, siteUrl, `mod/quiz/report.php?id=${quizId}&mode=${mode}&attempts=enrolled_with&page=${p}`);
|
|
56
|
+
try {
|
|
57
|
+
if ((await page.locator("body#page-mod-quiz-report").count()) === 0 || (await moodleErrorMessage(page)) !== undefined) {
|
|
58
|
+
throw new QuizError(`You can't see quiz ${quizId}'s results: ${(await moodleErrorMessage(page)) ?? "it isn't a quiz, or you aren't allowed"}`);
|
|
59
|
+
}
|
|
60
|
+
const rows = await readQuizReport(page, mode);
|
|
61
|
+
const fresh = rows.filter((row) => !all.has(row.attemptId));
|
|
62
|
+
if (fresh.length === 0)
|
|
63
|
+
break;
|
|
64
|
+
for (const row of fresh)
|
|
65
|
+
all.set(row.attemptId, row);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
await page.close().catch(() => undefined);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return Array.from(all.values());
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A quiz's attempts as its grades report shows them: per attempt the user, state, dates, grade and
|
|
75
|
+
* the mark per question; with `responses`, also the answer per question from the responses report.
|
|
76
|
+
* Every page of the report.
|
|
77
|
+
*/
|
|
78
|
+
export async function quizResults(context, siteUrl, quizId, { responses = false } = {}) {
|
|
79
|
+
const attempts = await readReport(context, siteUrl, quizId, "overview");
|
|
80
|
+
if (responses) {
|
|
81
|
+
const answered = new Map((await readReport(context, siteUrl, quizId, "responses")).map((row) => [row.attemptId, row]));
|
|
82
|
+
for (const attempt of attempts) {
|
|
83
|
+
const other = answered.get(attempt.attemptId);
|
|
84
|
+
for (const question of attempt.questions) {
|
|
85
|
+
const response = other?.questions.find((q) => q.slot === question.slot)?.response;
|
|
86
|
+
if (response !== undefined)
|
|
87
|
+
question.response = response;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { quiz: { id: quizId }, attempts };
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=quizResults.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quizResults.js","sourceRoot":"","sources":["../../src/moodle/quizResults.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAuBhE,sHAAsH;AACtH,MAAM,UAAU,cAAc,CAAC,IAAU,EAAE,IAA8B;IACvE,OAAO,MAAM,CACX,IAAI,EACJ,CAAC,IAAY,EAAE,EAAE;QASf,MAAM,GAAG,GAAI,UAA0C,CAAC,QAAQ,CAAC;QACjE,MAAM,KAAK,GAAG,GAAG,CAAC,aAAa,CAAC,WAAW,CAAC,IAAI,GAAG,CAAC,aAAa,CAAC,iCAAiC,CAAC,CAAC;QACrG,IAAI,CAAC,KAAK;YAAE,OAAO,EAAE,CAAC;QACtB,MAAM,KAAK,GAAG,CAAC,CAA4B,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACtF,mEAAmE;QACnE,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAAC;QAC/D,MAAM,OAAO,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;QACpH,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;YACpE,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,2BAA2B,CAAC,EAAE,KAAK,CAAC,CAAC;YAChF,MAAM,IAAI,GAAG,GAAG,CAAC,aAAa,CAAC,gDAAgD,CAAC,CAAC;YACjF,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI;gBAAE,OAAO,EAAE,CAAC;YACnC,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;YACrD,MAAM,EAAE,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,WAAW,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;YACvG,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;gBACvC,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,CAAC,+BAA+B,CAAC,CAAC;gBACjE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,WAAW,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;gBACrG,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI;oBAAE,OAAO,EAAE,CAAC;gBAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,sBAAsB,CAAC,IAAI,IAAI,CAAC;gBACjE,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;gBACvC,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,IAAI,SAAS,CAAC;gBAC/E,OAAO,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,KAAK,IAAI,KAAK,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;YACtJ,CAAC,CAAC,CAAC;YACH,4GAA4G;YAC5G,MAAM,MAAM,GAAG,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACxD,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;YACjF,OAAO;gBACL;oBACE,SAAS;oBACT,MAAM,EAAE,MAAM,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,WAAW,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;oBAC5F,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC;oBACjC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC3B,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC/B,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBACnC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBACjC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBACrE,SAAS;iBACV;aACF,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC,EACD,IAAI,CACL,CAAC;AACJ,CAAC;AAED,MAAM,SAAS,GAAG,GAAG,CAAC;AAEtB,KAAK,UAAU,UAAU,CAAC,OAAuB,EAAE,OAAe,EAAE,MAAc,EAAE,IAA8B;IAChH,MAAM,GAAG,GAAG,IAAI,GAAG,EAA6B,CAAC;IACjD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,0BAA0B,MAAM,SAAS,IAAI,gCAAgC,CAAC,EAAE,CAAC,CAAC;QACpI,IAAI,CAAC;YACH,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,kBAAkB,CAAC,IAAI,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;gBACtH,MAAM,IAAI,SAAS,CAAC,sBAAsB,MAAM,eAAe,CAAC,MAAM,kBAAkB,CAAC,IAAI,CAAC,CAAC,IAAI,wCAAwC,EAAE,CAAC,CAAC;YACjJ,CAAC;YACD,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC;YAC5D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,MAAM;YAC9B,KAAK,MAAM,GAAG,IAAI,KAAK;gBAAE,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QACvD,CAAC;gBAAS,CAAC;YACT,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;AAClC,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAuB,EAAE,OAAe,EAAE,MAAc,EAAE,EAAE,SAAS,GAAG,KAAK,KAA8B,EAAE;IAC7I,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;IACxE,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;QACvH,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAC9C,KAAK,MAAM,QAAQ,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;gBACzC,MAAM,QAAQ,GAAG,KAAK,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC;gBAClF,IAAI,QAAQ,KAAK,SAAS;oBAAE,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC;YAC3D,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC;AAC5C,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from "playwright-core";
|
|
2
|
+
export interface QuizSlot {
|
|
3
|
+
/** Moodle's id for the slot, and its question number. */
|
|
4
|
+
slotId: number;
|
|
5
|
+
number?: number;
|
|
6
|
+
page: number;
|
|
7
|
+
/** The question's id; none for a random slot. */
|
|
8
|
+
questionId?: number;
|
|
9
|
+
/** The question type ("multichoice"…), "random" for a random question. */
|
|
10
|
+
type: string;
|
|
11
|
+
name: string;
|
|
12
|
+
mark?: string;
|
|
13
|
+
}
|
|
14
|
+
/** The edit page's slots in order, each with the page it's on; read by ids, classes and links. */
|
|
15
|
+
export declare function readQuizSlots(page: Page): Promise<QuizSlot[]>;
|
|
16
|
+
/** A quiz's questions as its edit page lists them: slots with their page, question, type, name and mark. */
|
|
17
|
+
export declare function quizStructure(context: BrowserContext, siteUrl: string, quizId: number): Promise<{
|
|
18
|
+
quiz: {
|
|
19
|
+
id: number;
|
|
20
|
+
};
|
|
21
|
+
slots: QuizSlot[];
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
24
|
+
* Adds bank questions to a quiz by id, and random questions from a bank category, at the end or on a
|
|
25
|
+
* page, as the edit page's bank actions post them. Returns the quiz's structure afterwards. A quiz
|
|
26
|
+
* already attempted can't change its questions: Moodle refuses, and so does this.
|
|
27
|
+
*/
|
|
28
|
+
export declare function addQuestionsToQuiz(context: BrowserContext, siteUrl: string, quizId: number, { questionIds, random, page: onPage }: {
|
|
29
|
+
questionIds?: number[];
|
|
30
|
+
random?: {
|
|
31
|
+
categoryId: number;
|
|
32
|
+
count: number;
|
|
33
|
+
includeSubcategories?: boolean;
|
|
34
|
+
};
|
|
35
|
+
page?: number;
|
|
36
|
+
}): Promise<{
|
|
37
|
+
changed: string;
|
|
38
|
+
quiz: {
|
|
39
|
+
id: number;
|
|
40
|
+
};
|
|
41
|
+
slots: QuizSlot[];
|
|
42
|
+
}>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { QuizError } from "./quiz.js";
|
|
2
|
+
import { moodleErrorMessage, openSitePage } from "./session.js";
|
|
3
|
+
/** The edit page's slots in order, each with the page it's on; read by ids, classes and links. */
|
|
4
|
+
export function readQuizSlots(page) {
|
|
5
|
+
return page.evaluate(() => {
|
|
6
|
+
const doc = globalThis.document;
|
|
7
|
+
const slots = [];
|
|
8
|
+
let current = 0;
|
|
9
|
+
for (const item of Array.from(doc.querySelectorAll("li.page, li.slot"))) {
|
|
10
|
+
const page = /^page-(\d+)$/.exec(item.id);
|
|
11
|
+
if (page) {
|
|
12
|
+
current = Number(page[1]);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const slotId = Number(/^slot-(\d+)$/.exec(item.id)?.[1]);
|
|
16
|
+
if (!slotId)
|
|
17
|
+
continue;
|
|
18
|
+
// A random slot (a question set reference in Moodle 5) has an empty "qtype_" class (confirmed empirically, 5.2).
|
|
19
|
+
const type = /\bqtype_([a-z0-9_]*)/.exec(item.className)?.[1] || "random";
|
|
20
|
+
const edit = item.querySelector('a[href*="/question/bank/editquestion/question.php"]');
|
|
21
|
+
const questionId = Number(new URL(edit?.getAttribute("href") ?? "", "http://x/").searchParams.get("id")) || undefined;
|
|
22
|
+
const number = Number(((item.querySelector(".slotnumber.unshuffled") ?? item.querySelector(".slotnumber"))?.textContent ?? "").replace(/\D+/g, "")) || undefined;
|
|
23
|
+
const mark = (item.querySelector(".instancemaxmark")?.textContent ?? "").replace(/\s+/g, " ").trim() || undefined;
|
|
24
|
+
slots.push({
|
|
25
|
+
slotId,
|
|
26
|
+
...(number ? { number } : {}),
|
|
27
|
+
page: current,
|
|
28
|
+
...(questionId && type !== "random" ? { questionId } : {}),
|
|
29
|
+
type,
|
|
30
|
+
name: (item.querySelector(".questionname")?.textContent ?? item.querySelector(".instancename")?.textContent ?? "").replace(/\s+/g, " ").trim(),
|
|
31
|
+
...(mark ? { mark } : {}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return slots;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function openEditPage(context, siteUrl, quizId) {
|
|
38
|
+
const page = await openSitePage(context, siteUrl, `mod/quiz/edit.php?cmid=${quizId}`);
|
|
39
|
+
if ((await page.locator("body#page-mod-quiz-edit").count()) === 0) {
|
|
40
|
+
const reason = await moodleErrorMessage(page);
|
|
41
|
+
await page.close().catch(() => undefined);
|
|
42
|
+
throw new QuizError(`You can't edit quiz ${quizId}: ${reason ?? "it isn't a quiz, or you aren't allowed"}.`);
|
|
43
|
+
}
|
|
44
|
+
return page;
|
|
45
|
+
}
|
|
46
|
+
/** A quiz's questions as its edit page lists them: slots with their page, question, type, name and mark. */
|
|
47
|
+
export async function quizStructure(context, siteUrl, quizId) {
|
|
48
|
+
const page = await openEditPage(context, siteUrl, quizId);
|
|
49
|
+
try {
|
|
50
|
+
return { quiz: { id: quizId }, slots: await readQuizSlots(page) };
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await page.close().catch(() => undefined);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Adds bank questions to a quiz by id, and random questions from a bank category, at the end or on a
|
|
58
|
+
* page, as the edit page's bank actions post them. Returns the quiz's structure afterwards. A quiz
|
|
59
|
+
* already attempted can't change its questions: Moodle refuses, and so does this.
|
|
60
|
+
*/
|
|
61
|
+
export async function addQuestionsToQuiz(context, siteUrl, quizId, { questionIds = [], random, page: onPage }) {
|
|
62
|
+
if (questionIds.length === 0 && !random)
|
|
63
|
+
throw new QuizError("Give the questions to add: their ids, or a category to draw random ones from.");
|
|
64
|
+
const page = await openEditPage(context, siteUrl, quizId);
|
|
65
|
+
try {
|
|
66
|
+
// The page's add menus are empty when the quiz can't change (it has attempts).
|
|
67
|
+
if ((await page.locator(".add-menu-outer *").count()) === 0) {
|
|
68
|
+
throw new QuizError(`Quiz ${quizId} can't change its questions now: it has attempts already. Make a new quiz, or delete its attempts in Moodle.`);
|
|
69
|
+
}
|
|
70
|
+
const before = (await readQuizSlots(page)).length;
|
|
71
|
+
const posts = [];
|
|
72
|
+
if (questionIds.length > 0)
|
|
73
|
+
posts.push({ add: "1", addonpage: String(onPage ?? 0), ...Object.fromEntries(questionIds.map((id) => [`q${id}`, "1"])) });
|
|
74
|
+
if (random)
|
|
75
|
+
posts.push({ addrandom: "1", addonpage: String(onPage ?? 0), categoryid: String(random.categoryId), randomcount: String(random.count), recurse: random.includeSubcategories ? "1" : "0" });
|
|
76
|
+
for (const fields of posts) {
|
|
77
|
+
const status = await page.evaluate(async ({ fields, quizId }) => {
|
|
78
|
+
const g = globalThis;
|
|
79
|
+
const response = await g.fetch(`${g.M.cfg.wwwroot}/mod/quiz/edit.php?cmid=${quizId}`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
credentials: "same-origin",
|
|
82
|
+
body: new g.URLSearchParams({ ...fields, sesskey: g.M.cfg.sesskey }),
|
|
83
|
+
});
|
|
84
|
+
return response.status;
|
|
85
|
+
}, { fields, quizId });
|
|
86
|
+
if (status >= 400)
|
|
87
|
+
throw new QuizError(`Moodle answered ${status} adding questions to quiz ${quizId}: check the questions' ids and the category.`);
|
|
88
|
+
}
|
|
89
|
+
await page.reload({ waitUntil: "domcontentloaded" });
|
|
90
|
+
const slots = await readQuizSlots(page);
|
|
91
|
+
const added = slots.length - before;
|
|
92
|
+
if (added <= 0)
|
|
93
|
+
throw new QuizError(`Moodle added no question to quiz ${quizId}: check the questions' ids (from the bank) and the category.`);
|
|
94
|
+
return { changed: `Added ${added} question${added === 1 ? "" : "s"} to quiz ${quizId}.`, quiz: { id: quizId }, slots };
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
await page.close().catch(() => undefined);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=quizStructure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quizStructure.js","sourceRoot":"","sources":["../../src/moodle/quizStructure.ts"],"names":[],"mappings":"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"}
|