@falkenslab/moodle-driver 0.0.0-stage → 0.4.0

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 (300) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -2
  3. package/dist/areas/activities/build.d.ts +98 -0
  4. package/dist/areas/activities/build.js +248 -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/scorm/scorm.d.ts +60 -0
  73. package/dist/areas/scorm/scorm.js +171 -0
  74. package/dist/areas/scorm/scorm.js.map +1 -0
  75. package/dist/areas/site/info.d.ts +25 -0
  76. package/dist/areas/site/info.js +45 -0
  77. package/dist/areas/site/info.js.map +1 -0
  78. package/dist/areas/site/users.d.ts +56 -0
  79. package/dist/areas/site/users.js +89 -0
  80. package/dist/areas/site/users.js.map +1 -0
  81. package/dist/areas/surveys/choice.d.ts +38 -0
  82. package/dist/areas/surveys/choice.js +105 -0
  83. package/dist/areas/surveys/choice.js.map +1 -0
  84. package/dist/areas/surveys/surveys.d.ts +51 -0
  85. package/dist/areas/surveys/surveys.js +201 -0
  86. package/dist/areas/surveys/surveys.js.map +1 -0
  87. package/dist/browser/chromium.d.ts +15 -0
  88. package/dist/browser/chromium.js +53 -0
  89. package/dist/browser/chromium.js.map +1 -0
  90. package/dist/browser/cookies.d.ts +11 -0
  91. package/dist/browser/cookies.js +31 -0
  92. package/dist/browser/cookies.js.map +1 -0
  93. package/dist/browser/manager.d.ts +57 -0
  94. package/dist/browser/manager.js +177 -0
  95. package/dist/browser/manager.js.map +1 -0
  96. package/dist/browser.d.ts +53 -0
  97. package/dist/browser.js +167 -0
  98. package/dist/browser.js.map +1 -0
  99. package/dist/categories.d.ts +28 -0
  100. package/dist/categories.js +225 -0
  101. package/dist/categories.js.map +1 -0
  102. package/dist/chromiumInstaller.d.ts +15 -0
  103. package/dist/chromiumInstaller.js +52 -0
  104. package/dist/chromiumInstaller.js.map +1 -0
  105. package/dist/cli/args.d.ts +33 -0
  106. package/dist/cli/args.js +194 -0
  107. package/dist/cli/args.js.map +1 -0
  108. package/dist/cli/commands.generated.d.ts +4 -0
  109. package/dist/cli/commands.generated.js +2217 -0
  110. package/dist/cli/commands.generated.js.map +1 -0
  111. package/dist/cli/config.d.ts +46 -0
  112. package/dist/cli/config.js +107 -0
  113. package/dist/cli/config.js.map +1 -0
  114. package/dist/cli/main.d.ts +2 -0
  115. package/dist/cli/main.js +19 -0
  116. package/dist/cli/main.js.map +1 -0
  117. package/dist/cli/output.d.ts +4 -0
  118. package/dist/cli/output.js +71 -0
  119. package/dist/cli/output.js.map +1 -0
  120. package/dist/cli/run.d.ts +15 -0
  121. package/dist/cli/run.js +194 -0
  122. package/dist/cli/run.js.map +1 -0
  123. package/dist/cli/spec.d.ts +42 -0
  124. package/dist/cli/spec.js +4 -0
  125. package/dist/cli/spec.js.map +1 -0
  126. package/dist/client.d.ts +224 -0
  127. package/dist/client.js +182 -0
  128. package/dist/client.js.map +1 -0
  129. package/dist/core/ajax.d.ts +25 -0
  130. package/dist/core/ajax.js +85 -0
  131. package/dist/core/ajax.js.map +1 -0
  132. package/dist/core/editor.d.ts +18 -0
  133. package/dist/core/editor.js +51 -0
  134. package/dist/core/editor.js.map +1 -0
  135. package/dist/core/errors.d.ts +12 -0
  136. package/dist/core/errors.js +10 -0
  137. package/dist/core/errors.js.map +1 -0
  138. package/dist/core/files.d.ts +16 -0
  139. package/dist/core/files.js +58 -0
  140. package/dist/core/files.js.map +1 -0
  141. package/dist/core/forms.d.ts +45 -0
  142. package/dist/core/forms.js +200 -0
  143. package/dist/core/forms.js.map +1 -0
  144. package/dist/core/http.d.ts +18 -0
  145. package/dist/core/http.js +22 -0
  146. package/dist/core/http.js.map +1 -0
  147. package/dist/core/inPage.d.ts +8 -0
  148. package/dist/core/inPage.js +10 -0
  149. package/dist/core/inPage.js.map +1 -0
  150. package/dist/core/login.d.ts +40 -0
  151. package/dist/core/login.js +83 -0
  152. package/dist/core/login.js.map +1 -0
  153. package/dist/core/pace.d.ts +4 -0
  154. package/dist/core/pace.js +21 -0
  155. package/dist/core/pace.js.map +1 -0
  156. package/dist/core/session.d.ts +55 -0
  157. package/dist/core/session.js +157 -0
  158. package/dist/core/session.js.map +1 -0
  159. package/dist/core/site.d.ts +8 -0
  160. package/dist/core/site.js +36 -0
  161. package/dist/core/site.js.map +1 -0
  162. package/dist/core/text.d.ts +21 -0
  163. package/dist/core/text.js +113 -0
  164. package/dist/core/text.js.map +1 -0
  165. package/dist/home.d.ts +19 -0
  166. package/dist/home.js +54 -0
  167. package/dist/home.js.map +1 -0
  168. package/dist/index.d.ts +41 -0
  169. package/dist/index.js +52 -0
  170. package/dist/index.js.map +1 -0
  171. package/dist/internals.d.ts +11 -0
  172. package/dist/internals.js +15 -0
  173. package/dist/internals.js.map +1 -0
  174. package/dist/moodle/activities.d.ts +94 -0
  175. package/dist/moodle/activities.js +240 -0
  176. package/dist/moodle/activities.js.map +1 -0
  177. package/dist/moodle/ajax.d.ts +22 -0
  178. package/dist/moodle/ajax.js +91 -0
  179. package/dist/moodle/ajax.js.map +1 -0
  180. package/dist/moodle/assign.d.ts +65 -0
  181. package/dist/moodle/assign.js +162 -0
  182. package/dist/moodle/assign.js.map +1 -0
  183. package/dist/moodle/calendar.d.ts +41 -0
  184. package/dist/moodle/calendar.js +80 -0
  185. package/dist/moodle/calendar.js.map +1 -0
  186. package/dist/moodle/choice.d.ts +33 -0
  187. package/dist/moodle/choice.js +100 -0
  188. package/dist/moodle/choice.js.map +1 -0
  189. package/dist/moodle/completion.d.ts +48 -0
  190. package/dist/moodle/completion.js +108 -0
  191. package/dist/moodle/completion.js.map +1 -0
  192. package/dist/moodle/content.d.ts +43 -0
  193. package/dist/moodle/content.js +105 -0
  194. package/dist/moodle/content.js.map +1 -0
  195. package/dist/moodle/course.d.ts +51 -0
  196. package/dist/moodle/course.js +45 -0
  197. package/dist/moodle/course.js.map +1 -0
  198. package/dist/moodle/courses.d.ts +57 -0
  199. package/dist/moodle/courses.js +70 -0
  200. package/dist/moodle/courses.js.map +1 -0
  201. package/dist/moodle/editor.d.ts +18 -0
  202. package/dist/moodle/editor.js +50 -0
  203. package/dist/moodle/editor.js.map +1 -0
  204. package/dist/moodle/files.d.ts +16 -0
  205. package/dist/moodle/files.js +57 -0
  206. package/dist/moodle/files.js.map +1 -0
  207. package/dist/moodle/forms.d.ts +44 -0
  208. package/dist/moodle/forms.js +199 -0
  209. package/dist/moodle/forms.js.map +1 -0
  210. package/dist/moodle/forum.d.ts +55 -0
  211. package/dist/moodle/forum.js +115 -0
  212. package/dist/moodle/forum.js.map +1 -0
  213. package/dist/moodle/forumPosts.d.ts +83 -0
  214. package/dist/moodle/forumPosts.js +113 -0
  215. package/dist/moodle/forumPosts.js.map +1 -0
  216. package/dist/moodle/gradebook.d.ts +102 -0
  217. package/dist/moodle/gradebook.js +246 -0
  218. package/dist/moodle/gradebook.js.map +1 -0
  219. package/dist/moodle/grades.d.ts +24 -0
  220. package/dist/moodle/grades.js +67 -0
  221. package/dist/moodle/grades.js.map +1 -0
  222. package/dist/moodle/grading.d.ts +92 -0
  223. package/dist/moodle/grading.js +161 -0
  224. package/dist/moodle/grading.js.map +1 -0
  225. package/dist/moodle/h5p.d.ts +31 -0
  226. package/dist/moodle/h5p.js +100 -0
  227. package/dist/moodle/h5p.js.map +1 -0
  228. package/dist/moodle/inPage.d.ts +8 -0
  229. package/dist/moodle/inPage.js +10 -0
  230. package/dist/moodle/inPage.js.map +1 -0
  231. package/dist/moodle/lesson.d.ts +57 -0
  232. package/dist/moodle/lesson.js +140 -0
  233. package/dist/moodle/lesson.js.map +1 -0
  234. package/dist/moodle/login.d.ts +37 -0
  235. package/dist/moodle/login.js +77 -0
  236. package/dist/moodle/login.js.map +1 -0
  237. package/dist/moodle/messages.d.ts +66 -0
  238. package/dist/moodle/messages.js +80 -0
  239. package/dist/moodle/messages.js.map +1 -0
  240. package/dist/moodle/overview.d.ts +83 -0
  241. package/dist/moodle/overview.js +137 -0
  242. package/dist/moodle/overview.js.map +1 -0
  243. package/dist/moodle/pace.d.ts +4 -0
  244. package/dist/moodle/pace.js +19 -0
  245. package/dist/moodle/pace.js.map +1 -0
  246. package/dist/moodle/participants.d.ts +55 -0
  247. package/dist/moodle/participants.js +131 -0
  248. package/dist/moodle/participants.js.map +1 -0
  249. package/dist/moodle/questionBank.d.ts +81 -0
  250. package/dist/moodle/questionBank.js +235 -0
  251. package/dist/moodle/questionBank.js.map +1 -0
  252. package/dist/moodle/quiz.d.ts +117 -0
  253. package/dist/moodle/quiz.js +291 -0
  254. package/dist/moodle/quiz.js.map +1 -0
  255. package/dist/moodle/quizResults.d.ts +36 -0
  256. package/dist/moodle/quizResults.js +93 -0
  257. package/dist/moodle/quizResults.js.map +1 -0
  258. package/dist/moodle/quizStructure.d.ts +42 -0
  259. package/dist/moodle/quizStructure.js +100 -0
  260. package/dist/moodle/quizStructure.js.map +1 -0
  261. package/dist/moodle/session.d.ts +39 -0
  262. package/dist/moodle/session.js +99 -0
  263. package/dist/moodle/session.js.map +1 -0
  264. package/dist/moodle/site.d.ts +8 -0
  265. package/dist/moodle/site.js +36 -0
  266. package/dist/moodle/site.js.map +1 -0
  267. package/dist/moodle/siteInfo.d.ts +25 -0
  268. package/dist/moodle/siteInfo.js +45 -0
  269. package/dist/moodle/siteInfo.js.map +1 -0
  270. package/dist/moodle/structure.d.ts +94 -0
  271. package/dist/moodle/structure.js +103 -0
  272. package/dist/moodle/structure.js.map +1 -0
  273. package/dist/moodle/submission.d.ts +48 -0
  274. package/dist/moodle/submission.js +109 -0
  275. package/dist/moodle/submission.js.map +1 -0
  276. package/dist/moodle/survey.d.ts +47 -0
  277. package/dist/moodle/survey.js +197 -0
  278. package/dist/moodle/survey.js.map +1 -0
  279. package/dist/moodle/text.d.ts +21 -0
  280. package/dist/moodle/text.js +113 -0
  281. package/dist/moodle/text.js.map +1 -0
  282. package/dist/moodle/user.d.ts +54 -0
  283. package/dist/moodle/user.js +78 -0
  284. package/dist/moodle/user.js.map +1 -0
  285. package/dist/options.d.ts +30 -0
  286. package/dist/options.js +35 -0
  287. package/dist/options.js.map +1 -0
  288. package/dist/sessionCookies.d.ts +11 -0
  289. package/dist/sessionCookies.js +31 -0
  290. package/dist/sessionCookies.js.map +1 -0
  291. package/dist/testing/fakeMoodle.d.ts +55 -0
  292. package/dist/testing/fakeMoodle.js +103 -0
  293. package/dist/testing/fakeMoodle.js.map +1 -0
  294. package/dist/testing/gradingPanel.d.ts +6 -0
  295. package/dist/testing/gradingPanel.js +22 -0
  296. package/dist/testing/gradingPanel.js.map +1 -0
  297. package/dist/testing/index.d.ts +5 -0
  298. package/dist/testing/index.js +18 -0
  299. package/dist/testing/index.js.map +1 -0
  300. package/package.json +78 -4
@@ -0,0 +1,39 @@
1
+ import type { BrowserContext, Page } from "playwright-core";
2
+ /** No live Moodle session: the person has to log in again (ADR-002). */
3
+ export declare class NotLoggedInError extends Error {
4
+ name: string;
5
+ constructor();
6
+ }
7
+ /** What a Moodle page says about its session, read without depending on the interface's language. */
8
+ export interface SessionFacts {
9
+ /** Moodle adds the `notloggedin` body class when nobody is logged in. */
10
+ notLoggedIn: boolean;
11
+ /** `M.cfg.userId`: 0 when logged out; `null` on a page that isn't Moodle's (an SSO provider's). */
12
+ userId: number | null;
13
+ /** A logout link, in the user menu of a real user's pages. */
14
+ hasLogoutLink: boolean;
15
+ /** A login link: a guest (who also has a user id) gets one and no logout link. */
16
+ hasLoginLink: boolean;
17
+ }
18
+ export declare function readSessionFacts(page: Page): Promise<SessionFacts>;
19
+ /** Whether a page at `url`, with these facts, belongs to the site and has a real user logged in. */
20
+ export declare function isLoggedIn(url: string, siteUrl: string, facts: SessionFacts): boolean;
21
+ /** Whether `page`, as it is now, shows the site with a real user logged in. */
22
+ export declare function pageIsLoggedIn(page: Page, siteUrl: string): Promise<boolean>;
23
+ /** Has `show` called with the page each function opened, once it's done with it. */
24
+ export declare function followPages(context: BrowserContext, show: (url: string) => Promise<void>): void;
25
+ /**
26
+ * Opens a page of the site (`path` relative to it) in a new tab and checks the session; throws
27
+ * `NotLoggedInError`, closing the tab, when Moodle answers with its login page.
28
+ */
29
+ export declare function openSitePage(context: BrowserContext, siteUrl: string, path: string, { follow }?: {
30
+ follow?: boolean | undefined;
31
+ }): Promise<Page>;
32
+ /**
33
+ * Logs the person out of Moodle through the user menu's logout link (it carries the sesskey; without
34
+ * one Moodle asks to confirm, and the confirmation is accepted). Returns false when nobody was logged
35
+ * in. Only Moodle's session ends: a single sign-on provider may keep its own.
36
+ */
37
+ export declare function logOut(context: BrowserContext, siteUrl: string): Promise<boolean>;
38
+ /** Moodle's message on an error page (its fatal error box: no permission, no such record), which keeps the page's body id. */
39
+ export declare function moodleErrorMessage(page: Page): Promise<string | undefined>;
@@ -0,0 +1,99 @@
1
+ /** No live Moodle session: the person has to log in again (ADR-002). */
2
+ export class NotLoggedInError extends Error {
3
+ name = "NotLoggedInError";
4
+ constructor() {
5
+ super("There is no Moodle session, or it expired: the person has to log in again.");
6
+ }
7
+ }
8
+ export function readSessionFacts(page) {
9
+ return page.evaluate(() => {
10
+ // Runs in the page: Moodle's globals and the DOM, typed loosely on purpose (no DOM lib in Node).
11
+ const g = globalThis;
12
+ const userId = g.M?.cfg?.userId;
13
+ return {
14
+ notLoggedIn: g.document.body?.classList.contains("notloggedin") ?? true,
15
+ userId: typeof userId === "number" ? userId : null,
16
+ hasLogoutLink: g.document.querySelector('a[href*="/login/logout.php"]') !== null,
17
+ hasLoginLink: g.document.querySelector('a[href*="/login/index.php"]') !== null,
18
+ };
19
+ });
20
+ }
21
+ /** Whether a page at `url`, with these facts, belongs to the site and has a real user logged in. */
22
+ export function isLoggedIn(url, siteUrl, facts) {
23
+ if (!url.startsWith(siteUrl))
24
+ return false;
25
+ if (url.startsWith(new URL("login/", siteUrl).href))
26
+ return false;
27
+ // Pages without the navigation bar (the assignment grader) have neither link (confirmed empirically,
28
+ // Moodle 5.2): only a login link without a logout one means a guest.
29
+ const guest = facts.hasLoginLink && !facts.hasLogoutLink;
30
+ return !facts.notLoggedIn && (facts.userId ?? 0) > 0 && !guest;
31
+ }
32
+ /** Whether `page`, as it is now, shows the site with a real user logged in. */
33
+ export async function pageIsLoggedIn(page, siteUrl) {
34
+ return isLoggedIn(page.url(), siteUrl, await readSessionFacts(page));
35
+ }
36
+ // While the person watches (ADR-007), a context's browser manager shows the pages functions worked on.
37
+ const followers = new WeakMap();
38
+ /** Has `show` called with the page each function opened, once it's done with it. */
39
+ export function followPages(context, show) {
40
+ followers.set(context, show);
41
+ }
42
+ /**
43
+ * Opens a page of the site (`path` relative to it) in a new tab and checks the session; throws
44
+ * `NotLoggedInError`, closing the tab, when Moodle answers with its login page.
45
+ */
46
+ export async function openSitePage(context, siteUrl, path, { follow = true } = {}) {
47
+ const page = await context.newPage();
48
+ try {
49
+ await page.goto(new URL(path, siteUrl).href, { waitUntil: "domcontentloaded" });
50
+ if (!(await pageIsLoggedIn(page, siteUrl)))
51
+ throw new NotLoggedInError();
52
+ // The page as it was opened (a form's URL, not where posting it led), once it's closed.
53
+ const opened = page.url();
54
+ if (follow)
55
+ page.once("close", () => void followers.get(context)?.(opened));
56
+ return page;
57
+ }
58
+ catch (error) {
59
+ await page.close().catch(() => undefined);
60
+ throw error;
61
+ }
62
+ }
63
+ /**
64
+ * Logs the person out of Moodle through the user menu's logout link (it carries the sesskey; without
65
+ * one Moodle asks to confirm, and the confirmation is accepted). Returns false when nobody was logged
66
+ * in. Only Moodle's session ends: a single sign-on provider may keep its own.
67
+ */
68
+ export async function logOut(context, siteUrl) {
69
+ const page = await context.newPage();
70
+ const home = new URL("my/", siteUrl).href;
71
+ try {
72
+ await page.goto(home, { waitUntil: "domcontentloaded" });
73
+ if (!(await pageIsLoggedIn(page, siteUrl)))
74
+ return false;
75
+ const link = await page.locator('a[href*="/login/logout.php"]').first().getAttribute("href").catch(() => null);
76
+ // Moodle's redirect after logging out can abort this navigation (ERR_ABORTED, confirmed empirically,
77
+ // Moodle 5.2); whether the session ended is checked below either way.
78
+ await page.goto(new URL(link ?? "login/logout.php", siteUrl).href, { waitUntil: "domcontentloaded" }).catch((error) => {
79
+ if (!/ERR_ABORTED/.test(error.message))
80
+ throw error;
81
+ });
82
+ const confirm = page.locator('form[action*="/login/logout.php"] [type="submit"]');
83
+ if ((await confirm.count()) > 0)
84
+ await Promise.all([page.waitForNavigation({ waitUntil: "domcontentloaded" }), confirm.first().click()]);
85
+ await page.goto(home, { waitUntil: "domcontentloaded" });
86
+ if (await pageIsLoggedIn(page, siteUrl))
87
+ throw new Error("Moodle didn't end the session: the person is still logged in. Try again, or log out in the browser.");
88
+ return true;
89
+ }
90
+ finally {
91
+ await page.close().catch(() => undefined);
92
+ }
93
+ }
94
+ /** Moodle's message on an error page (its fatal error box: no permission, no such record), which keeps the page's body id. */
95
+ export async function moodleErrorMessage(page) {
96
+ const box = page.locator('[data-rel="fatalerror"] .errormessage');
97
+ return (await box.count()) > 0 ? ((await box.first().textContent()) ?? "").replace(/\s+/g, " ").trim() : undefined;
98
+ }
99
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/moodle/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /** `M.cfg.wwwroot` in a Moodle page, JSON-escaped as Moodle writes it (`"http:\/\/host"`). */
2
+ export declare function readWwwroot(html: string): string | undefined;
3
+ /**
4
+ * The site's base URL for any URL of it (the site, a course, the login page), read from the
5
+ * `M.cfg.wwwroot` Moodle puts in every page; fetched without the browser or any session. Throws
6
+ * `MoodleUrlError` when it can't be reached or isn't a Moodle.
7
+ */
8
+ export declare function findMoodleSite(url: string, fetchImpl?: typeof fetch): Promise<string>;
@@ -0,0 +1,36 @@
1
+ import { MoodleUrlError, parseSiteUrl } from "../options.js";
2
+ // The first answer of a site that just started can take long (confirmed empirically, the sandbox).
3
+ const TIMEOUT_MS = 60_000;
4
+ /** `M.cfg.wwwroot` in a Moodle page, JSON-escaped as Moodle writes it (`"http:\/\/host"`). */
5
+ export function readWwwroot(html) {
6
+ const match = /"wwwroot":"((?:[^"\\]|\\.)*)"/.exec(html);
7
+ if (!match)
8
+ return undefined;
9
+ try {
10
+ return JSON.parse(`"${match[1]}"`);
11
+ }
12
+ catch {
13
+ return undefined;
14
+ }
15
+ }
16
+ /**
17
+ * The site's base URL for any URL of it (the site, a course, the login page), read from the
18
+ * `M.cfg.wwwroot` Moodle puts in every page; fetched without the browser or any session. Throws
19
+ * `MoodleUrlError` when it can't be reached or isn't a Moodle.
20
+ */
21
+ export async function findMoodleSite(url, fetchImpl = fetch) {
22
+ parseSiteUrl(url); // http(s) and well formed, or MoodleUrlError.
23
+ let html;
24
+ try {
25
+ const response = await fetchImpl(url.trim(), { redirect: "follow", signal: AbortSignal.timeout(TIMEOUT_MS) });
26
+ html = await response.text();
27
+ }
28
+ catch (error) {
29
+ throw new MoodleUrlError(`Couldn't reach ${url}: ${error.message}. Check the URL and that the site is up.`);
30
+ }
31
+ const wwwroot = readWwwroot(html);
32
+ if (!wwwroot)
33
+ throw new MoodleUrlError(`${url} doesn't look like a Moodle site (its page has no M.cfg.wwwroot). Check the URL.`);
34
+ return parseSiteUrl(wwwroot);
35
+ }
36
+ //# sourceMappingURL=site.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"site.js","sourceRoot":"","sources":["../../src/moodle/site.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7D,mGAAmG;AACnG,MAAM,UAAU,GAAG,MAAM,CAAC;AAE1B,8FAA8F;AAC9F,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,KAAK,GAAG,+BAA+B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzD,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,CAAW,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAW,EAAE,YAA0B,KAAK;IAC/E,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,8CAA8C;IACjE,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAC9G,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,cAAc,CAAC,kBAAkB,GAAG,KAAM,KAAe,CAAC,OAAO,0CAA0C,CAAC,CAAC;IACzH,CAAC;IACD,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,cAAc,CAAC,GAAG,GAAG,kFAAkF,CAAC,CAAC;IACjI,OAAO,YAAY,CAAC,OAAO,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { BrowserContext } from "playwright-core";
2
+ /**
3
+ * What the site tells anyone about itself, through Moodle's AJAX entry point for services that need
4
+ * no login (`service-nologin.php`): readable before logging in, without the browser. Moodle shows its
5
+ * version only to administrators, so it isn't here.
6
+ */
7
+ export declare function publicSiteInfo(siteUrl: string, fetchImpl?: typeof fetch): Promise<{
8
+ supportPage?: string | undefined;
9
+ maintenance?: string | undefined;
10
+ login: {
11
+ guestAccess: boolean;
12
+ identityProviders: string[];
13
+ appLogin?: "app" | "browser" | "embedded" | undefined;
14
+ };
15
+ languages?: string[] | undefined;
16
+ name: string;
17
+ url: string;
18
+ language: string | undefined;
19
+ }>;
20
+ /** The activity and resource types the person can add to a course: none when they can't edit it. */
21
+ export declare function courseActivityTypes(context: BrowserContext, siteUrl: string, courseId: number): Promise<{
22
+ purpose?: string | undefined;
23
+ type: string;
24
+ title: string;
25
+ }[]>;
@@ -0,0 +1,45 @@
1
+ import { callService, parseServiceResponse } from "./ajax.js";
2
+ import { decodeEntities } from "./text.js";
3
+ const TIMEOUT_MS = 60_000;
4
+ // typeoflogin: how the mobile app is told to log in, which says whether the site uses its own form.
5
+ const LOGIN_TYPES = { 1: "app", 2: "browser", 3: "embedded" };
6
+ /**
7
+ * What the site tells anyone about itself, through Moodle's AJAX entry point for services that need
8
+ * no login (`service-nologin.php`): readable before logging in, without the browser. Moodle shows its
9
+ * version only to administrators, so it isn't here.
10
+ */
11
+ export async function publicSiteInfo(siteUrl, fetchImpl = fetch) {
12
+ const methodname = "tool_mobile_get_public_config";
13
+ const response = await fetchImpl(new URL(`lib/ajax/service-nologin.php?info=${methodname}`, siteUrl).href, {
14
+ method: "POST",
15
+ headers: { "Content-Type": "application/json" },
16
+ body: JSON.stringify([{ index: 0, methodname, args: {} }]),
17
+ signal: AbortSignal.timeout(TIMEOUT_MS),
18
+ });
19
+ const config = parseServiceResponse(methodname, response.status, await response.text());
20
+ // "en|English,es|Español": the languages the menu offers; empty means every installed one.
21
+ const languages = (config.langlist ?? "").split(",").map((entry) => entry.split("|")[0]?.trim() ?? "").filter(Boolean);
22
+ return {
23
+ name: decodeEntities(config.sitename),
24
+ url: config.wwwroot,
25
+ language: config.lang,
26
+ ...(languages.length > 0 ? { languages } : {}),
27
+ login: {
28
+ ...(config.typeoflogin !== undefined && LOGIN_TYPES[config.typeoflogin] ? { appLogin: LOGIN_TYPES[config.typeoflogin] } : {}),
29
+ guestAccess: config.guestlogin === 1,
30
+ identityProviders: (config.identityproviders ?? []).map((p) => decodeEntities(p.name)),
31
+ },
32
+ ...(config.maintenanceenabled ? { maintenance: decodeEntities(config.maintenancemessage ?? "") || "The site is in maintenance mode." } : {}),
33
+ ...(config.supportpage ? { supportPage: config.supportpage } : {}),
34
+ };
35
+ }
36
+ /** The activity and resource types the person can add to a course: none when they can't edit it. */
37
+ export async function courseActivityTypes(context, siteUrl, courseId) {
38
+ const { content_items: items } = await callService(context, siteUrl, "core_course_get_course_content_items", { courseid: courseId });
39
+ // A type with several items (LTI tools) is listed once.
40
+ const seen = new Set();
41
+ return items
42
+ .filter((item) => !seen.has(item.name) && seen.add(item.name))
43
+ .map((item) => ({ type: item.name, title: decodeEntities(item.title), ...(item.purpose ? { purpose: item.purpose } : {}) }));
44
+ }
45
+ //# sourceMappingURL=siteInfo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"siteInfo.js","sourceRoot":"","sources":["../../src/moodle/siteInfo.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ import type { BrowserContext } from "playwright-core";
2
+ import { type ActivityDetails, type CourseState } from "./course.js";
3
+ /** Moodle's completion states (COMPLETION_INCOMPLETE … COMPLETION_COMPLETE_FAIL), by their value. */
4
+ export declare const COMPLETION_STATES: readonly ["incomplete", "complete", "complete-pass", "complete-fail"];
5
+ export type CompletionState = (typeof COMPLETION_STATES)[number];
6
+ export interface CourseActivity {
7
+ /** The course module id, which every activity function takes. */
8
+ id: number;
9
+ name: string;
10
+ /** The module: "page", "assign", "quiz", "forum", "resource"… */
11
+ type: string;
12
+ url?: string;
13
+ /** Shown to students. */
14
+ visible: boolean;
15
+ /** The person can open it now. */
16
+ availableToMe: boolean;
17
+ restricted: boolean;
18
+ /** The person's completion, when the activity tracks it for them. */
19
+ completion?: {
20
+ state: CompletionState;
21
+ complete: boolean;
22
+ };
23
+ /** With details: as the course page shows them, in the site's language. */
24
+ dates?: {
25
+ label: string;
26
+ value: string;
27
+ }[];
28
+ /** With details: the restriction as the course page shows it. */
29
+ restriction?: string;
30
+ }
31
+ export interface CourseSection {
32
+ id: number;
33
+ number: number;
34
+ title: string;
35
+ visible: boolean;
36
+ restricted: boolean;
37
+ url?: string;
38
+ /** For a subsection, the section that holds it. */
39
+ parentSectionId?: number;
40
+ activities: CourseActivity[];
41
+ }
42
+ /** The state as sections with their activities, in the course's order; details merged when given. */
43
+ export declare function structureOf(state: CourseState, details?: Map<number, ActivityDetails>): CourseSection[];
44
+ /** A course's sections and activities; with `details`, each activity's dates and restriction from the course page (slower). */
45
+ export declare function courseStructure(context: BrowserContext, siteUrl: string, courseId: number, { details }?: {
46
+ details?: boolean | undefined;
47
+ }): Promise<{
48
+ course: {
49
+ id: number;
50
+ url: string | undefined;
51
+ };
52
+ sections: CourseSection[];
53
+ }>;
54
+ export interface CourseAssignment {
55
+ /** The assignment's course module id. */
56
+ id: number;
57
+ name: string;
58
+ url: string;
59
+ visible: boolean;
60
+ section: {
61
+ number: number;
62
+ title: string;
63
+ };
64
+ }
65
+ /** The course's assignments, in the course's order. */
66
+ export declare function assignmentsOf(state: CourseState): CourseAssignment[];
67
+ export declare function listAssignments(context: BrowserContext, siteUrl: string, courseId: number): Promise<CourseAssignment[]>;
68
+ export interface PendingActivity {
69
+ activityId: number;
70
+ name: string;
71
+ type: string;
72
+ url?: string;
73
+ section: {
74
+ number: number;
75
+ title: string;
76
+ };
77
+ /** Tracked and not complete, or (completion not tracked) graded and not graded yet. */
78
+ reason: "incomplete" | "not-graded";
79
+ completion?: string;
80
+ availableToMe: boolean;
81
+ dates?: {
82
+ label: string;
83
+ value: string;
84
+ }[];
85
+ restriction?: string;
86
+ }
87
+ /**
88
+ * What the person still has to do in a course: activities whose completion is tracked and not
89
+ * complete or, where completion isn't tracked, graded activities without a grade yet.
90
+ */
91
+ export declare function pendingActivities(context: BrowserContext, siteUrl: string, courseId: number): Promise<{
92
+ completionTracked: boolean;
93
+ pending: PendingActivity[];
94
+ }>;
@@ -0,0 +1,103 @@
1
+ import { activityDetails, courseState } from "./course.js";
2
+ import { myGrades } from "./grades.js";
3
+ import { decodeEntities } from "./text.js";
4
+ /** Moodle's completion states (COMPLETION_INCOMPLETE … COMPLETION_COMPLETE_FAIL), by their value. */
5
+ export const COMPLETION_STATES = ["incomplete", "complete", "complete-pass", "complete-fail"];
6
+ /** The state as sections with their activities, in the course's order; details merged when given. */
7
+ export function structureOf(state, details) {
8
+ const cms = new Map(state.cm.map((cm) => [cm.id, cm]));
9
+ return state.section.map((s) => {
10
+ const ids = s.cmlist ?? state.cm.filter((cm) => cm.sectionid === s.id).map((cm) => cm.id);
11
+ const activities = ids.flatMap((id) => {
12
+ const cm = cms.get(id);
13
+ if (!cm)
14
+ return [];
15
+ const extra = details?.get(Number(cm.id));
16
+ const state = cm.istrackeduser && cm.completionstate !== undefined ? COMPLETION_STATES[cm.completionstate] : undefined;
17
+ return [
18
+ {
19
+ id: Number(cm.id),
20
+ name: decodeEntities(cm.name),
21
+ type: cm.module,
22
+ url: cm.url,
23
+ visible: cm.visible,
24
+ availableToMe: (cm.uservisible ?? true) && (cm.accessvisible ?? true),
25
+ restricted: cm.hascmrestrictions ?? false,
26
+ completion: state ? { state, complete: cm.isoverallcomplete ?? state !== "incomplete" } : undefined,
27
+ dates: extra && extra.dates.length > 0 ? extra.dates : undefined,
28
+ restriction: extra?.restriction,
29
+ },
30
+ ];
31
+ });
32
+ return {
33
+ id: Number(s.id),
34
+ number: s.number,
35
+ title: decodeEntities(s.title),
36
+ visible: s.visible ?? true,
37
+ restricted: s.hasrestrictions ?? false,
38
+ url: s.sectionurl,
39
+ parentSectionId: s.parentsectionid ? Number(s.parentsectionid) : undefined,
40
+ activities,
41
+ };
42
+ });
43
+ }
44
+ /** A course's sections and activities; with `details`, each activity's dates and restriction from the course page (slower). */
45
+ export async function courseStructure(context, siteUrl, courseId, { details = false } = {}) {
46
+ const state = await courseState(context, siteUrl, courseId);
47
+ const extra = details ? await activityDetails(context, siteUrl, courseId, state) : undefined;
48
+ return { course: { id: courseId, url: state.course?.baseurl }, sections: structureOf(state, extra) };
49
+ }
50
+ /** The course's assignments, in the course's order. */
51
+ export function assignmentsOf(state) {
52
+ const sections = new Map(state.section.map((section) => [section.id, section]));
53
+ return state.cm
54
+ .filter((cm) => cm.module === "assign")
55
+ .map((cm) => {
56
+ const section = sections.get(cm.sectionid);
57
+ return {
58
+ id: Number(cm.id),
59
+ name: decodeEntities(cm.name),
60
+ url: cm.url ?? "",
61
+ visible: cm.visible,
62
+ section: { number: section?.number ?? 0, title: decodeEntities(section?.title ?? "") },
63
+ };
64
+ });
65
+ }
66
+ export async function listAssignments(context, siteUrl, courseId) {
67
+ return assignmentsOf(await courseState(context, siteUrl, courseId));
68
+ }
69
+ /**
70
+ * What the person still has to do in a course: activities whose completion is tracked and not
71
+ * complete or, where completion isn't tracked, graded activities without a grade yet.
72
+ */
73
+ export async function pendingActivities(context, siteUrl, courseId) {
74
+ const state = await courseState(context, siteUrl, courseId);
75
+ const sections = structureOf(state, await activityDetails(context, siteUrl, courseId, state));
76
+ const ungraded = new Set((await myGrades(context, siteUrl, courseId).catch(() => [])).filter((row) => row.kind === "item" && row.activityId && !row.grade).map((row) => row.activityId));
77
+ const completionTracked = state.cm.some((cm) => cm.istrackeduser && cm.completionstate !== undefined);
78
+ const pending = [];
79
+ for (const section of sections) {
80
+ for (const activity of section.activities) {
81
+ if (!activity.visible && !activity.availableToMe)
82
+ continue;
83
+ const tracked = activity.completion !== undefined;
84
+ const reason = tracked ? (activity.completion.complete ? undefined : "incomplete") : ungraded.has(activity.id) ? "not-graded" : undefined;
85
+ if (!reason)
86
+ continue;
87
+ pending.push({
88
+ activityId: activity.id,
89
+ name: activity.name,
90
+ type: activity.type,
91
+ url: activity.url,
92
+ section: { number: section.number, title: section.title },
93
+ reason,
94
+ completion: activity.completion?.state,
95
+ availableToMe: activity.availableToMe,
96
+ dates: activity.dates,
97
+ restriction: activity.restriction,
98
+ });
99
+ }
100
+ }
101
+ return { completionTracked, pending };
102
+ }
103
+ //# sourceMappingURL=structure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structure.js","sourceRoot":"","sources":["../../src/moodle/structure.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import type { BrowserContext, Page } from "playwright-core";
2
+ /** The assignment isn't there, or the person can't do that now. */
3
+ export declare class SubmissionError extends Error {
4
+ name: string;
5
+ }
6
+ /** An assignment as the student's page shows it. */
7
+ export interface StudentAssignment {
8
+ description: string;
9
+ descriptionFiles: {
10
+ name: string;
11
+ url: string;
12
+ }[];
13
+ dates: {
14
+ label: string;
15
+ value: string;
16
+ }[];
17
+ status: {
18
+ label: string;
19
+ value: string;
20
+ }[];
21
+ submissionState?: string;
22
+ graded: boolean;
23
+ submittedFiles: {
24
+ name: string;
25
+ url: string;
26
+ }[];
27
+ feedback: {
28
+ label: string;
29
+ value: string;
30
+ }[];
31
+ canEdit: boolean;
32
+ canSubmit: boolean;
33
+ }
34
+ export declare function readStudentAssignment(page: Page): Promise<StudentAssignment>;
35
+ export declare function studentAssignment(context: BrowserContext, siteUrl: string, cmid: number): Promise<StudentAssignment>;
36
+ /** Saves the submission's online text and adds files, as "Save changes" on the edit page does; gives the assignment afterwards. */
37
+ export declare function saveSubmission(context: BrowserContext, siteUrl: string, cmid: number, input: {
38
+ onlineText?: string;
39
+ files?: string[];
40
+ }): Promise<StudentAssignment>;
41
+ /**
42
+ * Submits the saved submission for grading, accepting the site's statement when it asks for one
43
+ * (`acceptStatement`, once the person agrees; a bare boolean as before 0.2 too); gives the
44
+ * assignment afterwards.
45
+ */
46
+ export declare function submitForGrading(context: BrowserContext, siteUrl: string, cmid: number, options?: {
47
+ acceptStatement?: boolean;
48
+ } | boolean): Promise<StudentAssignment>;
@@ -0,0 +1,109 @@
1
+ import { checkLocalFiles } from "./files.js";
2
+ import { formValue, openForm, setFields, submitForm, uploadToDraft } from "./forms.js";
3
+ import { inPage } from "./inPage.js";
4
+ import { NotLoggedInError, openSitePage } from "./session.js";
5
+ /** The assignment isn't there, or the person can't do that now. */
6
+ export class SubmissionError extends Error {
7
+ name = "SubmissionError";
8
+ }
9
+ export function readStudentAssignment(page) {
10
+ return inPage(page, () => {
11
+ const doc = globalThis.document;
12
+ const text = (el) => (el?.innerText ?? "")
13
+ .replace(/[ \t]+/g, " ")
14
+ .replace(/\n{3,}/g, "\n\n")
15
+ .trim();
16
+ const links = (selector) => Array.from(doc.querySelectorAll(selector)).map((a) => ({ name: text(a), url: a.href }));
17
+ const rows = (selector) => Array.from(doc.querySelectorAll(selector)).map((row) => ({ label: text(row.querySelector("th")), value: text(row.querySelector("td")) }));
18
+ const stateCell = Array.from(doc.querySelectorAll(".submissionstatustable td")).find((td) => /\bsubmissionstatus[a-z]+\b/.test(td.className));
19
+ const actions = Array.from(doc.querySelectorAll('form input[name="action"]')).map((input) => input.value);
20
+ return {
21
+ description: text(doc.querySelector(".activity-description, #intro")),
22
+ descriptionFiles: links('.activity-description a[href*="pluginfile.php"], #intro a[href*="pluginfile.php"]'),
23
+ dates: Array.from(doc.querySelectorAll('[data-region="activity-dates"] .date-item')).map((item) => {
24
+ const label = text(item.querySelector(".date-item-label"));
25
+ return { label: label.replace(/:$/, ""), value: text(item).slice(label.length).trim() };
26
+ }),
27
+ status: rows(".submissionstatustable tr"),
28
+ submissionState: stateCell ? /\bsubmissionstatus([a-z]+)\b/.exec(stateCell.className)?.[1] : undefined,
29
+ graded: doc.querySelector(".submissionstatustable td.submissiongraded") !== null,
30
+ submittedFiles: links('.submissionstatustable a[href*="pluginfile.php"]'),
31
+ feedback: rows(".feedback tr"),
32
+ canEdit: actions.includes("editsubmission"),
33
+ canSubmit: actions.includes("submit"),
34
+ };
35
+ }, null);
36
+ }
37
+ export async function studentAssignment(context, siteUrl, cmid) {
38
+ const page = await openSitePage(context, siteUrl, `mod/assign/view.php?id=${cmid}`);
39
+ try {
40
+ if ((await page.locator("body#page-mod-assign-view").count()) === 0) {
41
+ throw new SubmissionError(`Activity ${cmid} isn't an assignment you can open. Take the id of an assignment (module "assign") from the course structure.`);
42
+ }
43
+ return await readStudentAssignment(page);
44
+ }
45
+ finally {
46
+ await page.close().catch(() => undefined);
47
+ }
48
+ }
49
+ /** Saves the submission's online text and adds files, as "Save changes" on the edit page does; gives the assignment afterwards. */
50
+ export async function saveSubmission(context, siteUrl, cmid, input) {
51
+ if (input.onlineText === undefined && (!input.files || input.files.length === 0))
52
+ throw new SubmissionError("Give online text, files or both.");
53
+ if (input.files)
54
+ await checkLocalFiles(input.files);
55
+ const page = await openForm(context, siteUrl, `mod/assign/view.php?id=${cmid}&action=editsubmission`).catch((error) => {
56
+ if (error instanceof NotLoggedInError)
57
+ throw error;
58
+ throw new SubmissionError(`You can't edit a submission to assignment ${cmid} now (submitted already, past its cut-off date, or not yours to submit).`);
59
+ });
60
+ try {
61
+ if (input.files && input.files.length > 0) {
62
+ const draft = await formValue(page, "files_filemanager");
63
+ if (!draft)
64
+ throw new SubmissionError(`Assignment ${cmid} doesn't take file submissions.`);
65
+ await uploadToDraft(page, draft, input.files);
66
+ }
67
+ const values = {};
68
+ if (input.onlineText !== undefined) {
69
+ if ((await formValue(page, "onlinetext_editor[format]")) === undefined)
70
+ throw new SubmissionError(`Assignment ${cmid} doesn't take online text.`);
71
+ values["onlinetext_editor[text]"] = input.onlineText;
72
+ }
73
+ await setFields(page, values);
74
+ await submitForm(page, "submitbutton");
75
+ }
76
+ finally {
77
+ await page.close().catch(() => undefined);
78
+ }
79
+ return studentAssignment(context, siteUrl, cmid);
80
+ }
81
+ /**
82
+ * Submits the saved submission for grading, accepting the site's statement when it asks for one
83
+ * (`acceptStatement`, once the person agrees; a bare boolean as before 0.2 too); gives the
84
+ * assignment afterwards.
85
+ */
86
+ export async function submitForGrading(context, siteUrl, cmid, options = {}) {
87
+ const acceptStatement = typeof options === "boolean" ? options : (options.acceptStatement ?? false);
88
+ const page = await openForm(context, siteUrl, `mod/assign/view.php?id=${cmid}&action=submit`).catch((error) => {
89
+ if (error instanceof NotLoggedInError)
90
+ throw error;
91
+ throw new SubmissionError(`Assignment ${cmid} has nothing to submit now (no draft, submitted already, or it doesn't ask students to click submit).`);
92
+ });
93
+ try {
94
+ const statement = page.locator('form.mform [name="submissionstatement"][type="checkbox"]');
95
+ if ((await statement.count()) > 0) {
96
+ if (!acceptStatement) {
97
+ const text = await statement.first().evaluate((box) => box.closest(".form-check, .fitem")?.textContent ?? "");
98
+ throw new SubmissionError(`Submitting asks to accept this statement; call again with acceptStatement once the person agrees: ${text.replace(/\s+/g, " ").trim()}`);
99
+ }
100
+ await setFields(page, { submissionstatement: true });
101
+ }
102
+ await submitForm(page, "submitbutton");
103
+ }
104
+ finally {
105
+ await page.close().catch(() => undefined);
106
+ }
107
+ return studentAssignment(context, siteUrl, cmid);
108
+ }
109
+ //# sourceMappingURL=submission.js.map