@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,162 @@
1
+ import { callService } from "./ajax.js";
2
+ import { setFields } from "./forms.js";
3
+ import { openSitePage } from "./session.js";
4
+ /** The id isn't an assignment's course module, or the person can't grade it. */
5
+ export class NotAnAssignmentError extends Error {
6
+ name = "NotAnAssignmentError";
7
+ }
8
+ // Ids never change for a course module: one grader page per assignment and site, for the process.
9
+ const idCache = new Map();
10
+ /** Reads `data-assignmentid` and `data-contextid` from a page (the grader's), or `undefined`. */
11
+ export function readAssignmentIds(page) {
12
+ return page.evaluate(() => {
13
+ const g = globalThis;
14
+ const element = g.document.querySelector("[data-assignmentid][data-contextid]");
15
+ const instanceId = Number(element?.getAttribute("data-assignmentid"));
16
+ const contextId = Number(element?.getAttribute("data-contextid"));
17
+ return instanceId > 0 && contextId > 0 ? { instanceId, contextId } : undefined;
18
+ });
19
+ }
20
+ export async function assignmentIds(context, siteUrl, cmid) {
21
+ const key = `${siteUrl}|${cmid}`;
22
+ const cached = idCache.get(key);
23
+ if (cached)
24
+ return cached;
25
+ const page = await openSitePage(context, siteUrl, `mod/assign/view.php?id=${cmid}&action=grader`);
26
+ try {
27
+ const ids = await readAssignmentIds(page);
28
+ if (!ids)
29
+ throw new NotAnAssignmentError(`Activity ${cmid} isn't an assignment you can grade (its grader page has no assignment). Take the id of an assignment (module "assign").`);
30
+ idCache.set(key, ids);
31
+ return ids;
32
+ }
33
+ finally {
34
+ await page.close().catch(() => undefined);
35
+ }
36
+ }
37
+ export function listParticipants(context, siteUrl, instanceId) {
38
+ return callService(context, siteUrl, "mod_assign_list_participants", {
39
+ assignid: instanceId,
40
+ groupid: 0,
41
+ filter: "",
42
+ skip: 0,
43
+ limit: 0,
44
+ onlyids: false,
45
+ includeenrolments: false,
46
+ tablesort: false,
47
+ });
48
+ }
49
+ export function participantDates(context, siteUrl, instanceId, userId) {
50
+ return callService(context, siteUrl, "mod_assign_get_participant", { assignid: instanceId, userid: userId, embeduser: false });
51
+ }
52
+ /**
53
+ * The content of a submission as the grader's panel renders it: per plugin, the full block
54
+ * (`.full_assignsubmission_<plugin>_<sid>`) or, for content short enough to have none, the summary
55
+ * one. Parsed in the page, by class only (ADR-004). The comments plugin isn't the submission.
56
+ */
57
+ export function readSubmissionContent(page, html) {
58
+ return page.evaluate((html) => {
59
+ const g = globalThis;
60
+ const doc = new g.DOMParser().parseFromString(html, "text/html");
61
+ const BLOCKS = new Set(["P", "DIV", "LI", "UL", "OL", "TR", "TABLE", "PRE", "BLOCKQUOTE", "H1", "H2", "H3", "H4", "H5", "H6"]);
62
+ const found = new Map();
63
+ for (const element of Array.from(doc.querySelectorAll('[class*="assignsubmission_"]'))) {
64
+ const match = /\b(full|summary)_assignsubmission_([a-z0-9]+)_(\d+)\b/.exec(element.className);
65
+ if (!match || match[2] === "comments")
66
+ continue;
67
+ const key = `${match[2]}_${match[3]}`;
68
+ const entry = found.get(key) ?? { plugin: match[2] };
69
+ entry[match[1]] = element;
70
+ found.set(key, entry);
71
+ }
72
+ return Array.from(found.values()).map(({ plugin, full, summary }) => {
73
+ const block = (full ?? summary);
74
+ for (const control of Array.from(block.querySelectorAll(".expandsummaryicon, script, style")))
75
+ control.remove();
76
+ const files = Array.from(block.querySelectorAll('a[href*="pluginfile.php"]')).map((a) => ({ name: (a.textContent ?? "").trim(), url: a.getAttribute("href") ?? "" }));
77
+ // Depth first with a stack, not a named recursive function: tsx's keepNames would wrap it in a
78
+ // `__name` helper the page doesn't have (confirmed empirically).
79
+ let raw = "";
80
+ const stack = [block];
81
+ while (stack.length > 0) {
82
+ const item = stack.pop();
83
+ if (typeof item === "string")
84
+ raw += item;
85
+ else if (item.nodeType === 3)
86
+ raw += (item.textContent ?? "").replace(/\s+/g, " ");
87
+ else if (item.nodeName === "BR")
88
+ raw += "\n";
89
+ else if (item.nodeName === "PRE")
90
+ raw += `\n${item.textContent ?? ""}\n`;
91
+ else {
92
+ const isBlock = BLOCKS.has(item.nodeName);
93
+ raw += item.nodeName === "LI" ? "\n- " : isBlock ? "\n" : "";
94
+ // A list item ends where the next one starts: no blank line between items.
95
+ stack.push(isBlock && item.nodeName !== "LI" ? "\n" : "", ...Array.from(item.childNodes).reverse());
96
+ }
97
+ }
98
+ const text = raw
99
+ .replace(/[ \t]+\n/g, "\n")
100
+ .replace(/\n[ \t]+/g, "\n")
101
+ .replace(/\n- +/g, "\n- ")
102
+ .replace(/\n{3,}/g, "\n\n")
103
+ .trim();
104
+ return { plugin, text, files };
105
+ });
106
+ }, html);
107
+ }
108
+ /** The grader panel's HTML for one participant: their current submission and grade form. */
109
+ export async function gradingPanelHtml(context, siteUrl, contextId, userId) {
110
+ const fragment = await callService(context, siteUrl, "core_get_fragment", {
111
+ component: "mod_assign",
112
+ callback: "gradingpanel",
113
+ contextid: contextId,
114
+ args: [
115
+ { name: "userid", value: String(userId) },
116
+ { name: "attemptnumber", value: "-1" },
117
+ { name: "jsonformdata", value: '""' },
118
+ ],
119
+ });
120
+ return fragment.html;
121
+ }
122
+ /** Moodle's grader didn't take the grade. */
123
+ export class GradingError extends Error {
124
+ name = "GradingError";
125
+ }
126
+ /**
127
+ * Fills the grading panel's form for one participant in `page` (a blank tab), and returns it
128
+ * serialized as the grader does (`form.serialize()`), ready for `mod_assign_submit_grading_form`.
129
+ */
130
+ export async function fillGradingForm(page, panelHtml, fields) {
131
+ await page.setContent(panelHtml);
132
+ await setFields(page, fields);
133
+ return page.evaluate(() => {
134
+ const g = globalThis;
135
+ return new g.URLSearchParams(new g.FormData(g.document.querySelector("form.gradeform"))).toString();
136
+ });
137
+ }
138
+ /** The grade and feedback currently in a grading panel, if any. */
139
+ export async function panelGrade(page, panelHtml) {
140
+ await page.setContent(panelHtml);
141
+ return page.evaluate(() => {
142
+ const doc = globalThis.document;
143
+ const grade = doc.querySelector('form.gradeform [name="grade"]')?.value;
144
+ const feedback = doc.querySelector('form.gradeform [name="assignfeedbackcomments_editor[text]"]')?.value;
145
+ // "Current grade in gradebook": the total Moodle computed, also for a rubric or a guide.
146
+ const gradebook = (doc.querySelector('form.gradeform [id^="fitem_id_currentgrade"] .felement')?.textContent ?? "").replace(/\s+/g, " ").trim();
147
+ return { grade: grade || undefined, ...(gradebook && gradebook !== "-" ? { gradebookGrade: gradebook } : {}), feedback: feedback || undefined };
148
+ });
149
+ }
150
+ /** Submits a serialized grading form; Moodle answers warnings when it refuses it. */
151
+ export async function submitGradingForm(context, siteUrl, instanceId, userId, serialized) {
152
+ const warnings = await callService(context, siteUrl, "mod_assign_submit_grading_form", {
153
+ assignmentid: instanceId,
154
+ userid: userId,
155
+ jsonformdata: JSON.stringify(serialized),
156
+ });
157
+ if (warnings.length > 0) {
158
+ // Moodle's form messages don't reach the service's warnings: a grade out of range only says it couldn't save.
159
+ throw new GradingError(`Moodle didn't save the grade: ${warnings.map((w) => w.message ?? w.warningcode).join("; ")} Check the grade is within the assignment's range or scale, and the fields' names.`);
160
+ }
161
+ }
162
+ //# sourceMappingURL=assign.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assign.js","sourceRoot":"","sources":["../../src/moodle/assign.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import type { BrowserContext } from "playwright-core";
2
+ /** Moodle refused the event, or it isn't there. */
3
+ export declare class CalendarError extends Error {
4
+ name: string;
5
+ }
6
+ export interface CalendarEvent {
7
+ id: number;
8
+ name: string;
9
+ /** The event's kind: "course", "group", "user", or an activity's ("due", "open"…). */
10
+ type: string;
11
+ start: string;
12
+ durationMinutes?: number;
13
+ courseId?: number;
14
+ /** For an activity's event, its module and instance. */
15
+ activity?: {
16
+ type: string;
17
+ instance: number;
18
+ };
19
+ }
20
+ /** The person's upcoming events, as the calendar's "Upcoming events" view lists them, in one course or all. */
21
+ export declare function upcomingEvents(context: BrowserContext, siteUrl: string, { courseId }?: {
22
+ courseId?: number;
23
+ }): Promise<CalendarEvent[]>;
24
+ /**
25
+ * Creates a course event, through the calendar's own form: its start, and its duration and
26
+ * description when given. Returns it as the calendar has it.
27
+ */
28
+ export declare function createEvent(context: BrowserContext, siteUrl: string, { courseId, name, start, durationMinutes, description }: {
29
+ courseId: number;
30
+ name: string;
31
+ start: Date;
32
+ durationMinutes?: number;
33
+ description?: string;
34
+ }): Promise<{
35
+ changed: string;
36
+ event: CalendarEvent;
37
+ }>;
38
+ /** Deletes a calendar event the person can manage. */
39
+ export declare function deleteEvent(context: BrowserContext, siteUrl: string, eventId: number): Promise<{
40
+ changed: string;
41
+ }>;
@@ -0,0 +1,80 @@
1
+ import { MoodleServiceError, callService } from "./ajax.js";
2
+ import { isoDate } from "./courses.js";
3
+ import { setFields } from "./forms.js";
4
+ import { decodeEntities } from "./text.js";
5
+ /** Moodle refused the event, or it isn't there. */
6
+ export class CalendarError extends Error {
7
+ name = "CalendarError";
8
+ }
9
+ function toEvent(raw) {
10
+ return {
11
+ id: raw.id,
12
+ name: decodeEntities(raw.name),
13
+ type: raw.eventtype,
14
+ start: isoDate(raw.timestart),
15
+ ...(raw.timeduration > 0 ? { durationMinutes: Math.round(raw.timeduration / 60) } : {}),
16
+ ...(raw.course ? { courseId: raw.course.id } : {}),
17
+ ...(raw.modulename && raw.instance ? { activity: { type: raw.modulename, instance: raw.instance } } : {}),
18
+ };
19
+ }
20
+ /** The person's upcoming events, as the calendar's "Upcoming events" view lists them, in one course or all. */
21
+ export async function upcomingEvents(context, siteUrl, { courseId } = {}) {
22
+ // Course 1 is the site: every course the person is in.
23
+ const { events } = await callService(context, siteUrl, "core_calendar_get_calendar_upcoming_view", { courseid: courseId ?? 1, categoryid: 0 });
24
+ return events.map(toEvent);
25
+ }
26
+ /**
27
+ * Creates a course event, through the calendar's own form: its start, and its duration and
28
+ * description when given. Returns it as the calendar has it.
29
+ */
30
+ export async function createEvent(context, siteUrl, { courseId, name, start, durationMinutes, description }) {
31
+ // The form takes its start as the time it's opened for: no date fields to fill in the person's time zone.
32
+ const fragment = await callService(context, siteUrl, "core_get_fragment", {
33
+ component: "core_calendar",
34
+ callback: "event_form",
35
+ contextid: 1,
36
+ args: [
37
+ { name: "courseid", value: String(courseId) },
38
+ { name: "starttime", value: String(Math.floor(start.getTime() / 1000)) },
39
+ ],
40
+ });
41
+ const page = await context.newPage();
42
+ let formdata;
43
+ try {
44
+ await page.setContent(fragment.html);
45
+ const types = await page.evaluate(() => Array.from(globalThis.document.querySelectorAll('form [name="eventtype"] option')).map((o) => o.value));
46
+ if (!types.includes("course"))
47
+ throw new CalendarError(`You can't add events to course ${courseId}'s calendar.`);
48
+ const values = { name, eventtype: "course", courseid: String(courseId) };
49
+ if (durationMinutes !== undefined)
50
+ Object.assign(values, { duration: 2, timedurationminutes: durationMinutes });
51
+ if (description !== undefined)
52
+ values["description[text]"] = description;
53
+ await setFields(page, values, "form");
54
+ formdata = await page.evaluate(() => {
55
+ const g = globalThis;
56
+ return new g.URLSearchParams(new g.FormData(g.document.querySelector("form"))).toString();
57
+ });
58
+ }
59
+ finally {
60
+ await page.close().catch(() => undefined);
61
+ }
62
+ const answer = await callService(context, siteUrl, "core_calendar_submit_create_update_form", { formdata });
63
+ if (!answer.event || answer.validationerror)
64
+ throw new CalendarError(`Moodle refused the event "${name}": check its name, start and duration.`);
65
+ const event = toEvent(answer.event);
66
+ return { changed: `Event ${event.id} "${event.name}" added to course ${courseId}'s calendar.`, event };
67
+ }
68
+ /** Deletes a calendar event the person can manage. */
69
+ export async function deleteEvent(context, siteUrl, eventId) {
70
+ try {
71
+ await callService(context, siteUrl, "core_calendar_delete_calendar_events", { events: [{ eventid: eventId, repeat: false }] });
72
+ }
73
+ catch (error) {
74
+ if (error instanceof MoodleServiceError && /^invalidrecord/.test(error.errorcode))
75
+ throw new CalendarError(`There's no calendar event ${eventId}: it was deleted, or never existed.`);
76
+ throw error;
77
+ }
78
+ return { changed: `Event ${eventId} deleted.` };
79
+ }
80
+ //# sourceMappingURL=calendar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar.js","sourceRoot":"","sources":["../../src/moodle/calendar.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { BrowserContext, Page } from "playwright-core";
2
+ /** The choice isn't there, or the person can't answer it now. */
3
+ export declare class ChoiceError extends Error {
4
+ name: string;
5
+ }
6
+ export interface Choice {
7
+ id: number;
8
+ name: string;
9
+ intro: string;
10
+ /** The options with the ids answers take; full ones can't be chosen. */
11
+ options: {
12
+ id: number;
13
+ text: string;
14
+ selected: boolean;
15
+ full: boolean;
16
+ }[];
17
+ /** Several options at once. */
18
+ multiple: boolean;
19
+ /** The form is there: the choice is open and the person may answer (or change their answer). */
20
+ canAnswer: boolean;
21
+ /** Whether a given answer can be removed or changed. */
22
+ canChange: boolean;
23
+ /** The options chosen, as the page names them, also where the form is gone. */
24
+ answer: string[];
25
+ /** The page's notices: closed, not open yet, results not published… */
26
+ messages: string[];
27
+ }
28
+ /** The choice page, by its form's inputs and the selection block. */
29
+ export declare function readChoice(page: Page): Promise<Omit<Choice, "id">>;
30
+ /** A choice: its options, whether it can be answered or changed, and the person's answer. */
31
+ export declare function getChoice(context: BrowserContext, siteUrl: string, cmid: number): Promise<Choice>;
32
+ /** Answers a choice with one option (several where it allows them) by id, as "Save my choice" does; returns the choice afterwards. */
33
+ export declare function answerChoice(context: BrowserContext, siteUrl: string, cmid: number, optionIds: number[]): Promise<Choice>;
@@ -0,0 +1,100 @@
1
+ import { inPage } from "./inPage.js";
2
+ import { openSitePage } from "./session.js";
3
+ /** The choice isn't there, or the person can't answer it now. */
4
+ export class ChoiceError extends Error {
5
+ name = "ChoiceError";
6
+ }
7
+ /** The choice page, by its form's inputs and the selection block. */
8
+ export function readChoice(page) {
9
+ // Through inPage: it has a helper function (see the conventions on page.evaluate).
10
+ return inPage(page, () => {
11
+ const doc = globalThis.document;
12
+ const text = (el) => (el?.innerText ?? "").replace(/\s+/g, " ").trim();
13
+ const form = doc.querySelector('#region-main form[action*="/mod/choice/view.php"]');
14
+ const inputs = Array.from(form?.querySelectorAll('input[name="answer"], input[name="answer[]"]') ?? []);
15
+ const options = inputs.map((input) => ({
16
+ id: Number(input.value),
17
+ text: text(doc.querySelector(`label[for="${input.id}"]`)),
18
+ selected: input.checked,
19
+ full: input.disabled,
20
+ }));
21
+ // "Your selection: while" (several joined with "; ")? The label before the colon is the site's language.
22
+ const selection = text(doc.querySelector("#yourselection"));
23
+ const fromBlock = selection.includes(":") ? selection.slice(selection.indexOf(":") + 1).split(/;\s*/).map((s) => s.trim()).filter(Boolean) : [];
24
+ return {
25
+ name: doc.querySelector("[data-activityname]")?.getAttribute("data-activityname") ?? "",
26
+ intro: text(doc.querySelector(".activity-description, #intro")),
27
+ options,
28
+ multiple: inputs.some((input) => input.type === "checkbox"),
29
+ canAnswer: form !== null && options.length > 0,
30
+ canChange: doc.querySelector('#region-main a[href*="action=delchoice"]') !== null,
31
+ answer: options.some((o) => o.selected) ? options.filter((o) => o.selected).map((o) => o.text) : fromBlock,
32
+ messages: Array.from(doc.querySelectorAll("#region-main .alert, #region-main .generalbox"))
33
+ .filter((box) => box.id !== "yourselection" && !box.querySelector("form"))
34
+ .map((box) => {
35
+ // Without the notification's close button, whose label is in the site's language.
36
+ const copy = box.cloneNode(true);
37
+ for (const extra of Array.from(copy.querySelectorAll("button, .visually-hidden")))
38
+ extra.remove();
39
+ return (copy.textContent ?? "").replace(/\s+/g, " ").trim();
40
+ })
41
+ .filter(Boolean),
42
+ };
43
+ }, null);
44
+ }
45
+ async function openChoice(context, siteUrl, cmid) {
46
+ const page = await openSitePage(context, siteUrl, `mod/choice/view.php?id=${cmid}`);
47
+ if ((await page.locator("body#page-mod-choice-view").count()) === 0) {
48
+ await page.close().catch(() => undefined);
49
+ throw new ChoiceError(`Activity ${cmid} isn't a choice you can open. Take the id of a choice (module "choice") from the course structure.`);
50
+ }
51
+ return page;
52
+ }
53
+ /** A choice: its options, whether it can be answered or changed, and the person's answer. */
54
+ export async function getChoice(context, siteUrl, cmid) {
55
+ const page = await openChoice(context, siteUrl, cmid);
56
+ try {
57
+ return { id: cmid, ...(await readChoice(page)) };
58
+ }
59
+ finally {
60
+ await page.close().catch(() => undefined);
61
+ }
62
+ }
63
+ /** Answers a choice with one option (several where it allows them) by id, as "Save my choice" does; returns the choice afterwards. */
64
+ export async function answerChoice(context, siteUrl, cmid, optionIds) {
65
+ if (optionIds.length === 0)
66
+ throw new ChoiceError("Give the id of the option to choose.");
67
+ const page = await openChoice(context, siteUrl, cmid);
68
+ try {
69
+ const choice = await readChoice(page);
70
+ if (!choice.canAnswer) {
71
+ // Moodle explains only some cases (closed before answering); an answered, closed choice just shows the answer.
72
+ const answered = choice.answer.length > 0 ? ` Your answer stays: ${choice.answer.join(", ")}.` : "";
73
+ const said = choice.messages.length > 0 ? ` Moodle says: ${choice.messages.join(" ")}` : "";
74
+ throw new ChoiceError(`You can't answer choice ${cmid} now: it's closed, not open yet, or doesn't let answers change.${answered}${said}`);
75
+ }
76
+ if (!choice.multiple && optionIds.length > 1)
77
+ throw new ChoiceError(`Choice ${cmid} takes one option only.`);
78
+ for (const id of optionIds) {
79
+ const option = choice.options.find((o) => o.id === id);
80
+ if (!option)
81
+ throw new ChoiceError(`Choice ${cmid} has no option ${id}; its options are: ${choice.options.map((o) => `${o.id} "${o.text}"`).join(", ")}.`);
82
+ if (option.full)
83
+ throw new ChoiceError(`Option ${id} "${option.text}" is full.`);
84
+ }
85
+ const form = '#region-main form[action*="/mod/choice/view.php"]';
86
+ for (const option of choice.options) {
87
+ const input = page.locator(`${form} input[name^="answer"][value="${option.id}"]`);
88
+ if (optionIds.includes(option.id))
89
+ await input.check();
90
+ else if (choice.multiple)
91
+ await input.uncheck();
92
+ }
93
+ await Promise.all([page.waitForNavigation({ waitUntil: "domcontentloaded" }), page.click(`${form} [type="submit"]`)]);
94
+ }
95
+ finally {
96
+ await page.close().catch(() => undefined);
97
+ }
98
+ return getChoice(context, siteUrl, cmid);
99
+ }
100
+ //# sourceMappingURL=choice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"choice.js","sourceRoot":"","sources":["../../src/moodle/choice.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import type { BrowserContext, Page } from "playwright-core";
2
+ import { type CompletionState } from "./structure.js";
3
+ /** Completion can't be changed or read there: not enabled, automatic, or not allowed. */
4
+ export declare class CompletionError extends Error {
5
+ name: string;
6
+ }
7
+ /**
8
+ * Marks the person's own completion of an activity with manual completion, done or not, as its
9
+ * "Mark as done" button does; doing it twice changes nothing. An activity that completes on its own
10
+ * gives a `CompletionError`.
11
+ */
12
+ export declare function setCompletion(context: BrowserContext, siteUrl: string, cmid: number, done: boolean): Promise<{
13
+ changed: string;
14
+ }>;
15
+ /** Sets someone's completion of an activity as a teacher does from the progress report; returns the state saved. */
16
+ export declare function overrideCompletion(context: BrowserContext, siteUrl: string, cmid: number, userId: number, state: CompletionState): Promise<{
17
+ changed: string;
18
+ state: CompletionState;
19
+ overridden: boolean;
20
+ }>;
21
+ export interface CompletionCell {
22
+ activityId: number;
23
+ state: CompletionState;
24
+ tracking: "manual" | "auto";
25
+ /** A teacher set it, not the person or Moodle. */
26
+ overridden: boolean;
27
+ }
28
+ export interface CompletionReport {
29
+ activities: {
30
+ id: number;
31
+ name: string;
32
+ type: string;
33
+ }[];
34
+ participants: {
35
+ userId: number;
36
+ fullName: string;
37
+ completion: CompletionCell[];
38
+ }[];
39
+ }
40
+ /** One page of the progress report: its activities from the header links, its rows' icons by name. */
41
+ export declare function readCompletionReport(page: Page): Promise<CompletionReport>;
42
+ /**
43
+ * A course's progress report: every participant's completion of every activity that tracks it, page
44
+ * by page, in one group when given. People come as user id and full name only.
45
+ */
46
+ export declare function completionReport(context: BrowserContext, siteUrl: string, courseId: number, { group }?: {
47
+ group?: number;
48
+ }): Promise<CompletionReport>;
@@ -0,0 +1,108 @@
1
+ import { MoodleServiceError, callService } from "./ajax.js";
2
+ import { openSitePage } from "./session.js";
3
+ import { COMPLETION_STATES } from "./structure.js";
4
+ /** Completion can't be changed or read there: not enabled, automatic, or not allowed. */
5
+ export class CompletionError extends Error {
6
+ name = "CompletionError";
7
+ }
8
+ /** Moodle's refusals, in its terms. */
9
+ function completionError(error, cmid) {
10
+ if (!(error instanceof MoodleServiceError))
11
+ return error;
12
+ if (error.errorcode === "cannotmanualctrack")
13
+ return new CompletionError(`Activity ${cmid} completes automatically (on view, grade, submission…): it can't be marked by hand.`);
14
+ if (error.errorcode === "completionnotenabled")
15
+ return new CompletionError(`Completion isn't enabled in activity ${cmid}'s course.`);
16
+ if (error.errorcode === "nopermissions" || error.errorcode === "nopermission")
17
+ return new CompletionError(`You aren't allowed to change completion in activity ${cmid}.`);
18
+ return error;
19
+ }
20
+ /**
21
+ * Marks the person's own completion of an activity with manual completion, done or not, as its
22
+ * "Mark as done" button does; doing it twice changes nothing. An activity that completes on its own
23
+ * gives a `CompletionError`.
24
+ */
25
+ export async function setCompletion(context, siteUrl, cmid, done) {
26
+ try {
27
+ await callService(context, siteUrl, "core_completion_update_activity_completion_status_manually", { cmid, completed: done });
28
+ }
29
+ catch (error) {
30
+ throw completionError(error, cmid);
31
+ }
32
+ return { changed: `Activity ${cmid} marked ${done ? "done" : "not done"}.` };
33
+ }
34
+ /** Sets someone's completion of an activity as a teacher does from the progress report; returns the state saved. */
35
+ export async function overrideCompletion(context, siteUrl, cmid, userId, state) {
36
+ let saved;
37
+ try {
38
+ saved = await callService(context, siteUrl, "core_completion_override_activity_completion_status", { userid: userId, cmid, newstate: COMPLETION_STATES.indexOf(state) });
39
+ }
40
+ catch (error) {
41
+ throw completionError(error, cmid);
42
+ }
43
+ const now = COMPLETION_STATES[saved.state] ?? "incomplete";
44
+ return { changed: `User ${userId}'s completion of activity ${cmid} set to ${now}.`, state: now, overridden: Boolean(saved.overrideby) };
45
+ }
46
+ /** One page of the progress report: its activities from the header links, its rows' icons by name. */
47
+ export function readCompletionReport(page) {
48
+ return page.evaluate(() => {
49
+ const doc = globalThis.document;
50
+ const STATES = { n: "incomplete", y: "complete", pass: "complete-pass", fail: "complete-fail" };
51
+ const activities = Array.from(doc.querySelectorAll("#completion-progress thead th.completion-header a")).flatMap((link) => {
52
+ const match = /\/mod\/([a-z0-9_]+)\/view\.php\?(?:.*&)?id=(\d+)/.exec(link.getAttribute("href") ?? "");
53
+ return match ? [{ id: Number(match[2]), name: (link.getAttribute("title") ?? link.textContent ?? "").trim(), type: match[1] }] : [];
54
+ });
55
+ const participants = Array.from(doc.querySelectorAll("#completion-progress tbody tr")).flatMap((row) => {
56
+ const link = row.querySelector('th a[href*="/user/view.php?id="]');
57
+ const userId = Number(new URL(link?.getAttribute("href") ?? "", "http://x/").searchParams.get("id"));
58
+ if (!userId)
59
+ return [];
60
+ // One cell per activity, in the header's order; the icon's file names the state.
61
+ const completion = Array.from(row.querySelectorAll("td.completion-progresscell")).flatMap((cell, i) => {
62
+ const icon = /completion-(manual|auto)-(n|y|pass|fail)(-override)?/.exec(cell.querySelector("img")?.getAttribute("src") ?? "");
63
+ const activity = activities[i];
64
+ return icon && activity ? [{ activityId: activity.id, state: STATES[icon[2]], tracking: icon[1], overridden: icon[3] !== undefined }] : [];
65
+ });
66
+ return [{ userId, fullName: (link?.textContent ?? "").replace(/\s+/g, " ").trim(), completion }];
67
+ });
68
+ return { activities, participants };
69
+ });
70
+ }
71
+ const MAX_PAGES = 100;
72
+ /**
73
+ * A course's progress report: every participant's completion of every activity that tracks it, page
74
+ * by page, in one group when given. People come as user id and full name only.
75
+ */
76
+ export async function completionReport(context, siteUrl, courseId, { group } = {}) {
77
+ const report = { activities: [], participants: [] };
78
+ const seen = new Set();
79
+ let start = 0;
80
+ for (let p = 0; p < MAX_PAGES; p++) {
81
+ const page = await openSitePage(context, siteUrl, `report/progress/index.php?course=${courseId}&start=${start}${group ? `&group=${group}` : ""}`);
82
+ try {
83
+ // An error page keeps the report's body id; only the report has its activity selector.
84
+ if ((await page.locator("body#page-report-progress-index #include-activity-select-report").count()) === 0) {
85
+ throw new CompletionError(`You can't see course ${courseId}'s progress report: the course doesn't exist, or you aren't allowed.`);
86
+ }
87
+ if ((await page.locator("#completion-progress").count()) === 0) {
88
+ // The page explains in the site's language: no activity tracks completion, or nobody to list.
89
+ return report;
90
+ }
91
+ const read = await readCompletionReport(page);
92
+ if (p === 0)
93
+ report.activities = read.activities;
94
+ const fresh = read.participants.filter((row) => !seen.has(row.userId));
95
+ if (fresh.length === 0)
96
+ break;
97
+ for (const row of fresh)
98
+ seen.add(row.userId);
99
+ report.participants.push(...fresh);
100
+ start += read.participants.length;
101
+ }
102
+ finally {
103
+ await page.close().catch(() => undefined);
104
+ }
105
+ }
106
+ return report;
107
+ }
108
+ //# sourceMappingURL=completion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completion.js","sourceRoot":"","sources":["../../src/moodle/completion.ts"],"names":[],"mappings":"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"}