@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.
Files changed (297) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +133 -2
  3. package/dist/areas/activities/build.d.ts +98 -0
  4. package/dist/areas/activities/build.js +246 -0
  5. package/dist/areas/activities/build.js.map +1 -0
  6. package/dist/areas/activities/content.d.ts +53 -0
  7. package/dist/areas/activities/content.js +113 -0
  8. package/dist/areas/activities/content.js.map +1 -0
  9. package/dist/areas/assignments/grader.d.ts +68 -0
  10. package/dist/areas/assignments/grader.js +169 -0
  11. package/dist/areas/assignments/grader.js.map +1 -0
  12. package/dist/areas/assignments/grading.d.ts +86 -0
  13. package/dist/areas/assignments/grading.js +162 -0
  14. package/dist/areas/assignments/grading.js.map +1 -0
  15. package/dist/areas/assignments/student.d.ts +57 -0
  16. package/dist/areas/assignments/student.js +115 -0
  17. package/dist/areas/assignments/student.js.map +1 -0
  18. package/dist/areas/calendar/calendar.d.ts +43 -0
  19. package/dist/areas/calendar/calendar.js +84 -0
  20. package/dist/areas/calendar/calendar.js.map +1 -0
  21. package/dist/areas/completion/completion.d.ts +50 -0
  22. package/dist/areas/completion/completion.js +112 -0
  23. package/dist/areas/completion/completion.js.map +1 -0
  24. package/dist/areas/courses/list.d.ts +59 -0
  25. package/dist/areas/courses/list.js +74 -0
  26. package/dist/areas/courses/list.js.map +1 -0
  27. package/dist/areas/courses/overview.d.ts +83 -0
  28. package/dist/areas/courses/overview.js +137 -0
  29. package/dist/areas/courses/overview.js.map +1 -0
  30. package/dist/areas/courses/state.d.ts +51 -0
  31. package/dist/areas/courses/state.js +45 -0
  32. package/dist/areas/courses/state.js.map +1 -0
  33. package/dist/areas/courses/structure.d.ts +89 -0
  34. package/dist/areas/courses/structure.js +104 -0
  35. package/dist/areas/courses/structure.js.map +1 -0
  36. package/dist/areas/forums/discussions.d.ts +57 -0
  37. package/dist/areas/forums/discussions.js +119 -0
  38. package/dist/areas/forums/discussions.js.map +1 -0
  39. package/dist/areas/forums/posts.d.ts +83 -0
  40. package/dist/areas/forums/posts.js +114 -0
  41. package/dist/areas/forums/posts.js.map +1 -0
  42. package/dist/areas/grades/gradebook.d.ts +104 -0
  43. package/dist/areas/grades/gradebook.js +250 -0
  44. package/dist/areas/grades/gradebook.js.map +1 -0
  45. package/dist/areas/grades/user-report.d.ts +26 -0
  46. package/dist/areas/grades/user-report.js +71 -0
  47. package/dist/areas/grades/user-report.js.map +1 -0
  48. package/dist/areas/h5p/h5p.d.ts +33 -0
  49. package/dist/areas/h5p/h5p.js +104 -0
  50. package/dist/areas/h5p/h5p.js.map +1 -0
  51. package/dist/areas/lessons/lessons.d.ts +60 -0
  52. package/dist/areas/lessons/lessons.js +145 -0
  53. package/dist/areas/lessons/lessons.js.map +1 -0
  54. package/dist/areas/messages/messages.d.ts +68 -0
  55. package/dist/areas/messages/messages.js +84 -0
  56. package/dist/areas/messages/messages.js.map +1 -0
  57. package/dist/areas/participants/participants.d.ts +57 -0
  58. package/dist/areas/participants/participants.js +135 -0
  59. package/dist/areas/participants/participants.js.map +1 -0
  60. package/dist/areas/questions/banks.d.ts +83 -0
  61. package/dist/areas/questions/banks.js +239 -0
  62. package/dist/areas/questions/banks.js.map +1 -0
  63. package/dist/areas/quizzes/attempts.d.ts +123 -0
  64. package/dist/areas/quizzes/attempts.js +300 -0
  65. package/dist/areas/quizzes/attempts.js.map +1 -0
  66. package/dist/areas/quizzes/results.d.ts +36 -0
  67. package/dist/areas/quizzes/results.js +93 -0
  68. package/dist/areas/quizzes/results.js.map +1 -0
  69. package/dist/areas/quizzes/structure.d.ts +42 -0
  70. package/dist/areas/quizzes/structure.js +100 -0
  71. package/dist/areas/quizzes/structure.js.map +1 -0
  72. package/dist/areas/site/info.d.ts +25 -0
  73. package/dist/areas/site/info.js +45 -0
  74. package/dist/areas/site/info.js.map +1 -0
  75. package/dist/areas/site/users.d.ts +56 -0
  76. package/dist/areas/site/users.js +89 -0
  77. package/dist/areas/site/users.js.map +1 -0
  78. package/dist/areas/surveys/choice.d.ts +38 -0
  79. package/dist/areas/surveys/choice.js +105 -0
  80. package/dist/areas/surveys/choice.js.map +1 -0
  81. package/dist/areas/surveys/surveys.d.ts +51 -0
  82. package/dist/areas/surveys/surveys.js +201 -0
  83. package/dist/areas/surveys/surveys.js.map +1 -0
  84. package/dist/browser/chromium.d.ts +15 -0
  85. package/dist/browser/chromium.js +53 -0
  86. package/dist/browser/chromium.js.map +1 -0
  87. package/dist/browser/cookies.d.ts +11 -0
  88. package/dist/browser/cookies.js +31 -0
  89. package/dist/browser/cookies.js.map +1 -0
  90. package/dist/browser/manager.d.ts +57 -0
  91. package/dist/browser/manager.js +177 -0
  92. package/dist/browser/manager.js.map +1 -0
  93. package/dist/browser.d.ts +53 -0
  94. package/dist/browser.js +167 -0
  95. package/dist/browser.js.map +1 -0
  96. package/dist/categories.d.ts +28 -0
  97. package/dist/categories.js +222 -0
  98. package/dist/categories.js.map +1 -0
  99. package/dist/chromiumInstaller.d.ts +15 -0
  100. package/dist/chromiumInstaller.js +52 -0
  101. package/dist/chromiumInstaller.js.map +1 -0
  102. package/dist/cli/args.d.ts +33 -0
  103. package/dist/cli/args.js +194 -0
  104. package/dist/cli/args.js.map +1 -0
  105. package/dist/cli/commands.generated.d.ts +4 -0
  106. package/dist/cli/commands.generated.js +2203 -0
  107. package/dist/cli/commands.generated.js.map +1 -0
  108. package/dist/cli/config.d.ts +46 -0
  109. package/dist/cli/config.js +107 -0
  110. package/dist/cli/config.js.map +1 -0
  111. package/dist/cli/main.d.ts +2 -0
  112. package/dist/cli/main.js +19 -0
  113. package/dist/cli/main.js.map +1 -0
  114. package/dist/cli/output.d.ts +4 -0
  115. package/dist/cli/output.js +71 -0
  116. package/dist/cli/output.js.map +1 -0
  117. package/dist/cli/run.d.ts +15 -0
  118. package/dist/cli/run.js +194 -0
  119. package/dist/cli/run.js.map +1 -0
  120. package/dist/cli/spec.d.ts +42 -0
  121. package/dist/cli/spec.js +4 -0
  122. package/dist/cli/spec.js.map +1 -0
  123. package/dist/client.d.ts +217 -0
  124. package/dist/client.js +179 -0
  125. package/dist/client.js.map +1 -0
  126. package/dist/core/ajax.d.ts +25 -0
  127. package/dist/core/ajax.js +85 -0
  128. package/dist/core/ajax.js.map +1 -0
  129. package/dist/core/editor.d.ts +18 -0
  130. package/dist/core/editor.js +51 -0
  131. package/dist/core/editor.js.map +1 -0
  132. package/dist/core/errors.d.ts +12 -0
  133. package/dist/core/errors.js +10 -0
  134. package/dist/core/errors.js.map +1 -0
  135. package/dist/core/files.d.ts +16 -0
  136. package/dist/core/files.js +58 -0
  137. package/dist/core/files.js.map +1 -0
  138. package/dist/core/forms.d.ts +45 -0
  139. package/dist/core/forms.js +200 -0
  140. package/dist/core/forms.js.map +1 -0
  141. package/dist/core/http.d.ts +18 -0
  142. package/dist/core/http.js +22 -0
  143. package/dist/core/http.js.map +1 -0
  144. package/dist/core/inPage.d.ts +8 -0
  145. package/dist/core/inPage.js +10 -0
  146. package/dist/core/inPage.js.map +1 -0
  147. package/dist/core/login.d.ts +40 -0
  148. package/dist/core/login.js +83 -0
  149. package/dist/core/login.js.map +1 -0
  150. package/dist/core/pace.d.ts +4 -0
  151. package/dist/core/pace.js +21 -0
  152. package/dist/core/pace.js.map +1 -0
  153. package/dist/core/session.d.ts +55 -0
  154. package/dist/core/session.js +157 -0
  155. package/dist/core/session.js.map +1 -0
  156. package/dist/core/site.d.ts +8 -0
  157. package/dist/core/site.js +36 -0
  158. package/dist/core/site.js.map +1 -0
  159. package/dist/core/text.d.ts +21 -0
  160. package/dist/core/text.js +113 -0
  161. package/dist/core/text.js.map +1 -0
  162. package/dist/home.d.ts +19 -0
  163. package/dist/home.js +54 -0
  164. package/dist/home.js.map +1 -0
  165. package/dist/index.d.ts +40 -0
  166. package/dist/index.js +51 -0
  167. package/dist/index.js.map +1 -0
  168. package/dist/internals.d.ts +11 -0
  169. package/dist/internals.js +15 -0
  170. package/dist/internals.js.map +1 -0
  171. package/dist/moodle/activities.d.ts +94 -0
  172. package/dist/moodle/activities.js +240 -0
  173. package/dist/moodle/activities.js.map +1 -0
  174. package/dist/moodle/ajax.d.ts +22 -0
  175. package/dist/moodle/ajax.js +91 -0
  176. package/dist/moodle/ajax.js.map +1 -0
  177. package/dist/moodle/assign.d.ts +65 -0
  178. package/dist/moodle/assign.js +162 -0
  179. package/dist/moodle/assign.js.map +1 -0
  180. package/dist/moodle/calendar.d.ts +41 -0
  181. package/dist/moodle/calendar.js +80 -0
  182. package/dist/moodle/calendar.js.map +1 -0
  183. package/dist/moodle/choice.d.ts +33 -0
  184. package/dist/moodle/choice.js +100 -0
  185. package/dist/moodle/choice.js.map +1 -0
  186. package/dist/moodle/completion.d.ts +48 -0
  187. package/dist/moodle/completion.js +108 -0
  188. package/dist/moodle/completion.js.map +1 -0
  189. package/dist/moodle/content.d.ts +43 -0
  190. package/dist/moodle/content.js +105 -0
  191. package/dist/moodle/content.js.map +1 -0
  192. package/dist/moodle/course.d.ts +51 -0
  193. package/dist/moodle/course.js +45 -0
  194. package/dist/moodle/course.js.map +1 -0
  195. package/dist/moodle/courses.d.ts +57 -0
  196. package/dist/moodle/courses.js +70 -0
  197. package/dist/moodle/courses.js.map +1 -0
  198. package/dist/moodle/editor.d.ts +18 -0
  199. package/dist/moodle/editor.js +50 -0
  200. package/dist/moodle/editor.js.map +1 -0
  201. package/dist/moodle/files.d.ts +16 -0
  202. package/dist/moodle/files.js +57 -0
  203. package/dist/moodle/files.js.map +1 -0
  204. package/dist/moodle/forms.d.ts +44 -0
  205. package/dist/moodle/forms.js +199 -0
  206. package/dist/moodle/forms.js.map +1 -0
  207. package/dist/moodle/forum.d.ts +55 -0
  208. package/dist/moodle/forum.js +115 -0
  209. package/dist/moodle/forum.js.map +1 -0
  210. package/dist/moodle/forumPosts.d.ts +83 -0
  211. package/dist/moodle/forumPosts.js +113 -0
  212. package/dist/moodle/forumPosts.js.map +1 -0
  213. package/dist/moodle/gradebook.d.ts +102 -0
  214. package/dist/moodle/gradebook.js +246 -0
  215. package/dist/moodle/gradebook.js.map +1 -0
  216. package/dist/moodle/grades.d.ts +24 -0
  217. package/dist/moodle/grades.js +67 -0
  218. package/dist/moodle/grades.js.map +1 -0
  219. package/dist/moodle/grading.d.ts +92 -0
  220. package/dist/moodle/grading.js +161 -0
  221. package/dist/moodle/grading.js.map +1 -0
  222. package/dist/moodle/h5p.d.ts +31 -0
  223. package/dist/moodle/h5p.js +100 -0
  224. package/dist/moodle/h5p.js.map +1 -0
  225. package/dist/moodle/inPage.d.ts +8 -0
  226. package/dist/moodle/inPage.js +10 -0
  227. package/dist/moodle/inPage.js.map +1 -0
  228. package/dist/moodle/lesson.d.ts +57 -0
  229. package/dist/moodle/lesson.js +140 -0
  230. package/dist/moodle/lesson.js.map +1 -0
  231. package/dist/moodle/login.d.ts +37 -0
  232. package/dist/moodle/login.js +77 -0
  233. package/dist/moodle/login.js.map +1 -0
  234. package/dist/moodle/messages.d.ts +66 -0
  235. package/dist/moodle/messages.js +80 -0
  236. package/dist/moodle/messages.js.map +1 -0
  237. package/dist/moodle/overview.d.ts +83 -0
  238. package/dist/moodle/overview.js +137 -0
  239. package/dist/moodle/overview.js.map +1 -0
  240. package/dist/moodle/pace.d.ts +4 -0
  241. package/dist/moodle/pace.js +19 -0
  242. package/dist/moodle/pace.js.map +1 -0
  243. package/dist/moodle/participants.d.ts +55 -0
  244. package/dist/moodle/participants.js +131 -0
  245. package/dist/moodle/participants.js.map +1 -0
  246. package/dist/moodle/questionBank.d.ts +81 -0
  247. package/dist/moodle/questionBank.js +235 -0
  248. package/dist/moodle/questionBank.js.map +1 -0
  249. package/dist/moodle/quiz.d.ts +117 -0
  250. package/dist/moodle/quiz.js +291 -0
  251. package/dist/moodle/quiz.js.map +1 -0
  252. package/dist/moodle/quizResults.d.ts +36 -0
  253. package/dist/moodle/quizResults.js +93 -0
  254. package/dist/moodle/quizResults.js.map +1 -0
  255. package/dist/moodle/quizStructure.d.ts +42 -0
  256. package/dist/moodle/quizStructure.js +100 -0
  257. package/dist/moodle/quizStructure.js.map +1 -0
  258. package/dist/moodle/session.d.ts +39 -0
  259. package/dist/moodle/session.js +99 -0
  260. package/dist/moodle/session.js.map +1 -0
  261. package/dist/moodle/site.d.ts +8 -0
  262. package/dist/moodle/site.js +36 -0
  263. package/dist/moodle/site.js.map +1 -0
  264. package/dist/moodle/siteInfo.d.ts +25 -0
  265. package/dist/moodle/siteInfo.js +45 -0
  266. package/dist/moodle/siteInfo.js.map +1 -0
  267. package/dist/moodle/structure.d.ts +94 -0
  268. package/dist/moodle/structure.js +103 -0
  269. package/dist/moodle/structure.js.map +1 -0
  270. package/dist/moodle/submission.d.ts +48 -0
  271. package/dist/moodle/submission.js +109 -0
  272. package/dist/moodle/submission.js.map +1 -0
  273. package/dist/moodle/survey.d.ts +47 -0
  274. package/dist/moodle/survey.js +197 -0
  275. package/dist/moodle/survey.js.map +1 -0
  276. package/dist/moodle/text.d.ts +21 -0
  277. package/dist/moodle/text.js +113 -0
  278. package/dist/moodle/text.js.map +1 -0
  279. package/dist/moodle/user.d.ts +54 -0
  280. package/dist/moodle/user.js +78 -0
  281. package/dist/moodle/user.js.map +1 -0
  282. package/dist/options.d.ts +30 -0
  283. package/dist/options.js +35 -0
  284. package/dist/options.js.map +1 -0
  285. package/dist/sessionCookies.d.ts +11 -0
  286. package/dist/sessionCookies.js +31 -0
  287. package/dist/sessionCookies.js.map +1 -0
  288. package/dist/testing/fakeMoodle.d.ts +55 -0
  289. package/dist/testing/fakeMoodle.js +103 -0
  290. package/dist/testing/fakeMoodle.js.map +1 -0
  291. package/dist/testing/gradingPanel.d.ts +6 -0
  292. package/dist/testing/gradingPanel.js +22 -0
  293. package/dist/testing/gradingPanel.js.map +1 -0
  294. package/dist/testing/index.d.ts +5 -0
  295. package/dist/testing/index.js +18 -0
  296. package/dist/testing/index.js.map +1 -0
  297. package/package.json +77 -4
@@ -0,0 +1,239 @@
1
+ // Question banks (Moodle 5: shared banks are qbank activities; quizzes keep private ones), their
2
+ // questions read from the bank page by field names and icons, questions imported from a GIFT or
3
+ // Moodle XML file through the import form, and single questions made through their edit form.
4
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import path from "node:path";
7
+ import { checkLocalFiles } from "../../core/files.js";
8
+ import { formValue, openForm, setFields, submitForm, uploadToDraft } from "../../core/forms.js";
9
+ import { NotLoggedInError, openSitePage } from "../../core/session.js";
10
+ import { MoodleError } from "../../core/errors.js";
11
+ /** The bank isn't there, or Moodle refused the questions. */
12
+ export class QuestionBankError extends MoodleError {
13
+ name = "QuestionBankError";
14
+ constructor(message) {
15
+ super(message, "question-bank");
16
+ }
17
+ }
18
+ /** A course's question banks: its shared ones and the activities' own, as the banks page lists them. */
19
+ export async function questionBanks(context, siteUrl, courseId) {
20
+ const page = await openSitePage(context, siteUrl, `question/banks.php?courseid=${courseId}`);
21
+ try {
22
+ if ((await page.locator("body#page-question-banks").count()) === 0) {
23
+ throw new QuestionBankError(`You can't see course ${courseId}'s question banks: the course doesn't exist, or you aren't allowed.`);
24
+ }
25
+ return await page.evaluate(() => {
26
+ const doc = globalThis.document;
27
+ return Array.from(doc.querySelectorAll(".question-banks-list li")).flatMap((item) => {
28
+ const link = item.querySelector('a[href*="/mod/"][href*="view.php"]');
29
+ const match = /\/mod\/([a-z0-9_]+)\/view\.php\?(?:.*&)?id=(\d+)/.exec(link?.getAttribute("href") ?? "");
30
+ if (!match)
31
+ return [];
32
+ return [{ id: Number(match[2]), name: (link?.textContent ?? "").replace(/\s+/g, " ").trim(), type: match[1], shared: item.closest(".shared-banks") !== null }];
33
+ });
34
+ });
35
+ }
36
+ finally {
37
+ await page.close().catch(() => undefined);
38
+ }
39
+ }
40
+ /** The bank page's categories (its category filter's options) and its context id. */
41
+ async function bankCategories(page) {
42
+ return page.evaluate(() => {
43
+ const doc = globalThis.document;
44
+ const categories = Array.from(doc.querySelectorAll('[data-filterregion="filtertypedata"] select[data-field-name="category"] option'))
45
+ .filter((option) => option.value !== "")
46
+ // Names come indented, with their question count: " Default for X (6)".
47
+ .map((option) => ({ id: Number(option.value), name: (option.textContent ?? "").replace(/\s+/g, " ").replace(/\s*\(\d+\)$/, "").trim() }));
48
+ return { contextId: Number(doc.querySelector(".questionbankwindow[data-contextid]")?.getAttribute("data-contextid")), categories };
49
+ });
50
+ }
51
+ /** The questions on a bank page: id from their checkbox, type from their icon, name from the in-place editor. */
52
+ export function readBankQuestions(page, categoryId) {
53
+ return page.evaluate((categoryId) => {
54
+ const doc = globalThis.document;
55
+ return Array.from(doc.querySelectorAll("#categoryquestions tbody tr")).flatMap((row) => {
56
+ const id = Number(/^q(\d+)$/.exec(row.querySelector('input[type="checkbox"][name^="q"]')?.getAttribute("name") ?? "")?.[1]);
57
+ if (!id)
58
+ return [];
59
+ const type = /\/qtype_([a-z0-9_]+)\//.exec(row.querySelector('[data-columnid*="question_type_column"] img')?.getAttribute("src") ?? "")?.[1] ?? "";
60
+ const name = row.querySelector('[data-itemtype="questionname"]')?.getAttribute("data-value") ?? "";
61
+ const status = row.querySelector('select[name="question_status_dropdown"]')?.value;
62
+ const version = (row.querySelector('[data-columnid*="version_number_column"]')?.textContent ?? "").trim();
63
+ return [{ id, name, type, ...(status ? { status } : {}), ...(version ? { version } : {}), categoryId }];
64
+ });
65
+ }, categoryId);
66
+ }
67
+ // Modules that keep questions (FEATURE_USES_QUESTIONS in Moodle 5): a shared bank, a quiz's own.
68
+ const BANK_MODULES = new Set(["qbank", "quiz"]);
69
+ const bankModules = new Map();
70
+ /**
71
+ * Checks that a course module keeps questions before opening its bank: Moodle creates a bank's
72
+ * categories for any activity whose bank page is opened (confirmed empirically, 5.2). The module
73
+ * comes from its settings form, which changes nothing.
74
+ */
75
+ async function checkBank(context, siteUrl, activityId) {
76
+ const key = `${siteUrl}|${activityId}`;
77
+ let module = bankModules.get(key);
78
+ if (!module) {
79
+ const page = await openSitePage(context, siteUrl, `course/modedit.php?update=${activityId}`);
80
+ try {
81
+ module = (await formValue(page, "modulename")) ?? "";
82
+ }
83
+ finally {
84
+ await page.close().catch(() => undefined);
85
+ }
86
+ if (module)
87
+ bankModules.set(key, module);
88
+ }
89
+ if (!BANK_MODULES.has(module)) {
90
+ throw new QuestionBankError(`Activity ${activityId} ${module ? `is a ${module}, which` : "doesn't exist, or you can't edit it, so it"} has no question bank. Take a bank's id from the course's question banks.`);
91
+ }
92
+ }
93
+ // A bank page shows this many questions at most; more than that in one category isn't read.
94
+ const PER_PAGE = 1000;
95
+ /** A bank's categories and every question in them, category by category. */
96
+ export async function questions(context, siteUrl, activityId) {
97
+ await checkBank(context, siteUrl, activityId);
98
+ const page = await openSitePage(context, siteUrl, `question/edit.php?cmid=${activityId}`);
99
+ try {
100
+ if ((await page.locator("body#page-question-edit .questionbankwindow").count()) === 0) {
101
+ throw new QuestionBankError(`Activity ${activityId} has no question bank you can see. Take a bank's id from the course's question banks.`);
102
+ }
103
+ const { contextId, categories } = await bankCategories(page);
104
+ const found = [];
105
+ for (const category of categories) {
106
+ await page.goto(new URL(`question/edit.php?cmid=${activityId}&cat=${category.id},${contextId}&qperpage=${PER_PAGE}`, siteUrl).href, { waitUntil: "domcontentloaded" });
107
+ found.push(...(await readBankQuestions(page, category.id)));
108
+ }
109
+ return { categories, questions: found };
110
+ }
111
+ finally {
112
+ await page.close().catch(() => undefined);
113
+ }
114
+ }
115
+ /** The form's category option ("<id>,<context>") for a category id; the form's default without one. */
116
+ async function categoryOption(page, categoryId) {
117
+ if (categoryId === undefined)
118
+ return undefined;
119
+ const values = await page.evaluate(() => Array.from(globalThis.document.querySelectorAll('form.mform select[name="category"] option')).map((o) => o.value));
120
+ const value = values.find((v) => v.split(",")[0] === String(categoryId));
121
+ if (!value)
122
+ throw new QuestionBankError(`The bank has no category ${categoryId}: take one from the bank's categories.`);
123
+ return value;
124
+ }
125
+ /** Moodle's messages on the page after a form or an import (problems first). */
126
+ async function pageProblems(page) {
127
+ return page.evaluate(() => {
128
+ const doc = globalThis.document;
129
+ return Array.from(doc.querySelectorAll("#region-main .alert-danger, #region-main .notifyproblem, #region-main .errorbox, #region-main .alert-warning"))
130
+ .map((element) => {
131
+ // Without the close button, the error box's heading and its "more information" link.
132
+ const copy = element.cloneNode(true);
133
+ for (const extra of Array.from(copy.querySelectorAll("button, .visually-hidden, .errorcode, h2, h3, h4")))
134
+ extra.remove();
135
+ return (copy.textContent ?? "").replace(/\s+/g, " ").trim();
136
+ })
137
+ .filter(Boolean);
138
+ });
139
+ }
140
+ /**
141
+ * Imports questions into a bank from a GIFT or Moodle XML file (a local path) or text, through the
142
+ * bank's import form; returns the questions created. A file Moodle can't parse comes back as a
143
+ * `QuestionBankError` with its messages, and nothing is imported.
144
+ */
145
+ export async function importQuestions(context, siteUrl, activityId, { format, file, text, categoryId }) {
146
+ if ((file === undefined) === (text === undefined))
147
+ throw new QuestionBankError("Give the questions as a file or as text, one of the two.");
148
+ if (file)
149
+ await checkLocalFiles([file]);
150
+ const before = new Set((await questions(context, siteUrl, activityId)).questions.map((q) => q.id));
151
+ // Text goes up as a file, as a person would upload one.
152
+ const dir = text !== undefined ? await mkdtemp(path.join(tmpdir(), "moodle-import-")) : undefined;
153
+ const upload = dir ? path.join(dir, format === "gift" ? "questions.txt" : "questions.xml") : file;
154
+ if (dir)
155
+ await writeFile(upload, text, "utf8");
156
+ const page = await openForm(context, siteUrl, `question/bank/importquestions/import.php?cmid=${activityId}`).catch((error) => {
157
+ if (error instanceof NotLoggedInError)
158
+ throw error;
159
+ throw new QuestionBankError(`Activity ${activityId} has no question bank you can import into.`);
160
+ });
161
+ try {
162
+ await uploadToDraft(page, (await formValue(page, "newfile")), [upload]);
163
+ const values = { format };
164
+ const category = await categoryOption(page, categoryId);
165
+ // The file's own categories would override the one chosen.
166
+ if (category)
167
+ Object.assign(values, { category, catfromfile: false, contextfromfile: false });
168
+ await setFields(page, values);
169
+ await submitForm(page, "submitbutton");
170
+ const problems = await pageProblems(page);
171
+ if (problems.length > 0)
172
+ throw new QuestionBankError(`Moodle didn't import the questions: ${problems.join(" ")}`);
173
+ }
174
+ finally {
175
+ await page.close().catch(() => undefined);
176
+ if (dir)
177
+ await rm(dir, { recursive: true, force: true });
178
+ }
179
+ const created = (await questions(context, siteUrl, activityId)).questions.filter((q) => !before.has(q.id));
180
+ return { changed: `Imported ${created.length} question${created.length === 1 ? "" : "s"} into bank ${activityId}.`, questions: created };
181
+ }
182
+ export const QUESTION_TYPES = ["multichoice", "truefalse", "shortanswer", "numerical", "essay"];
183
+ /** The option of a fraction select nearest to `fraction` (Moodle offers 100%, 90%, 83.33%…). */
184
+ async function nearestFraction(page, field, fraction) {
185
+ const options = await page.evaluate((field) => Array.from(globalThis.document.querySelectorAll(`select[name="${field}"] option`)).map((o) => o.value), field);
186
+ return options.reduce((best, value) => (Math.abs(Number(value) - fraction) < Math.abs(Number(best) - fraction) ? value : best), options[0] ?? "0.0");
187
+ }
188
+ /**
189
+ * Creates one question in a bank through its type's edit form; returns its id. Imports are the way
190
+ * for several; this is for one-offs.
191
+ */
192
+ export async function createQuestion(context, siteUrl, activityId, input) {
193
+ if (!QUESTION_TYPES.includes(input.type))
194
+ throw new QuestionBankError(`Question type ${input.type} isn't one this creates (${QUESTION_TYPES.join(", ")}); import it as GIFT or Moodle XML instead.`);
195
+ const answers = input.answers ?? [];
196
+ if (["multichoice", "shortanswer", "numerical"].includes(input.type) && answers.length === 0)
197
+ throw new QuestionBankError(`A ${input.type} question needs answers.`);
198
+ if (input.type === "truefalse" && input.correct === undefined)
199
+ throw new QuestionBankError("A true/false question needs its right answer (correct).");
200
+ const { categories } = await questions(context, siteUrl, activityId);
201
+ // The bank lists its "Top" category first, which holds no questions: its default one comes next.
202
+ const category = input.categoryId ?? (categories[1] ?? categories[0])?.id;
203
+ const page = await openForm(context, siteUrl, `question/bank/editquestion/question.php?cmid=${activityId}&category=${category}&qtype=${input.type}`).catch((error) => {
204
+ if (error instanceof NotLoggedInError)
205
+ throw error;
206
+ throw new QuestionBankError(`Activity ${activityId} has no question bank you can add to, or category ${category} isn't in it.`);
207
+ });
208
+ try {
209
+ // The form comes with a few blank answers; "Blanks for more" adds three at a time.
210
+ for (let i = 0; i < 10 && answers.length > Number((await formValue(page, "noanswers")) ?? answers.length); i++) {
211
+ await Promise.all([page.waitForNavigation({ waitUntil: "domcontentloaded" }), page.click('form.mform [name="addanswers"]')]);
212
+ }
213
+ const values = { name: input.name, "questiontext[text]": input.text, ...(input.defaultMark !== undefined ? { defaultmark: input.defaultMark } : {}) };
214
+ const choice = await categoryOption(page, input.categoryId);
215
+ if (choice)
216
+ values.category = choice;
217
+ if (input.type === "truefalse")
218
+ values.correctanswer = input.correct ? 1 : 0;
219
+ if (input.type === "multichoice")
220
+ values.single = answers.filter((a) => a.fraction > 0).length > 1 ? 0 : 1;
221
+ for (const [i, answer] of answers.entries()) {
222
+ values[input.type === "multichoice" ? `answer[${i}][text]` : `answer[${i}]`] = answer.text;
223
+ values[`fraction[${i}]`] = await nearestFraction(page, `fraction[${i}]`, answer.fraction);
224
+ if (answer.feedback !== undefined)
225
+ values[`feedback[${i}][text]`] = answer.feedback;
226
+ if (answer.tolerance !== undefined)
227
+ values[`tolerance[${i}]`] = answer.tolerance;
228
+ }
229
+ await setFields(page, { ...values, ...input.fields });
230
+ await submitForm(page, "submitbutton");
231
+ // Moodle lands on the bank, naming the question it changed last.
232
+ const id = Number(new URL(page.url()).searchParams.get("lastchanged")) || undefined;
233
+ return { changed: `Created ${input.type} question${id ? ` ${id}` : ""} "${input.name}" in bank ${activityId}.`, questionId: id };
234
+ }
235
+ finally {
236
+ await page.close().catch(() => undefined);
237
+ }
238
+ }
239
+ //# sourceMappingURL=banks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"banks.js","sourceRoot":"","sources":["../../../src/areas/questions/banks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,123 @@
1
+ import type { BrowserContext, Page } from "playwright-core";
2
+ import type { Trail } from "../../options.js";
3
+ import { MoodleError } from "../../core/errors.js";
4
+ /** The quiz or attempt isn't there, or the person can't do what was asked. */
5
+ export declare class QuizError extends MoodleError {
6
+ name: string;
7
+ constructor(message: string);
8
+ }
9
+ /** A label and value as Moodle shows them (site language). */
10
+ export interface Detail {
11
+ label: string;
12
+ value: string;
13
+ }
14
+ /** What the quiz's page says: its info lines, dates, the person's attempts, whether they can start one. */
15
+ export interface QuizInfo {
16
+ intro: string;
17
+ info: string[];
18
+ dates: Detail[];
19
+ feedback?: string;
20
+ attempts: {
21
+ id: number;
22
+ inProgress: boolean;
23
+ details: Detail[];
24
+ }[];
25
+ canStart: boolean;
26
+ messages: string[];
27
+ }
28
+ /** One input of a question, by its form name, with its options and current value. */
29
+ export interface QuestionField {
30
+ name: string;
31
+ kind: "choice" | "checkbox" | "text" | "select";
32
+ label?: string;
33
+ options?: {
34
+ value: string;
35
+ label: string;
36
+ }[];
37
+ value?: string;
38
+ }
39
+ /** A question as the attempt or review page shows it. */
40
+ export interface Question {
41
+ slot: number;
42
+ page?: number;
43
+ number?: string;
44
+ type: string;
45
+ state: string;
46
+ mark?: string;
47
+ text: string;
48
+ images: string[];
49
+ fields: QuestionField[];
50
+ feedback?: string;
51
+ rightAnswer?: string;
52
+ }
53
+ export declare function readQuizView(page: Page): Promise<QuizInfo>;
54
+ /** The questions on an attempt or review page, with their fields read from the form. */
55
+ export declare function readQuestions(page: Page): Promise<Question[]>;
56
+ /** The pages of an attempt, from its navigation: slot → page. */
57
+ export declare function readNavigation(page: Page): Promise<Record<number, number>>;
58
+ /** A quiz's page as the person sees it: its description and dates, the person's attempts with their summary, and whether they can start one. */
59
+ export declare function quizInfo(context: BrowserContext, siteUrl: string, activityId: number): Promise<QuizInfo>;
60
+ /** Starts an attempt, or continues the one in progress, as the quiz page's button does; returns its id. */
61
+ export declare function startAttempt(context: BrowserContext, siteUrl: string, activityId: number): Promise<number>;
62
+ /** Every question of an attempt, page by page, with the page each is on. */
63
+ export declare function readAttempt(context: BrowserContext, siteUrl: string, attemptId: number): Promise<Question[]>;
64
+ /**
65
+ * Saves answers page by page, as the attempt's "next" button does; `beforeSave` runs with each
66
+ * page's questions (the realistic trail's reading pause). Throws, saving nothing more, on a field
67
+ * a page doesn't have.
68
+ */
69
+ export declare function saveAnswers(context: BrowserContext, siteUrl: string, attemptId: number, answers: {
70
+ slot: number;
71
+ responses: Record<string, string>;
72
+ }[], beforeSave?: (questions: Question[]) => Promise<void>): Promise<void>;
73
+ /** Submits the attempt for grading, as "Submit all and finish" does, without its confirm dialog. */
74
+ export declare function finishAttempt(context: BrowserContext, siteUrl: string, attemptId: number): Promise<void>;
75
+ /** A finished attempt's review, as far as the quiz's review options let the person see it. */
76
+ export declare function readReview(context: BrowserContext, siteUrl: string, attemptId: number): Promise<{
77
+ summary: Detail[];
78
+ questions: Question[];
79
+ }>;
80
+ /** Starts an attempt, or continues the one in progress, and gives its questions. */
81
+ export declare function startQuizAttempt(context: BrowserContext, siteUrl: string, activityId: number): Promise<{
82
+ changed: string;
83
+ attemptId: number;
84
+ questions: Question[];
85
+ }>;
86
+ /**
87
+ * Saves answers in an attempt in progress without finishing it, and gives its questions afterwards.
88
+ * With the realistic trail, it first takes the time a reader needs for the questions and their
89
+ * options (ADR-005), calling `onProgress` with the seconds elapsed and the total.
90
+ */
91
+ export declare function answerQuizQuestions(context: BrowserContext, siteUrl: string, attemptId: number, answers: {
92
+ slot: number;
93
+ responses: Record<string, string>;
94
+ }[], { trail, onProgress }?: {
95
+ trail?: Trail;
96
+ onProgress?: (elapsed: number, total: number) => void;
97
+ }): Promise<{
98
+ changed: string;
99
+ attemptId: number;
100
+ questions: Question[];
101
+ }>;
102
+ /** Submits an attempt for grading, and gives its review when the quiz shows it. */
103
+ export declare function finishQuizAttempt(context: BrowserContext, siteUrl: string, attemptId: number): Promise<{
104
+ changed: string;
105
+ attemptId: number;
106
+ finished: true;
107
+ review: {
108
+ summary: Detail[];
109
+ questions: Question[];
110
+ } | undefined;
111
+ }>;
112
+ /**
113
+ * Sets a user override on a quiz, through its override form: other opening and closing dates, a time
114
+ * limit in minutes, or a number of attempts (0 unlimited); what isn't given stays the quiz's.
115
+ */
116
+ export declare function setQuizOverride(context: BrowserContext, siteUrl: string, activityId: number, userId: number, { open, close, timeLimitMinutes, attempts }: {
117
+ open?: Date;
118
+ close?: Date;
119
+ timeLimitMinutes?: number;
120
+ attempts?: number;
121
+ }): Promise<{
122
+ changed: string;
123
+ }>;