@falkenslab/moodle-driver 0.4.1 → 0.5.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.
Files changed (185) hide show
  1. package/README.md +3 -2
  2. package/dist/areas/activities/build.d.ts +47 -10
  3. package/dist/areas/activities/build.js +157 -24
  4. package/dist/areas/activities/build.js.map +1 -1
  5. package/dist/areas/activities/conditions.d.ts +77 -0
  6. package/dist/areas/activities/conditions.js +216 -0
  7. package/dist/areas/activities/conditions.js.map +1 -0
  8. package/dist/areas/activities/content.d.ts +3 -0
  9. package/dist/areas/activities/content.js +1 -0
  10. package/dist/areas/activities/content.js.map +1 -1
  11. package/dist/areas/assignments/grader.d.ts +32 -0
  12. package/dist/areas/assignments/grader.js +66 -2
  13. package/dist/areas/assignments/grader.js.map +1 -1
  14. package/dist/areas/assignments/grading.d.ts +69 -6
  15. package/dist/areas/assignments/grading.js +173 -13
  16. package/dist/areas/assignments/grading.js.map +1 -1
  17. package/dist/areas/assignments/student.d.ts +14 -0
  18. package/dist/areas/assignments/student.js +43 -2
  19. package/dist/areas/assignments/student.js.map +1 -1
  20. package/dist/areas/backup/backup.d.ts +86 -0
  21. package/dist/areas/backup/backup.js +368 -0
  22. package/dist/areas/backup/backup.js.map +1 -0
  23. package/dist/areas/calendar/calendar.d.ts +27 -0
  24. package/dist/areas/calendar/calendar.js +115 -9
  25. package/dist/areas/calendar/calendar.js.map +1 -1
  26. package/dist/areas/completion/completion.d.ts +3 -0
  27. package/dist/areas/completion/completion.js.map +1 -1
  28. package/dist/areas/courses/list.d.ts +2 -0
  29. package/dist/areas/courses/list.js.map +1 -1
  30. package/dist/areas/courses/reset.d.ts +51 -0
  31. package/dist/areas/courses/reset.js +123 -0
  32. package/dist/areas/courses/reset.js.map +1 -0
  33. package/dist/areas/courses/state.d.ts +17 -0
  34. package/dist/areas/courses/state.js +37 -6
  35. package/dist/areas/courses/state.js.map +1 -1
  36. package/dist/areas/courses/structure.d.ts +14 -2
  37. package/dist/areas/courses/structure.js +11 -5
  38. package/dist/areas/courses/structure.js.map +1 -1
  39. package/dist/areas/forums/discussions.d.ts +8 -0
  40. package/dist/areas/forums/discussions.js +10 -0
  41. package/dist/areas/forums/discussions.js.map +1 -1
  42. package/dist/areas/forums/grading.d.ts +49 -0
  43. package/dist/areas/forums/grading.js +125 -0
  44. package/dist/areas/forums/grading.js.map +1 -0
  45. package/dist/areas/forums/posts.d.ts +37 -0
  46. package/dist/areas/forums/posts.js +141 -6
  47. package/dist/areas/forums/posts.js.map +1 -1
  48. package/dist/areas/grades/export.d.ts +38 -0
  49. package/dist/areas/grades/export.js +88 -0
  50. package/dist/areas/grades/export.js.map +1 -0
  51. package/dist/areas/grades/gradebook.d.ts +50 -0
  52. package/dist/areas/grades/gradebook.js +120 -0
  53. package/dist/areas/grades/gradebook.js.map +1 -1
  54. package/dist/areas/grades/singleview.d.ts +39 -0
  55. package/dist/areas/grades/singleview.js +186 -0
  56. package/dist/areas/grades/singleview.js.map +1 -0
  57. package/dist/areas/h5p/h5p.d.ts +2 -0
  58. package/dist/areas/h5p/h5p.js +12 -7
  59. package/dist/areas/h5p/h5p.js.map +1 -1
  60. package/dist/areas/lessons/lessons.d.ts +1 -0
  61. package/dist/areas/lessons/lessons.js.map +1 -1
  62. package/dist/areas/messages/messages.d.ts +32 -0
  63. package/dist/areas/messages/messages.js +44 -2
  64. package/dist/areas/messages/messages.js.map +1 -1
  65. package/dist/areas/participants/enrolments.d.ts +60 -0
  66. package/dist/areas/participants/enrolments.js +218 -0
  67. package/dist/areas/participants/enrolments.js.map +1 -0
  68. package/dist/areas/participants/groups.d.ts +75 -0
  69. package/dist/areas/participants/groups.js +201 -0
  70. package/dist/areas/participants/groups.js.map +1 -0
  71. package/dist/areas/participants/methods.d.ts +56 -0
  72. package/dist/areas/participants/methods.js +154 -0
  73. package/dist/areas/participants/methods.js.map +1 -0
  74. package/dist/areas/participants/participants.d.ts +11 -0
  75. package/dist/areas/participants/participants.js +24 -19
  76. package/dist/areas/participants/participants.js.map +1 -1
  77. package/dist/areas/questions/banks.d.ts +36 -1
  78. package/dist/areas/questions/banks.js +140 -17
  79. package/dist/areas/questions/banks.js.map +1 -1
  80. package/dist/areas/quizzes/attempts.d.ts +2 -0
  81. package/dist/areas/quizzes/attempts.js +9 -0
  82. package/dist/areas/quizzes/attempts.js.map +1 -1
  83. package/dist/areas/quizzes/grading.d.ts +50 -0
  84. package/dist/areas/quizzes/grading.js +131 -0
  85. package/dist/areas/quizzes/grading.js.map +1 -0
  86. package/dist/areas/quizzes/results.d.ts +3 -0
  87. package/dist/areas/quizzes/results.js +2 -1
  88. package/dist/areas/quizzes/results.js.map +1 -1
  89. package/dist/areas/quizzes/structure.d.ts +65 -9
  90. package/dist/areas/quizzes/structure.js +222 -23
  91. package/dist/areas/quizzes/structure.js.map +1 -1
  92. package/dist/areas/ratings/ratings.d.ts +26 -0
  93. package/dist/areas/ratings/ratings.js +108 -0
  94. package/dist/areas/ratings/ratings.js.map +1 -0
  95. package/dist/areas/reports/dates.d.ts +18 -0
  96. package/dist/areas/reports/dates.js +134 -0
  97. package/dist/areas/reports/dates.js.map +1 -0
  98. package/dist/areas/reports/reports.d.ts +143 -0
  99. package/dist/areas/reports/reports.js +267 -0
  100. package/dist/areas/reports/reports.js.map +1 -0
  101. package/dist/areas/scorm/scorm.d.ts +1 -0
  102. package/dist/areas/scorm/scorm.js.map +1 -1
  103. package/dist/areas/site/users.d.ts +7 -1
  104. package/dist/areas/site/users.js +9 -2
  105. package/dist/areas/site/users.js.map +1 -1
  106. package/dist/areas/surveys/choice.d.ts +2 -0
  107. package/dist/areas/surveys/choice.js +6 -3
  108. package/dist/areas/surveys/choice.js.map +1 -1
  109. package/dist/areas/surveys/surveys.d.ts +3 -0
  110. package/dist/areas/surveys/surveys.js +1 -0
  111. package/dist/areas/surveys/surveys.js.map +1 -1
  112. package/dist/areas/workshops/workshops.d.ts +133 -0
  113. package/dist/areas/workshops/workshops.js +335 -0
  114. package/dist/areas/workshops/workshops.js.map +1 -0
  115. package/dist/browser/background.d.ts +43 -0
  116. package/dist/browser/background.js +164 -0
  117. package/dist/browser/background.js.map +1 -0
  118. package/dist/browser/host.d.ts +30 -0
  119. package/dist/browser/host.js +373 -0
  120. package/dist/browser/host.js.map +1 -0
  121. package/dist/browser/manager.d.ts +9 -0
  122. package/dist/browser/manager.js +8 -0
  123. package/dist/browser/manager.js.map +1 -1
  124. package/dist/categories.d.ts +1 -0
  125. package/dist/categories.js +67 -0
  126. package/dist/categories.js.map +1 -1
  127. package/dist/cli/args.d.ts +8 -0
  128. package/dist/cli/args.js +49 -0
  129. package/dist/cli/args.js.map +1 -1
  130. package/dist/cli/background.d.ts +23 -0
  131. package/dist/cli/background.js +128 -0
  132. package/dist/cli/background.js.map +1 -0
  133. package/dist/cli/command.d.ts +66 -0
  134. package/dist/cli/command.js +328 -0
  135. package/dist/cli/command.js.map +1 -0
  136. package/dist/cli/commands.generated.js +2635 -577
  137. package/dist/cli/commands.generated.js.map +1 -1
  138. package/dist/cli/config.d.ts +2 -0
  139. package/dist/cli/config.js +2 -0
  140. package/dist/cli/config.js.map +1 -1
  141. package/dist/cli/main.js +30 -10
  142. package/dist/cli/main.js.map +1 -1
  143. package/dist/cli/run.d.ts +3 -14
  144. package/dist/cli/run.js +9 -185
  145. package/dist/cli/run.js.map +1 -1
  146. package/dist/cli/shell.d.ts +9 -0
  147. package/dist/cli/shell.js +233 -0
  148. package/dist/cli/shell.js.map +1 -0
  149. package/dist/client.d.ts +10 -120
  150. package/dist/client.js +77 -8
  151. package/dist/client.js.map +1 -1
  152. package/dist/core/errors.d.ts +1 -1
  153. package/dist/core/errors.js.map +1 -1
  154. package/dist/core/files.d.ts +5 -0
  155. package/dist/core/files.js +7 -3
  156. package/dist/core/files.js.map +1 -1
  157. package/dist/core/forms.d.ts +33 -3
  158. package/dist/core/forms.js +105 -8
  159. package/dist/core/forms.js.map +1 -1
  160. package/dist/core/http.d.ts +7 -0
  161. package/dist/core/http.js +28 -7
  162. package/dist/core/http.js.map +1 -1
  163. package/dist/core/login.d.ts +3 -0
  164. package/dist/core/login.js +2 -0
  165. package/dist/core/login.js.map +1 -1
  166. package/dist/core/session.d.ts +12 -1
  167. package/dist/core/session.js +12 -1
  168. package/dist/core/session.js.map +1 -1
  169. package/dist/index.d.ts +35 -11
  170. package/dist/index.js +32 -12
  171. package/dist/index.js.map +1 -1
  172. package/dist/internals.d.ts +9 -1
  173. package/dist/internals.js +9 -4
  174. package/dist/internals.js.map +1 -1
  175. package/dist/moodle-methods.generated.d.ts +1127 -0
  176. package/dist/moodle-methods.generated.js +4 -0
  177. package/dist/moodle-methods.generated.js.map +1 -0
  178. package/dist/options.d.ts +2 -0
  179. package/dist/options.js.map +1 -1
  180. package/dist/testing/fakeMoodle.d.ts +9 -2
  181. package/dist/testing/fakeMoodle.js +5 -3
  182. package/dist/testing/fakeMoodle.js.map +1 -1
  183. package/dist/testing/index.d.ts +7 -0
  184. package/dist/testing/index.js.map +1 -1
  185. package/package.json +2 -1
@@ -0,0 +1,131 @@
1
+ import { FormError } from "../../core/forms.js";
2
+ import { inPage } from "../../core/inPage.js";
3
+ import { moodleErrorMessage, openSitePage } from "../../core/session.js";
4
+ import { QuizError } from "./attempts.js";
5
+ import { readReport } from "./results.js";
6
+ /** The manual grading report's list of questions: slot and question from its links, type from its icon, counts by column. */
7
+ export function readQuestionsToGrade(page) {
8
+ return inPage(page, () => {
9
+ const doc = globalThis.document;
10
+ const count = (el) => Number(/\d+/.exec(el?.textContent ?? "")?.[0] ?? 0);
11
+ return Array.from(doc.querySelectorAll("table#questionstograde tbody tr")).flatMap((row) => {
12
+ const cells = Array.from(row.querySelectorAll("td"));
13
+ const link = row.querySelector('a.gradetheselink[href*="slot="]');
14
+ if (!link || cells.length < 6)
15
+ return [];
16
+ const params = new URL(link.getAttribute("href") ?? "", "http://x/").searchParams;
17
+ const slot = Number(params.get("slot"));
18
+ if (!slot)
19
+ return [];
20
+ const questionId = Number(params.get("qid")) || undefined;
21
+ // Columns: number, type, name, to grade, already graded, (automatically graded,) total.
22
+ return [
23
+ {
24
+ slot,
25
+ ...(questionId ? { questionId } : {}),
26
+ name: (cells[2]?.textContent ?? "").replace(/\s+/g, " ").trim(),
27
+ type: /\/qtype_([a-z0-9_]+)\//.exec(cells[1]?.querySelector("img")?.getAttribute("src") ?? "")?.[1] ?? "",
28
+ toGrade: count(cells[3]),
29
+ graded: count(cells[4]),
30
+ total: count(cells[cells.length - 1]),
31
+ },
32
+ ];
33
+ });
34
+ }, null);
35
+ }
36
+ /**
37
+ * The questions of a quiz that wait for grading by hand (essays…), as its manual grading report lists
38
+ * them, each with the finished attempts whose answer requires grading (from the results report).
39
+ * People as user id and full name.
40
+ */
41
+ export async function questionsToGrade(context, siteUrl, activityId) {
42
+ const page = await openSitePage(context, siteUrl, `mod/quiz/report.php?id=${activityId}&mode=grading`);
43
+ let listed;
44
+ try {
45
+ if ((await page.locator("body#page-mod-quiz-report").count()) === 0 || (await moodleErrorMessage(page)) !== undefined) {
46
+ throw new QuizError(`You can't grade quiz ${activityId}: ${((await moodleErrorMessage(page)) ?? "it isn't a quiz, or you aren't allowed").replace(/\.$/, "")}.`);
47
+ }
48
+ listed = await readQuestionsToGrade(page);
49
+ }
50
+ finally {
51
+ await page.close().catch(() => undefined);
52
+ }
53
+ const waiting = listed.some((q) => q.toGrade > 0) ? await readReport(context, siteUrl, activityId, "overview") : [];
54
+ return {
55
+ quiz: { id: activityId },
56
+ questions: listed.map((question) => ({
57
+ ...question,
58
+ attempts: waiting
59
+ .filter((attempt) => attempt.questions.some((q) => q.slot === question.slot && q.result === "requiresgrading"))
60
+ .map((attempt) => ({ id: attempt.id, userId: attempt.userId, fullName: attempt.fullName })),
61
+ })),
62
+ };
63
+ }
64
+ /**
65
+ * Grades one question of a finished attempt by hand, with a comment, through Moodle's comment form
66
+ * (the review's "Make comment or override mark"): an essay waiting for grading, or any question whose
67
+ * mark the teacher overrides. The mark is in the question's points (0 to its maximum in the quiz).
68
+ * Returns the attempt's grade afterwards when the results report shows it.
69
+ */
70
+ export async function gradeQuizQuestion(context, siteUrl, attemptId, slot, { mark, comment }) {
71
+ if (!Number.isFinite(mark))
72
+ throw new QuizError("Give the mark as a number of points.");
73
+ const page = await openSitePage(context, siteUrl, `mod/quiz/comment.php?attempt=${attemptId}&slot=${slot}`);
74
+ let activityId;
75
+ let maxMark;
76
+ try {
77
+ if ((await page.locator("body#page-mod-quiz-comment form#manualgradingform").count()) === 0) {
78
+ throw new QuizError(`You can't grade question ${slot} of attempt ${attemptId}: ${((await moodleErrorMessage(page)) ?? "the attempt isn't finished, the slot isn't in it, or you aren't allowed").replace(/\.$/, "")}.`);
79
+ }
80
+ // The course module id is in the body's classes (cmid-<id>).
81
+ activityId = Number(/\bcmid-(\d+)\b/.exec((await page.locator("body").getAttribute("class")) ?? "")?.[1]);
82
+ const outcome = await page.evaluate(async ({ mark, comment }) => {
83
+ const g = globalThis;
84
+ const form = g.document.querySelector("form#manualgradingform");
85
+ const fields = Array.from(form.querySelectorAll("[name]"));
86
+ const markField = fields.find((f) => /_-mark$/.test(f.name));
87
+ const commentField = fields.find((f) => /_-comment$/.test(f.name));
88
+ const maxMark = fields.find((f) => /_-maxmark$/.test(f.name))?.value;
89
+ if (!markField)
90
+ return { kind: "nomark" };
91
+ // Sent as the form would be, URL-encoded (it has no files).
92
+ const data = new g.URLSearchParams(new g.FormData(form));
93
+ data.set(markField.name, String(mark));
94
+ if (comment !== undefined && commentField)
95
+ data.set(commentField.name, comment);
96
+ data.set("submit", "1");
97
+ const response = await g.fetch(form.action, { method: "POST", body: data, credentials: "same-origin" });
98
+ const answer = new g.DOMParser().parseFromString(await response.text(), "text/html");
99
+ // A mark out of range or badly written: the form again, its mark marked as an error.
100
+ const fieldError = (answer.querySelector(".que .comment span.error")?.textContent ?? "").replace(/\s+/g, " ").trim();
101
+ if (fieldError)
102
+ return { kind: "field", message: fieldError, maxMark };
103
+ if (answer.querySelector("form#manualgradingform")) {
104
+ return { kind: "refused", message: (answer.querySelector("#region-main .alert-danger, #region-main .alert-error")?.textContent ?? "").replace(/\s+/g, " ").trim(), maxMark };
105
+ }
106
+ if (response.status >= 400)
107
+ return { kind: "refused", message: `HTTP ${response.status}`, maxMark };
108
+ return { kind: "saved", maxMark };
109
+ }, { mark, comment });
110
+ if (outcome.kind === "nomark")
111
+ throw new QuizError(`Question ${slot} of attempt ${attemptId} has no mark to set: it's a description, or not graded.`);
112
+ maxMark = outcome.maxMark !== undefined ? Number(outcome.maxMark) : undefined;
113
+ if (outcome.kind === "field")
114
+ throw new FormError(`Moodle refused the mark for question ${slot} of attempt ${attemptId}:`, [{ field: "mark", message: outcome.message }]);
115
+ if (outcome.kind === "refused")
116
+ throw new QuizError(`Moodle didn't save the grade for question ${slot} of attempt ${attemptId}: ${outcome.message || "it showed the form again"}.`);
117
+ }
118
+ finally {
119
+ await page.close().catch(() => undefined);
120
+ }
121
+ const grade = activityId ? (await readReport(context, siteUrl, activityId, "overview").catch(() => [])).find((a) => a.id === attemptId)?.grade : undefined;
122
+ return {
123
+ changed: `Graded question ${slot} of attempt ${attemptId}: ${mark}${maxMark !== undefined ? ` out of ${maxMark}` : ""}${comment !== undefined ? ", with a comment" : ""}${grade ? `; the attempt's grade is now ${grade}` : ""}.`,
124
+ attemptId,
125
+ slot,
126
+ mark,
127
+ ...(maxMark !== undefined ? { maxMark } : {}),
128
+ ...(grade ? { grade } : {}),
129
+ };
130
+ }
131
+ //# sourceMappingURL=grading.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grading.js","sourceRoot":"","sources":["../../../src/areas/quizzes/grading.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import type { BrowserContext, Page } from "playwright-core";
2
+ /** An attempt at a quiz, as `quizResults` reads it from the quiz's reports. */
2
3
  export interface QuizAttemptResult {
3
4
  id: number;
4
5
  userId: number;
@@ -21,6 +22,8 @@ export interface QuizAttemptResult {
21
22
  }
22
23
  /** One report page's rows: ids from inputs and links, the grade column by its class, the rest by position from it. */
23
24
  export declare function readQuizReport(page: Page, mode: "overview" | "responses"): Promise<QuizAttemptResult[]>;
25
+ /** Every page of one of a quiz's reports, its attempts once each. */
26
+ export declare function readReport(context: BrowserContext, siteUrl: string, activityId: number, mode: "overview" | "responses"): Promise<QuizAttemptResult[]>;
24
27
  /**
25
28
  * A quiz's attempts as its grades report shows them: per attempt the user, state, dates, grade and
26
29
  * the mark per question; with `responses`, also the answer per question from the responses report.
@@ -49,7 +49,8 @@ export function readQuizReport(page, mode) {
49
49
  }, mode);
50
50
  }
51
51
  const MAX_PAGES = 100;
52
- async function readReport(context, siteUrl, activityId, mode) {
52
+ /** Every page of one of a quiz's reports, its attempts once each. */
53
+ export async function readReport(context, siteUrl, activityId, mode) {
53
54
  const all = new Map();
54
55
  for (let p = 0; p < MAX_PAGES; p++) {
55
56
  const page = await openSitePage(context, siteUrl, `mod/quiz/report.php?id=${activityId}&mode=${mode}&attempts=enrolled_with&page=${p}`);
@@ -1 +1 @@
1
- {"version":3,"file":"results.js","sourceRoot":"","sources":["../../../src/areas/quizzes/results.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"results.js","sourceRoot":"","sources":["../../../src/areas/quizzes/results.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,11 @@
1
1
  import type { BrowserContext, Page } from "playwright-core";
2
+ /** A place for a question in a quiz, as `quizStructure` reads it from the quiz's edit page. */
2
3
  export interface QuizSlot {
3
- /** Moodle's id for the slot, and its question number. */
4
+ /** Moodle's id for the slot. */
4
5
  id: number;
6
+ /** The slot's place in the quiz, from 1: what the other functions take as `slot`. */
7
+ slot: number;
8
+ /** The question's number as shown (descriptions have none). */
5
9
  number?: number;
6
10
  page: number;
7
11
  /** The question's id; none for a random slot. */
@@ -11,15 +15,39 @@ export interface QuizSlot {
11
15
  name: string;
12
16
  mark?: string;
13
17
  }
14
- /** The edit page's slots in order, each with the page it's on; read by ids, classes and links. */
18
+ /** A section of a quiz: a heading before a group of pages, whose questions may be shuffled. */
19
+ export interface QuizSection {
20
+ id: number;
21
+ /** Empty for an untitled section (the first one, by default). */
22
+ heading: string;
23
+ /** The slot it starts at. */
24
+ firstSlot: number;
25
+ shuffle: boolean;
26
+ }
27
+ /** The edit page's sections and slots in order, each slot with the page it's on; read by ids, classes and links. */
28
+ export declare function readQuizEditor(page: Page): Promise<{
29
+ sections: QuizSection[];
30
+ slots: QuizSlot[];
31
+ }>;
32
+ /** The edit page's slots in order, each with the page it's on. */
15
33
  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. */
34
+ /** A quiz's questions as its edit page lists them: slots with their page, question, type, name and mark, and its sections. */
17
35
  export declare function quizStructure(context: BrowserContext, siteUrl: string, activityId: number): Promise<{
18
36
  quiz: {
19
37
  id: number;
20
38
  };
21
39
  slots: QuizSlot[];
40
+ sections: QuizSection[];
22
41
  }>;
42
+ /** What a change to a quiz's structure answers: what changed, and the quiz's slots and sections afterwards. */
43
+ export type QuizChange = {
44
+ changed: string;
45
+ quiz: {
46
+ id: number;
47
+ };
48
+ slots: QuizSlot[];
49
+ sections: QuizSection[];
50
+ };
23
51
  /**
24
52
  * Adds bank questions to a quiz by id, and random questions from a bank category, at the end or on a
25
53
  * page, as the edit page's bank actions post them. Returns the quiz's structure afterwards. A quiz
@@ -33,10 +61,38 @@ export declare function addQuestionsToQuiz(context: BrowserContext, siteUrl: str
33
61
  includeSubcategories?: boolean;
34
62
  };
35
63
  page?: number;
36
- }): Promise<{
37
- changed: string;
38
- quiz: {
39
- id: number;
40
- };
41
- slots: QuizSlot[];
64
+ }): Promise<QuizChange>;
65
+ /**
66
+ * Changes one slot of a quiz (by its place, from 1): its maximum mark, through the slots' service,
67
+ * and where it is, as dragging it on the edit page does: after another slot (`moveAfter`, 0 for
68
+ * first) and on a page. Moodle lets the mark change after attempts (it regrades them), not the rest.
69
+ */
70
+ export declare function editQuizSlot(context: BrowserContext, siteUrl: string, activityId: number, slot: number, { maxMark, moveAfter, page: toPage }: {
71
+ maxMark?: number;
72
+ moveAfter?: number;
73
+ page?: number;
74
+ }): Promise<QuizChange>;
75
+ /** Removes a slot (by its place, from 1) from a quiz, as the edit page's delete icon does; the question stays in its bank. */
76
+ export declare function removeQuizSlot(context: BrowserContext, siteUrl: string, activityId: number, slot: number): Promise<QuizChange>;
77
+ /**
78
+ * Sets a quiz's pages: `perPage` questions a page (0 for all on one), as the edit page's
79
+ * "Repaginate" does, or page breaks exactly after the given slots (and nowhere else), as its page
80
+ * break icons do one by one.
81
+ */
82
+ export declare function setQuizPages(context: BrowserContext, siteUrl: string, activityId: number, pages: {
83
+ perPage: number;
84
+ } | {
85
+ breaksAfter: number[];
86
+ }): Promise<QuizChange>;
87
+ /**
88
+ * Adds a section to a quiz starting at a slot, with its heading and whether its questions are
89
+ * shuffled, as the edit page's "Add a new section heading" and its heading and shuffle controls do.
90
+ * A section starts at a page: the slot must be the first of its page (see `setQuizPages`).
91
+ */
92
+ export declare function addQuizSection(context: BrowserContext, siteUrl: string, activityId: number, { firstSlot, heading, shuffle }: {
93
+ firstSlot: number;
94
+ heading: string;
95
+ shuffle?: boolean;
96
+ }): Promise<QuizChange & {
97
+ sectionId: number;
42
98
  }>;
@@ -1,12 +1,26 @@
1
1
  import { QuizError } from "./attempts.js";
2
+ import { callService } from "../../core/ajax.js";
2
3
  import { moodleErrorMessage, openSitePage } from "../../core/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) {
4
+ /** The edit page's sections and slots in order, each slot with the page it's on; read by ids, classes and links. */
5
+ export function readQuizEditor(page) {
5
6
  return page.evaluate(() => {
6
7
  const doc = globalThis.document;
7
8
  const slots = [];
9
+ const sections = [];
8
10
  let current = 0;
9
- for (const item of Array.from(doc.querySelectorAll("li.page, li.slot"))) {
11
+ for (const item of Array.from(doc.querySelectorAll("li.section, li.page, li.slot"))) {
12
+ const section = /^section-(\d+)$/.exec(item.id);
13
+ if (section) {
14
+ const title = item.querySelector(".instancesection");
15
+ sections.push({
16
+ id: Number(section[1]),
17
+ // An untitled section's heading is a hidden placeholder in the site's language.
18
+ heading: title && !/\bvisually-hidden\b/.test(title.className) ? (title.textContent ?? "").replace(/\s+/g, " ").trim() : "",
19
+ firstSlot: slots.length + 1,
20
+ shuffle: item.querySelector(`#shuffle-${section[1]}`)?.checked ?? false,
21
+ });
22
+ continue;
23
+ }
10
24
  const page = /^page-(\d+)$/.exec(item.id);
11
25
  if (page) {
12
26
  current = Number(page[1]);
@@ -23,6 +37,7 @@ export function readQuizSlots(page) {
23
37
  const mark = (item.querySelector(".instancemaxmark")?.textContent ?? "").replace(/\s+/g, " ").trim() || undefined;
24
38
  slots.push({
25
39
  id: slotId,
40
+ slot: slots.length + 1,
26
41
  ...(number ? { number } : {}),
27
42
  page: current,
28
43
  ...(questionId && type !== "random" ? { questionId } : {}),
@@ -31,9 +46,14 @@ export function readQuizSlots(page) {
31
46
  ...(mark ? { mark } : {}),
32
47
  });
33
48
  }
34
- return slots;
49
+ // A section placed after the last slot would start nowhere: keep the ones that start at a slot.
50
+ return { sections: sections.filter((s) => s.firstSlot <= slots.length), slots };
35
51
  });
36
52
  }
53
+ /** The edit page's slots in order, each with the page it's on. */
54
+ export async function readQuizSlots(page) {
55
+ return (await readQuizEditor(page)).slots;
56
+ }
37
57
  async function openEditPage(context, siteUrl, activityId) {
38
58
  const page = await openSitePage(context, siteUrl, `mod/quiz/edit.php?cmid=${activityId}`);
39
59
  if ((await page.locator("body#page-mod-quiz-edit").count()) === 0) {
@@ -43,16 +63,79 @@ async function openEditPage(context, siteUrl, activityId) {
43
63
  }
44
64
  return page;
45
65
  }
46
- /** A quiz's questions as its edit page lists them: slots with their page, question, type, name and mark. */
66
+ /** A quiz's questions as its edit page lists them: slots with their page, question, type, name and mark, and its sections. */
47
67
  export async function quizStructure(context, siteUrl, activityId) {
48
68
  const page = await openEditPage(context, siteUrl, activityId);
49
69
  try {
50
- return { quiz: { id: activityId }, slots: await readQuizSlots(page) };
70
+ const { slots, sections } = await readQuizEditor(page);
71
+ return { quiz: { id: activityId }, slots, sections };
51
72
  }
52
73
  finally {
53
74
  await page.close().catch(() => undefined);
54
75
  }
55
76
  }
77
+ /** The edit page's add menus are empty when the quiz can't change (it has attempts): Moodle's notice says why. */
78
+ async function checkEditable(page, activityId) {
79
+ if ((await page.locator(".add-menu-outer *").count()) > 0)
80
+ return;
81
+ const notice = ((await page.locator(".statusdisplay").first().textContent().catch(() => null)) ?? "").replace(/\s+/g, " ").trim();
82
+ throw new QuizError(`Quiz ${activityId} can't change its questions now: ${notice || "it has attempts already"}. Make a new quiz, or delete its attempts in Moodle.`);
83
+ }
84
+ /** Posts to the edit page as its forms do (adding, repaginating); Moodle redirects back to it, or shows an error. */
85
+ async function postEditPage(page, activityId, fields) {
86
+ return page.evaluate(async ({ fields, quizId }) => {
87
+ const g = globalThis;
88
+ const response = await g.fetch(`${g.M.cfg.wwwroot}/mod/quiz/edit.php?cmid=${quizId}`, {
89
+ method: "POST",
90
+ credentials: "same-origin",
91
+ body: new g.URLSearchParams({ ...fields, sesskey: g.M.cfg.sesskey }),
92
+ });
93
+ const answer = new g.DOMParser().parseFromString(await response.text(), "text/html");
94
+ const error = (answer.querySelector('[data-rel="fatalerror"] .errormessage')?.textContent ?? "").replace(/\s+/g, " ").trim();
95
+ return { status: response.status, ...(error ? { error } : {}) };
96
+ }, { fields, quizId: activityId });
97
+ }
98
+ /** The quiz's instance id, which the edit page's own requests carry (its toolboxes' settings). */
99
+ async function quizInstanceId(page, activityId) {
100
+ const html = await page.content();
101
+ const id = Number(/"quizid":"?(\d+)/.exec(html)?.[1]);
102
+ if (!id)
103
+ throw new QuizError(`Quiz ${activityId}'s edit page doesn't say which quiz it is: Moodle's page isn't what the driver expects.`);
104
+ return id;
105
+ }
106
+ /** One of the edit page's requests to edit_rest.php, with the sesskey and the quiz; throws Moodle's error as a `QuizError`. */
107
+ async function editRest(page, quizId, activityId, fields) {
108
+ const answer = await page.evaluate(async ({ fields, quizId }) => {
109
+ const g = globalThis;
110
+ const body = { sesskey: g.M.cfg.sesskey, quizid: String(quizId) };
111
+ for (const [key, value] of Object.entries(fields))
112
+ body[key] = String(value);
113
+ const response = await g.fetch(`${g.M.cfg.wwwroot}/mod/quiz/edit_rest.php`, { method: "POST", credentials: "same-origin", body: new g.URLSearchParams(body) });
114
+ return { status: response.status, text: await response.text() };
115
+ }, { fields, quizId });
116
+ let json;
117
+ try {
118
+ json = JSON.parse(answer.text);
119
+ }
120
+ catch {
121
+ throw new QuizError(`Moodle didn't change quiz ${activityId}: its answer (HTTP ${answer.status}) isn't the edit page's JSON.`);
122
+ }
123
+ const error = json?.error;
124
+ if (error)
125
+ throw new QuizError(`Moodle didn't change quiz ${activityId}: ${error.replace(/\.$/, "")}.`);
126
+ return json;
127
+ }
128
+ function slotAt(slots, slot, activityId) {
129
+ const found = slots.find((s) => s.slot === slot);
130
+ if (!found)
131
+ throw new QuizError(`Quiz ${activityId} has no slot ${slot}: it has ${slots.length} (see its structure).`);
132
+ return found;
133
+ }
134
+ async function afterChange(page, activityId, changed) {
135
+ await page.reload({ waitUntil: "domcontentloaded" });
136
+ const { slots, sections } = await readQuizEditor(page);
137
+ return { changed, quiz: { id: activityId }, slots, sections };
138
+ }
56
139
  /**
57
140
  * Adds bank questions to a quiz by id, and random questions from a bank category, at the end or on a
58
141
  * page, as the edit page's bank actions post them. Returns the quiz's structure afterwards. A quiz
@@ -63,10 +146,7 @@ export async function addQuestionsToQuiz(context, siteUrl, activityId, { questio
63
146
  throw new QuizError("Give the questions to add: their ids, or a category to draw random ones from.");
64
147
  const page = await openEditPage(context, siteUrl, activityId);
65
148
  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 ${activityId} can't change its questions now: it has attempts already. Make a new quiz, or delete its attempts in Moodle.`);
69
- }
149
+ await checkEditable(page, activityId);
70
150
  const before = (await readQuizSlots(page)).length;
71
151
  const posts = [];
72
152
  if (questionIds.length > 0)
@@ -74,24 +154,143 @@ export async function addQuestionsToQuiz(context, siteUrl, activityId, { questio
74
154
  if (random)
75
155
  posts.push({ addrandom: "1", addonpage: String(onPage ?? 0), categoryid: String(random.categoryId), randomcount: String(random.count), recurse: random.includeSubcategories ? "1" : "0" });
76
156
  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: activityId });
157
+ const { status } = await postEditPage(page, activityId, fields);
86
158
  if (status >= 400)
87
159
  throw new QuizError(`Moodle answered ${status} adding questions to quiz ${activityId}: check the questions' ids and the category.`);
88
160
  }
89
- await page.reload({ waitUntil: "domcontentloaded" });
90
- const slots = await readQuizSlots(page);
91
- const added = slots.length - before;
161
+ const result = await afterChange(page, activityId, "");
162
+ const added = result.slots.length - before;
92
163
  if (added <= 0)
93
164
  throw new QuizError(`Moodle added no question to quiz ${activityId}: check the questions' ids (from the bank) and the category.`);
94
- return { changed: `Added ${added} question${added === 1 ? "" : "s"} to quiz ${activityId}.`, quiz: { id: activityId }, slots };
165
+ return { ...result, changed: `Added ${added} question${added === 1 ? "" : "s"} to quiz ${activityId}.` };
166
+ }
167
+ finally {
168
+ await page.close().catch(() => undefined);
169
+ }
170
+ }
171
+ /**
172
+ * Changes one slot of a quiz (by its place, from 1): its maximum mark, through the slots' service,
173
+ * and where it is, as dragging it on the edit page does: after another slot (`moveAfter`, 0 for
174
+ * first) and on a page. Moodle lets the mark change after attempts (it regrades them), not the rest.
175
+ */
176
+ export async function editQuizSlot(context, siteUrl, activityId, slot, { maxMark, moveAfter, page: toPage }) {
177
+ if (maxMark === undefined && moveAfter === undefined && toPage === undefined)
178
+ throw new QuizError("Give what to change in the slot: maxMark, moveAfter or page.");
179
+ if (maxMark !== undefined && !(Number.isFinite(maxMark) && maxMark >= 0))
180
+ throw new QuizError("The maximum mark is a number of points, 0 or more.");
181
+ const page = await openEditPage(context, siteUrl, activityId);
182
+ try {
183
+ const quizId = await quizInstanceId(page, activityId);
184
+ const { slots } = await readQuizEditor(page);
185
+ const moving = slotAt(slots, slot, activityId);
186
+ const done = [];
187
+ if (moveAfter !== undefined || toPage !== undefined) {
188
+ await checkEditable(page, activityId);
189
+ // Staying in place unless moved: after the slot before it.
190
+ const after = moveAfter ?? slot - 1;
191
+ const previous = after > 0 ? slotAt(slots, after, activityId) : undefined;
192
+ if (previous?.id === moving.id)
193
+ throw new QuizError(`Slot ${slot} can't move after itself.`);
194
+ // Moodle takes a page from the previous slot's to the next one's; by default the previous slot's.
195
+ const target = toPage ?? previous?.page ?? 1;
196
+ await editRest(page, quizId, activityId, { class: "resource", field: "move", id: moving.id, previousid: previous?.id ?? 0, page: target });
197
+ done.push(moveAfter !== undefined ? `moved slot ${slot} ${after === 0 ? "first" : `after slot ${after}`}${toPage !== undefined ? `, on page ${toPage}` : ""}` : `moved slot ${slot} to page ${toPage}`);
198
+ }
199
+ if (maxMark !== undefined) {
200
+ await callService(context, siteUrl, "mod_quiz_update_slots", { quizid: quizId, slots: [{ id: moving.id, maxmark: maxMark }] });
201
+ done.push(`set the maximum mark of question "${moving.name}" to ${maxMark}`);
202
+ }
203
+ return await afterChange(page, activityId, `In quiz ${activityId}: ${done.join("; ")}.`);
204
+ }
205
+ finally {
206
+ await page.close().catch(() => undefined);
207
+ }
208
+ }
209
+ /** Removes a slot (by its place, from 1) from a quiz, as the edit page's delete icon does; the question stays in its bank. */
210
+ export async function removeQuizSlot(context, siteUrl, activityId, slot) {
211
+ const page = await openEditPage(context, siteUrl, activityId);
212
+ try {
213
+ await checkEditable(page, activityId);
214
+ const quizId = await quizInstanceId(page, activityId);
215
+ const removed = slotAt(await readQuizSlots(page), slot, activityId);
216
+ // The edit page simulates a DELETE request with this parameter.
217
+ await editRest(page, quizId, activityId, { class: "resource", action: "DELETE", id: removed.id });
218
+ return await afterChange(page, activityId, `Removed slot ${slot} ("${removed.name}") from quiz ${activityId}.`);
219
+ }
220
+ finally {
221
+ await page.close().catch(() => undefined);
222
+ }
223
+ }
224
+ /**
225
+ * Sets a quiz's pages: `perPage` questions a page (0 for all on one), as the edit page's
226
+ * "Repaginate" does, or page breaks exactly after the given slots (and nowhere else), as its page
227
+ * break icons do one by one.
228
+ */
229
+ export async function setQuizPages(context, siteUrl, activityId, pages) {
230
+ const page = await openEditPage(context, siteUrl, activityId);
231
+ try {
232
+ await checkEditable(page, activityId);
233
+ if ("perPage" in pages) {
234
+ if (!(Number.isInteger(pages.perPage) && pages.perPage >= 0))
235
+ throw new QuizError("perPage is a whole number of questions, 0 for all on one page.");
236
+ const { status, error } = await postEditPage(page, activityId, { repaginate: "1", questionsperpage: String(pages.perPage) });
237
+ if (error || status >= 400)
238
+ throw new QuizError(`Moodle didn't repaginate quiz ${activityId}: ${error ?? `HTTP ${status}`}.`);
239
+ return await afterChange(page, activityId, `Repaginated quiz ${activityId}: ${pages.perPage ? `${pages.perPage} question${pages.perPage === 1 ? "" : "s"} a page` : "all questions on one page"}.`);
240
+ }
241
+ const quizId = await quizInstanceId(page, activityId);
242
+ const slots = await readQuizSlots(page);
243
+ for (const after of pages.breaksAfter) {
244
+ if (after < 1 || after >= slots.length)
245
+ throw new QuizError(`A page break goes after a slot from 1 to ${slots.length - 1} in quiz ${activityId}; ${after} isn't one.`);
246
+ }
247
+ let toggled = 0;
248
+ for (let i = 1; i < slots.length; i++) {
249
+ const wanted = pages.breaksAfter.includes(i);
250
+ const has = slots[i].page !== slots[i - 1].page;
251
+ if (wanted === has)
252
+ continue;
253
+ // The request names the slot after the break: 2 splits the page there, 1 joins it to the previous one.
254
+ await editRest(page, quizId, activityId, { class: "resource", field: "updatepagebreak", id: slots[i].id, value: wanted ? 2 : 1 });
255
+ toggled++;
256
+ }
257
+ const where = pages.breaksAfter.length > 0 ? `after slot${pages.breaksAfter.length === 1 ? "" : "s"} ${[...pages.breaksAfter].sort((a, b) => a - b).join(", ")}` : "nowhere (one page)";
258
+ return await afterChange(page, activityId, toggled > 0 ? `Quiz ${activityId}'s page breaks are now ${where}.` : `Quiz ${activityId} already had its page breaks ${where}.`);
259
+ }
260
+ finally {
261
+ await page.close().catch(() => undefined);
262
+ }
263
+ }
264
+ /**
265
+ * Adds a section to a quiz starting at a slot, with its heading and whether its questions are
266
+ * shuffled, as the edit page's "Add a new section heading" and its heading and shuffle controls do.
267
+ * A section starts at a page: the slot must be the first of its page (see `setQuizPages`).
268
+ */
269
+ export async function addQuizSection(context, siteUrl, activityId, { firstSlot, heading, shuffle = false }) {
270
+ const page = await openEditPage(context, siteUrl, activityId);
271
+ try {
272
+ await checkEditable(page, activityId);
273
+ const quizId = await quizInstanceId(page, activityId);
274
+ const before = await readQuizEditor(page);
275
+ const first = slotAt(before.slots, firstSlot, activityId);
276
+ if (before.slots.some((s) => s.slot < firstSlot && s.page === first.page)) {
277
+ throw new QuizError(`Slot ${firstSlot} isn't the first of its page in quiz ${activityId}, and a section starts at a page: put a page break before it first.`);
278
+ }
279
+ let sectionId = before.sections.find((s) => s.firstSlot === firstSlot)?.id;
280
+ if (sectionId !== undefined)
281
+ throw new QuizError(`Quiz ${activityId} already has a section starting at slot ${firstSlot} (section ${sectionId}).`);
282
+ const { status, error } = await postEditPage(page, activityId, { addsectionatpage: String(first.page) });
283
+ if (error || status >= 400)
284
+ throw new QuizError(`Moodle didn't add a section to quiz ${activityId}: ${error ?? `HTTP ${status}`}.`);
285
+ await page.reload({ waitUntil: "domcontentloaded" });
286
+ sectionId = (await readQuizEditor(page)).sections.find((s) => !before.sections.some((old) => old.id === s.id))?.id;
287
+ if (sectionId === undefined)
288
+ throw new QuizError(`Moodle added no section to quiz ${activityId}.`);
289
+ await editRest(page, quizId, activityId, { class: "section", field: "updatesectiontitle", id: sectionId, newheading: heading });
290
+ if (shuffle)
291
+ await editRest(page, quizId, activityId, { class: "section", field: "updateshufflequestions", id: sectionId, newshuffle: 1 });
292
+ const result = await afterChange(page, activityId, `Added section "${heading}" to quiz ${activityId} at slot ${firstSlot}${shuffle ? ", its questions shuffled" : ""}.`);
293
+ return { ...result, sectionId };
95
294
  }
96
295
  finally {
97
296
  await page.close().catch(() => undefined);