@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,57 @@
1
+ import type { BrowserContext, Page } from "playwright-core";
2
+ import { MoodleError } from "../../core/errors.js";
3
+ /** The assignment isn't there, or the person can't do that now. */
4
+ export declare class SubmissionError extends MoodleError {
5
+ name: string;
6
+ constructor(message: string);
7
+ }
8
+ /** An assignment as the student's page shows it. */
9
+ export interface StudentAssignment {
10
+ description: string;
11
+ descriptionFiles: {
12
+ name: string;
13
+ url: string;
14
+ }[];
15
+ dates: {
16
+ label: string;
17
+ value: string;
18
+ }[];
19
+ status: {
20
+ label: string;
21
+ value: string;
22
+ }[];
23
+ submissionState?: string;
24
+ graded: boolean;
25
+ submittedFiles: {
26
+ name: string;
27
+ url: string;
28
+ }[];
29
+ feedback: {
30
+ label: string;
31
+ value: string;
32
+ }[];
33
+ canEdit: boolean;
34
+ canSubmit: boolean;
35
+ }
36
+ export declare function readStudentAssignment(page: Page): Promise<StudentAssignment>;
37
+ /** An assignment as the student sees it: its description and files, dates, the submission's status and files, the feedback, and whether it can be edited or submitted now. */
38
+ export declare function studentAssignment(context: BrowserContext, siteUrl: string, activityId: number): Promise<StudentAssignment>;
39
+ /** Saves the submission's online text and adds files, as "Save changes" on the edit page does; gives the assignment afterwards. */
40
+ export declare function saveSubmission(context: BrowserContext, siteUrl: string, activityId: number, input: {
41
+ onlineText?: string;
42
+ files?: string[];
43
+ }): Promise<{
44
+ changed: string;
45
+ assignment: StudentAssignment;
46
+ }>;
47
+ /**
48
+ * Submits the saved submission for grading, accepting the site's statement when it asks for one
49
+ * (`acceptStatement`, once the person agrees; a bare boolean as before 0.2 too); gives the
50
+ * assignment afterwards.
51
+ */
52
+ export declare function submitForGrading(context: BrowserContext, siteUrl: string, activityId: number, options?: {
53
+ acceptStatement?: boolean;
54
+ } | boolean): Promise<{
55
+ changed: string;
56
+ assignment: StudentAssignment;
57
+ }>;
@@ -0,0 +1,115 @@
1
+ import { checkLocalFiles } from "../../core/files.js";
2
+ import { formValue, openForm, setFields, submitForm, uploadToDraft } from "../../core/forms.js";
3
+ import { inPage } from "../../core/inPage.js";
4
+ import { NotLoggedInError, openSitePage } from "../../core/session.js";
5
+ import { MoodleError } from "../../core/errors.js";
6
+ /** The assignment isn't there, or the person can't do that now. */
7
+ export class SubmissionError extends MoodleError {
8
+ name = "SubmissionError";
9
+ constructor(message) {
10
+ super(message, "submission");
11
+ }
12
+ }
13
+ export function readStudentAssignment(page) {
14
+ return inPage(page, () => {
15
+ const doc = globalThis.document;
16
+ const text = (el) => (el?.innerText ?? "")
17
+ .replace(/[ \t]+/g, " ")
18
+ .replace(/\n{3,}/g, "\n\n")
19
+ .trim();
20
+ const links = (selector) => Array.from(doc.querySelectorAll(selector)).map((a) => ({ name: text(a), url: a.href }));
21
+ const rows = (selector) => Array.from(doc.querySelectorAll(selector)).map((row) => ({ label: text(row.querySelector("th")), value: text(row.querySelector("td")) }));
22
+ const stateCell = Array.from(doc.querySelectorAll(".submissionstatustable td")).find((td) => /\bsubmissionstatus[a-z]+\b/.test(td.className));
23
+ const actions = Array.from(doc.querySelectorAll('form input[name="action"]')).map((input) => input.value);
24
+ return {
25
+ description: text(doc.querySelector(".activity-description, #intro")),
26
+ descriptionFiles: links('.activity-description a[href*="pluginfile.php"], #intro a[href*="pluginfile.php"]'),
27
+ dates: Array.from(doc.querySelectorAll('[data-region="activity-dates"] .date-item')).map((item) => {
28
+ const label = text(item.querySelector(".date-item-label"));
29
+ return { label: label.replace(/:$/, ""), value: text(item).slice(label.length).trim() };
30
+ }),
31
+ status: rows(".submissionstatustable tr"),
32
+ submissionState: stateCell ? /\bsubmissionstatus([a-z]+)\b/.exec(stateCell.className)?.[1] : undefined,
33
+ graded: doc.querySelector(".submissionstatustable td.submissiongraded") !== null,
34
+ submittedFiles: links('.submissionstatustable a[href*="pluginfile.php"]'),
35
+ feedback: rows(".feedback tr"),
36
+ canEdit: actions.includes("editsubmission"),
37
+ canSubmit: actions.includes("submit"),
38
+ };
39
+ }, null);
40
+ }
41
+ /** An assignment as the student sees it: its description and files, dates, the submission's status and files, the feedback, and whether it can be edited or submitted now. */
42
+ export async function studentAssignment(context, siteUrl, activityId) {
43
+ const page = await openSitePage(context, siteUrl, `mod/assign/view.php?id=${activityId}`);
44
+ try {
45
+ if ((await page.locator("body#page-mod-assign-view").count()) === 0) {
46
+ throw new SubmissionError(`Activity ${activityId} isn't an assignment you can open. Take the id of an assignment (module "assign") from the course structure.`);
47
+ }
48
+ return await readStudentAssignment(page);
49
+ }
50
+ finally {
51
+ await page.close().catch(() => undefined);
52
+ }
53
+ }
54
+ /** Saves the submission's online text and adds files, as "Save changes" on the edit page does; gives the assignment afterwards. */
55
+ export async function saveSubmission(context, siteUrl, activityId, input) {
56
+ if (input.onlineText === undefined && (!input.files || input.files.length === 0))
57
+ throw new SubmissionError("Give online text, files or both.");
58
+ if (input.files)
59
+ await checkLocalFiles(input.files);
60
+ const page = await openForm(context, siteUrl, `mod/assign/view.php?id=${activityId}&action=editsubmission`).catch((error) => {
61
+ if (error instanceof NotLoggedInError)
62
+ throw error;
63
+ throw new SubmissionError(`You can't edit a submission to assignment ${activityId} now (submitted already, past its cut-off date, or not yours to submit).`);
64
+ });
65
+ try {
66
+ if (input.files && input.files.length > 0) {
67
+ const draft = await formValue(page, "files_filemanager");
68
+ if (!draft)
69
+ throw new SubmissionError(`Assignment ${activityId} doesn't take file submissions.`);
70
+ await uploadToDraft(page, draft, input.files);
71
+ }
72
+ const values = {};
73
+ if (input.onlineText !== undefined) {
74
+ if ((await formValue(page, "onlinetext_editor[format]")) === undefined)
75
+ throw new SubmissionError(`Assignment ${activityId} doesn't take online text.`);
76
+ values["onlinetext_editor[text]"] = input.onlineText;
77
+ }
78
+ await setFields(page, values);
79
+ await submitForm(page, "submitbutton");
80
+ }
81
+ finally {
82
+ await page.close().catch(() => undefined);
83
+ }
84
+ const saved = [input.onlineText !== undefined ? "online text" : "", input.files?.length ? `${input.files.length} file${input.files.length === 1 ? "" : "s"}` : ""].filter(Boolean).join(" and ");
85
+ return { changed: `Saved ${saved} in assignment ${activityId}'s submission.`, assignment: await studentAssignment(context, siteUrl, activityId) };
86
+ }
87
+ /**
88
+ * Submits the saved submission for grading, accepting the site's statement when it asks for one
89
+ * (`acceptStatement`, once the person agrees; a bare boolean as before 0.2 too); gives the
90
+ * assignment afterwards.
91
+ */
92
+ export async function submitForGrading(context, siteUrl, activityId, options = {}) {
93
+ const acceptStatement = typeof options === "boolean" ? options : (options.acceptStatement ?? false);
94
+ const page = await openForm(context, siteUrl, `mod/assign/view.php?id=${activityId}&action=submit`).catch((error) => {
95
+ if (error instanceof NotLoggedInError)
96
+ throw error;
97
+ throw new SubmissionError(`Assignment ${activityId} has nothing to submit now (no draft, submitted already, or it doesn't ask students to click submit).`);
98
+ });
99
+ try {
100
+ const statement = page.locator('form.mform [name="submissionstatement"][type="checkbox"]');
101
+ if ((await statement.count()) > 0) {
102
+ if (!acceptStatement) {
103
+ const text = await statement.first().evaluate((box) => box.closest(".form-check, .fitem")?.textContent ?? "");
104
+ throw new SubmissionError(`Submitting asks to accept this statement; call again with acceptStatement once the person agrees: ${text.replace(/\s+/g, " ").trim()}`);
105
+ }
106
+ await setFields(page, { submissionstatement: true });
107
+ }
108
+ await submitForm(page, "submitbutton");
109
+ }
110
+ finally {
111
+ await page.close().catch(() => undefined);
112
+ }
113
+ return { changed: `Submitted assignment ${activityId} for grading.`, assignment: await studentAssignment(context, siteUrl, activityId) };
114
+ }
115
+ //# sourceMappingURL=student.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"student.js","sourceRoot":"","sources":["../../../src/areas/assignments/student.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import type { BrowserContext } from "playwright-core";
2
+ import { MoodleError } from "../../core/errors.js";
3
+ /** Moodle refused the event, or it isn't there. */
4
+ export declare class CalendarError extends MoodleError {
5
+ name: string;
6
+ constructor(message: string);
7
+ }
8
+ export interface CalendarEvent {
9
+ id: number;
10
+ name: string;
11
+ /** The event's kind: "course", "group", "user", or an activity's ("due", "open"…). */
12
+ type: string;
13
+ start: string;
14
+ durationMinutes?: number;
15
+ courseId?: number;
16
+ /** For an activity's event, its module and instance. */
17
+ activity?: {
18
+ type: string;
19
+ instance: number;
20
+ };
21
+ }
22
+ /** The person's upcoming events, as the calendar's "Upcoming events" view lists them, in one course or all. */
23
+ export declare function upcomingEvents(context: BrowserContext, siteUrl: string, { courseId }?: {
24
+ courseId?: number;
25
+ }): Promise<CalendarEvent[]>;
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 declare function createEvent(context: BrowserContext, siteUrl: string, { courseId, name, start, durationMinutes, description }: {
31
+ courseId: number;
32
+ name: string;
33
+ start: Date;
34
+ durationMinutes?: number;
35
+ description?: string;
36
+ }): Promise<{
37
+ changed: string;
38
+ event: CalendarEvent;
39
+ }>;
40
+ /** Deletes a calendar event the person can manage. */
41
+ export declare function deleteEvent(context: BrowserContext, siteUrl: string, eventId: number): Promise<{
42
+ changed: string;
43
+ }>;
@@ -0,0 +1,84 @@
1
+ import { MoodleServiceError, callService } from "../../core/ajax.js";
2
+ import { isoDate } from "../courses/list.js";
3
+ import { setFields } from "../../core/forms.js";
4
+ import { decodeEntities } from "../../core/text.js";
5
+ import { MoodleError } from "../../core/errors.js";
6
+ /** Moodle refused the event, or it isn't there. */
7
+ export class CalendarError extends MoodleError {
8
+ name = "CalendarError";
9
+ constructor(message) {
10
+ super(message, "calendar");
11
+ }
12
+ }
13
+ function toEvent(raw) {
14
+ return {
15
+ id: raw.id,
16
+ name: decodeEntities(raw.name),
17
+ type: raw.eventtype,
18
+ start: isoDate(raw.timestart),
19
+ ...(raw.timeduration > 0 ? { durationMinutes: Math.round(raw.timeduration / 60) } : {}),
20
+ ...(raw.course ? { courseId: raw.course.id } : {}),
21
+ ...(raw.modulename && raw.instance ? { activity: { type: raw.modulename, instance: raw.instance } } : {}),
22
+ };
23
+ }
24
+ /** The person's upcoming events, as the calendar's "Upcoming events" view lists them, in one course or all. */
25
+ export async function upcomingEvents(context, siteUrl, { courseId } = {}) {
26
+ // Course 1 is the site: every course the person is in.
27
+ const { events } = await callService(context, siteUrl, "core_calendar_get_calendar_upcoming_view", { courseid: courseId ?? 1, categoryid: 0 });
28
+ return events.map(toEvent);
29
+ }
30
+ /**
31
+ * Creates a course event, through the calendar's own form: its start, and its duration and
32
+ * description when given. Returns it as the calendar has it.
33
+ */
34
+ export async function createEvent(context, siteUrl, { courseId, name, start, durationMinutes, description }) {
35
+ // The form takes its start as the time it's opened for: no date fields to fill in the person's time zone.
36
+ const fragment = await callService(context, siteUrl, "core_get_fragment", {
37
+ component: "core_calendar",
38
+ callback: "event_form",
39
+ contextid: 1,
40
+ args: [
41
+ { name: "courseid", value: String(courseId) },
42
+ { name: "starttime", value: String(Math.floor(start.getTime() / 1000)) },
43
+ ],
44
+ });
45
+ const page = await context.newPage();
46
+ let formdata;
47
+ try {
48
+ await page.setContent(fragment.html);
49
+ const types = await page.evaluate(() => Array.from(globalThis.document.querySelectorAll('form [name="eventtype"] option')).map((o) => o.value));
50
+ if (!types.includes("course"))
51
+ throw new CalendarError(`You can't add events to course ${courseId}'s calendar.`);
52
+ const values = { name, eventtype: "course", courseid: String(courseId) };
53
+ if (durationMinutes !== undefined)
54
+ Object.assign(values, { duration: 2, timedurationminutes: durationMinutes });
55
+ if (description !== undefined)
56
+ values["description[text]"] = description;
57
+ await setFields(page, values, "form");
58
+ formdata = await page.evaluate(() => {
59
+ const g = globalThis;
60
+ return new g.URLSearchParams(new g.FormData(g.document.querySelector("form"))).toString();
61
+ });
62
+ }
63
+ finally {
64
+ await page.close().catch(() => undefined);
65
+ }
66
+ const answer = await callService(context, siteUrl, "core_calendar_submit_create_update_form", { formdata });
67
+ if (!answer.event || answer.validationerror)
68
+ throw new CalendarError(`Moodle refused the event "${name}": check its name, start and duration.`);
69
+ const event = toEvent(answer.event);
70
+ return { changed: `Event ${event.id} "${event.name}" added to course ${courseId}'s calendar.`, event };
71
+ }
72
+ /** Deletes a calendar event the person can manage. */
73
+ export async function deleteEvent(context, siteUrl, eventId) {
74
+ try {
75
+ await callService(context, siteUrl, "core_calendar_delete_calendar_events", { events: [{ eventid: eventId, repeat: false }] });
76
+ }
77
+ catch (error) {
78
+ if (error instanceof MoodleServiceError && /^invalidrecord/.test(error.errorcode))
79
+ throw new CalendarError(`There's no calendar event ${eventId}: it was deleted, or never existed.`);
80
+ throw error;
81
+ }
82
+ return { changed: `Event ${eventId} deleted.` };
83
+ }
84
+ //# sourceMappingURL=calendar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar.js","sourceRoot":"","sources":["../../../src/areas/calendar/calendar.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import type { BrowserContext, Page } from "playwright-core";
2
+ import { type CompletionState } from "../courses/structure.js";
3
+ import { MoodleError } from "../../core/errors.js";
4
+ /** Completion can't be changed or read there: not enabled, automatic, or not allowed. */
5
+ export declare class CompletionError extends MoodleError {
6
+ name: string;
7
+ constructor(message: string);
8
+ }
9
+ /**
10
+ * Marks the person's own completion of an activity with manual completion, done or not, as its
11
+ * "Mark as done" button does; doing it twice changes nothing. An activity that completes on its own
12
+ * gives a `CompletionError`.
13
+ */
14
+ export declare function setCompletion(context: BrowserContext, siteUrl: string, activityId: number, done: boolean): Promise<{
15
+ changed: string;
16
+ }>;
17
+ /** Sets someone's completion of an activity as a teacher does from the progress report; returns the state saved. */
18
+ export declare function overrideCompletion(context: BrowserContext, siteUrl: string, activityId: number, userId: number, state: CompletionState): Promise<{
19
+ changed: string;
20
+ state: CompletionState;
21
+ overridden: boolean;
22
+ }>;
23
+ export interface CompletionCell {
24
+ activityId: number;
25
+ state: CompletionState;
26
+ tracking: "manual" | "auto";
27
+ /** A teacher set it, not the person or Moodle. */
28
+ overridden: boolean;
29
+ }
30
+ export interface CompletionReport {
31
+ activities: {
32
+ id: number;
33
+ name: string;
34
+ type: string;
35
+ }[];
36
+ participants: {
37
+ userId: number;
38
+ fullName: string;
39
+ completion: CompletionCell[];
40
+ }[];
41
+ }
42
+ /** One page of the progress report: its activities from the header links, its rows' icons by name. */
43
+ export declare function readCompletionReport(page: Page): Promise<CompletionReport>;
44
+ /**
45
+ * A course's progress report: every participant's completion of every activity that tracks it, page
46
+ * by page, in one group when given. People come as user id and full name only.
47
+ */
48
+ export declare function completionReport(context: BrowserContext, siteUrl: string, courseId: number, { group }?: {
49
+ group?: number;
50
+ }): Promise<CompletionReport>;
@@ -0,0 +1,112 @@
1
+ import { MoodleServiceError, callService } from "../../core/ajax.js";
2
+ import { openSitePage } from "../../core/session.js";
3
+ import { COMPLETION_STATES } from "../courses/structure.js";
4
+ import { MoodleError } from "../../core/errors.js";
5
+ /** Completion can't be changed or read there: not enabled, automatic, or not allowed. */
6
+ export class CompletionError extends MoodleError {
7
+ name = "CompletionError";
8
+ constructor(message) {
9
+ super(message, "completion");
10
+ }
11
+ }
12
+ /** Moodle's refusals, in its terms. */
13
+ function completionError(error, activityId) {
14
+ if (!(error instanceof MoodleServiceError))
15
+ return error;
16
+ if (error.errorcode === "cannotmanualctrack")
17
+ return new CompletionError(`Activity ${activityId} completes automatically (on view, grade, submission…): it can't be marked by hand.`);
18
+ if (error.errorcode === "completionnotenabled")
19
+ return new CompletionError(`Completion isn't enabled in activity ${activityId}'s course.`);
20
+ if (error.errorcode === "nopermissions" || error.errorcode === "nopermission")
21
+ return new CompletionError(`You aren't allowed to change completion in activity ${activityId}.`);
22
+ return error;
23
+ }
24
+ /**
25
+ * Marks the person's own completion of an activity with manual completion, done or not, as its
26
+ * "Mark as done" button does; doing it twice changes nothing. An activity that completes on its own
27
+ * gives a `CompletionError`.
28
+ */
29
+ export async function setCompletion(context, siteUrl, activityId, done) {
30
+ try {
31
+ await callService(context, siteUrl, "core_completion_update_activity_completion_status_manually", { cmid: activityId, completed: done });
32
+ }
33
+ catch (error) {
34
+ throw completionError(error, activityId);
35
+ }
36
+ return { changed: `Activity ${activityId} marked ${done ? "done" : "not done"}.` };
37
+ }
38
+ /** Sets someone's completion of an activity as a teacher does from the progress report; returns the state saved. */
39
+ export async function overrideCompletion(context, siteUrl, activityId, userId, state) {
40
+ let saved;
41
+ try {
42
+ saved = await callService(context, siteUrl, "core_completion_override_activity_completion_status", { userid: userId, cmid: activityId, newstate: COMPLETION_STATES.indexOf(state) });
43
+ }
44
+ catch (error) {
45
+ throw completionError(error, activityId);
46
+ }
47
+ const now = COMPLETION_STATES[saved.state] ?? "incomplete";
48
+ return { changed: `User ${userId}'s completion of activity ${activityId} set to ${now}.`, state: now, overridden: Boolean(saved.overrideby) };
49
+ }
50
+ /** One page of the progress report: its activities from the header links, its rows' icons by name. */
51
+ export function readCompletionReport(page) {
52
+ return page.evaluate(() => {
53
+ const doc = globalThis.document;
54
+ const STATES = { n: "incomplete", y: "complete", pass: "complete-pass", fail: "complete-fail" };
55
+ const activities = Array.from(doc.querySelectorAll("#completion-progress thead th.completion-header a")).flatMap((link) => {
56
+ const match = /\/mod\/([a-z0-9_]+)\/view\.php\?(?:.*&)?id=(\d+)/.exec(link.getAttribute("href") ?? "");
57
+ return match ? [{ id: Number(match[2]), name: (link.getAttribute("title") ?? link.textContent ?? "").trim(), type: match[1] }] : [];
58
+ });
59
+ const participants = Array.from(doc.querySelectorAll("#completion-progress tbody tr")).flatMap((row) => {
60
+ const link = row.querySelector('th a[href*="/user/view.php?id="]');
61
+ const userId = Number(new URL(link?.getAttribute("href") ?? "", "http://x/").searchParams.get("id"));
62
+ if (!userId)
63
+ return [];
64
+ // One cell per activity, in the header's order; the icon's file names the state.
65
+ const completion = Array.from(row.querySelectorAll("td.completion-progresscell")).flatMap((cell, i) => {
66
+ const icon = /completion-(manual|auto)-(n|y|pass|fail)(-override)?/.exec(cell.querySelector("img")?.getAttribute("src") ?? "");
67
+ const activity = activities[i];
68
+ return icon && activity ? [{ activityId: activity.id, state: STATES[icon[2]], tracking: icon[1], overridden: icon[3] !== undefined }] : [];
69
+ });
70
+ return [{ userId, fullName: (link?.textContent ?? "").replace(/\s+/g, " ").trim(), completion }];
71
+ });
72
+ return { activities, participants };
73
+ });
74
+ }
75
+ const MAX_PAGES = 100;
76
+ /**
77
+ * A course's progress report: every participant's completion of every activity that tracks it, page
78
+ * by page, in one group when given. People come as user id and full name only.
79
+ */
80
+ export async function completionReport(context, siteUrl, courseId, { group } = {}) {
81
+ const report = { activities: [], participants: [] };
82
+ const seen = new Set();
83
+ let start = 0;
84
+ for (let p = 0; p < MAX_PAGES; p++) {
85
+ const page = await openSitePage(context, siteUrl, `report/progress/index.php?course=${courseId}&start=${start}${group ? `&group=${group}` : ""}`);
86
+ try {
87
+ // An error page keeps the report's body id; only the report has its activity selector.
88
+ if ((await page.locator("body#page-report-progress-index #include-activity-select-report").count()) === 0) {
89
+ throw new CompletionError(`You can't see course ${courseId}'s progress report: the course doesn't exist, or you aren't allowed.`);
90
+ }
91
+ if ((await page.locator("#completion-progress").count()) === 0) {
92
+ // The page explains in the site's language: no activity tracks completion, or nobody to list.
93
+ return report;
94
+ }
95
+ const read = await readCompletionReport(page);
96
+ if (p === 0)
97
+ report.activities = read.activities;
98
+ const fresh = read.participants.filter((row) => !seen.has(row.userId));
99
+ if (fresh.length === 0)
100
+ break;
101
+ for (const row of fresh)
102
+ seen.add(row.userId);
103
+ report.participants.push(...fresh);
104
+ start += read.participants.length;
105
+ }
106
+ finally {
107
+ await page.close().catch(() => undefined);
108
+ }
109
+ }
110
+ return report;
111
+ }
112
+ //# sourceMappingURL=completion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completion.js","sourceRoot":"","sources":["../../../src/areas/completion/completion.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ import type { BrowserContext } from "playwright-core";
2
+ import { type FieldValue } from "../../core/forms.js";
3
+ import { MoodleError } from "../../core/errors.js";
4
+ /** Which courses by their dates; "all" includes those the person hid from their dashboard. */
5
+ export declare const COURSE_CLASSIFICATIONS: readonly ["all", "inprogress", "future", "past"];
6
+ export type CourseClassification = (typeof COURSE_CLASSIFICATIONS)[number];
7
+ export interface Course {
8
+ id: number;
9
+ shortname: string;
10
+ fullname: string;
11
+ category?: string;
12
+ url: string;
13
+ /** Whether students can see the course. */
14
+ visible: boolean;
15
+ startDate?: string;
16
+ endDate?: string;
17
+ /** The person's completion percentage, when the course tracks it. */
18
+ progress?: number;
19
+ /** When the person last opened the course. */
20
+ lastAccess?: string;
21
+ }
22
+ /** A course as `core_course_get_enrolled_courses_by_timeline_classification` returns it (Moodle 5.2). */
23
+ export interface MoodleCourse {
24
+ id: number;
25
+ shortname: string;
26
+ fullname: string;
27
+ coursecategory?: string;
28
+ viewurl: string;
29
+ visible: boolean;
30
+ startdate?: number;
31
+ enddate?: number;
32
+ progress?: number | null;
33
+ hasprogress?: boolean;
34
+ timeaccess?: number | null;
35
+ }
36
+ /** Unix seconds as ISO 8601; Moodle uses 0 for "none". */
37
+ export declare function isoDate(seconds: number | null | undefined): string | undefined;
38
+ export declare function toCourse(raw: MoodleCourse): Course;
39
+ /** The courses the person is enrolled in, as teacher or student. */
40
+ export declare function listCourses(context: BrowserContext, siteUrl: string, classification?: CourseClassification): Promise<Course[]>;
41
+ /** A course that isn't there, or the person can't change. */
42
+ export declare class CourseError extends MoodleError {
43
+ name: string;
44
+ constructor(message: string);
45
+ }
46
+ /**
47
+ * Changes a course's full name, short name, summary (HTML) or any other setting by its form field
48
+ * name, through the course's settings form; returns the course as `listCourses` gives it. Moodle's
49
+ * refusals (a short name already taken) come back as `FormError` by field.
50
+ */
51
+ export declare function updateCourse(context: BrowserContext, siteUrl: string, courseId: number, { fullName, shortName, summary, settings }: {
52
+ fullName?: string;
53
+ shortName?: string;
54
+ summary?: string;
55
+ settings?: Record<string, FieldValue>;
56
+ }): Promise<{
57
+ changed: string;
58
+ course?: Course;
59
+ }>;
@@ -0,0 +1,74 @@
1
+ import { callService } from "../../core/ajax.js";
2
+ import { openForm, setFields, submitForm } from "../../core/forms.js";
3
+ import { NotLoggedInError } from "../../core/session.js";
4
+ import { decodeEntities } from "../../core/text.js";
5
+ import { MoodleError } from "../../core/errors.js";
6
+ /** Which courses by their dates; "all" includes those the person hid from their dashboard. */
7
+ export const COURSE_CLASSIFICATIONS = ["all", "inprogress", "future", "past"];
8
+ const MOODLE_CLASSIFICATIONS = { all: "allincludinghidden", inprogress: "inprogress", future: "future", past: "past" };
9
+ /** Unix seconds as ISO 8601; Moodle uses 0 for "none". */
10
+ export function isoDate(seconds) {
11
+ return seconds ? new Date(seconds * 1000).toISOString() : undefined;
12
+ }
13
+ export function toCourse(raw) {
14
+ return {
15
+ id: raw.id,
16
+ shortname: decodeEntities(raw.shortname),
17
+ fullname: decodeEntities(raw.fullname),
18
+ category: raw.coursecategory ? decodeEntities(raw.coursecategory) : undefined,
19
+ url: raw.viewurl,
20
+ visible: raw.visible,
21
+ startDate: isoDate(raw.startdate),
22
+ endDate: isoDate(raw.enddate),
23
+ progress: raw.hasprogress && typeof raw.progress === "number" ? raw.progress : undefined,
24
+ lastAccess: isoDate(raw.timeaccess),
25
+ };
26
+ }
27
+ /** The courses the person is enrolled in, as teacher or student. */
28
+ export async function listCourses(context, siteUrl, classification = "all") {
29
+ const { courses } = await callService(context, siteUrl, "core_course_get_enrolled_courses_by_timeline_classification", {
30
+ classification: MOODLE_CLASSIFICATIONS[classification],
31
+ limit: 0,
32
+ offset: 0,
33
+ sort: "fullname",
34
+ });
35
+ return courses.map(toCourse);
36
+ }
37
+ /** A course that isn't there, or the person can't change. */
38
+ export class CourseError extends MoodleError {
39
+ name = "CourseError";
40
+ constructor(message) {
41
+ super(message, "course");
42
+ }
43
+ }
44
+ /**
45
+ * Changes a course's full name, short name, summary (HTML) or any other setting by its form field
46
+ * name, through the course's settings form; returns the course as `listCourses` gives it. Moodle's
47
+ * refusals (a short name already taken) come back as `FormError` by field.
48
+ */
49
+ export async function updateCourse(context, siteUrl, courseId, { fullName, shortName, summary, settings }) {
50
+ const values = { ...settings };
51
+ if (fullName !== undefined)
52
+ values.fullname = fullName;
53
+ if (shortName !== undefined)
54
+ values.shortname = shortName;
55
+ if (summary !== undefined)
56
+ values["summary_editor[text]"] = summary;
57
+ if (Object.keys(values).length === 0)
58
+ return { changed: "Nothing to change.", course: (await listCourses(context, siteUrl)).find((c) => c.id === courseId) };
59
+ const page = await openForm(context, siteUrl, `course/edit.php?id=${courseId}`).catch((error) => {
60
+ if (error instanceof NotLoggedInError)
61
+ throw error;
62
+ throw new CourseError(`You can't change course ${courseId}'s settings: it doesn't exist, or you aren't allowed.`);
63
+ });
64
+ try {
65
+ await setFields(page, values);
66
+ await submitForm(page, "saveanddisplay");
67
+ }
68
+ finally {
69
+ await page.close().catch(() => undefined);
70
+ }
71
+ const course = (await listCourses(context, siteUrl)).find((c) => c.id === courseId);
72
+ return { changed: `Course ${courseId} updated (${Object.keys(values).join(", ")}).`, course };
73
+ }
74
+ //# sourceMappingURL=list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.js","sourceRoot":"","sources":["../../../src/areas/courses/list.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAmB,MAAM,qBAAqB,CAAC;AACvF,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,8FAA8F;AAC9F,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,CAAU,CAAC;AAGvF,MAAM,sBAAsB,GAAyC,EAAE,GAAG,EAAE,oBAAoB,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAiC7J,0DAA0D;AAC1D,MAAM,UAAU,OAAO,CAAC,OAAkC;IACxD,OAAO,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AACtE,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAiB;IACxC,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,SAAS,EAAE,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC;QACxC,QAAQ,EAAE,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC;QACtC,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,SAAS;QAC7E,GAAG,EAAE,GAAG,CAAC,OAAO;QAChB,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;QACjC,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;QAC7B,QAAQ,EAAE,GAAG,CAAC,WAAW,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;QACxF,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;KACpC,CAAC;AACJ,CAAC;AAED,oEAAoE;AACpE,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAuB,EAAE,OAAe,EAAE,iBAAuC,KAAK;IACtH,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,WAAW,CAA8B,OAAO,EAAE,OAAO,EAAE,6DAA6D,EAAE;QAClJ,cAAc,EAAE,sBAAsB,CAAC,cAAc,CAAC;QACtD,KAAK,EAAE,CAAC;QACR,MAAM,EAAE,CAAC;QACT,IAAI,EAAE,UAAU;KACjB,CAAC,CAAC;IACH,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AAC/B,CAAC;AAED,6DAA6D;AAC7D,MAAM,OAAO,WAAY,SAAQ,WAAW;IACjC,IAAI,GAAG,aAAa,CAAC;IAC9B,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC3B,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAAuB,EACvB,OAAe,EACf,QAAgB,EAChB,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAsG;IAE9I,MAAM,MAAM,GAA+B,EAAE,GAAG,QAAQ,EAAE,CAAC;IAC3D,IAAI,QAAQ,KAAK,SAAS;QAAE,MAAM,CAAC,QAAQ,GAAG,QAAQ,CAAC;IACvD,IAAI,SAAS,KAAK,SAAS;QAAE,MAAM,CAAC,SAAS,GAAG,SAAS,CAAC;IAC1D,IAAI,OAAO,KAAK,SAAS;QAAE,MAAM,CAAC,sBAAsB,CAAC,GAAG,OAAO,CAAC;IACpE,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,CAAC,MAAM,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,EAAE,CAAC;IAC7J,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,sBAAsB,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;QACvG,IAAI,KAAK,YAAY,gBAAgB;YAAE,MAAM,KAAK,CAAC;QACnD,MAAM,IAAI,WAAW,CAAC,2BAA2B,QAAQ,uDAAuD,CAAC,CAAC;IACpH,CAAC,CAAC,CAAC;IACH,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC9B,MAAM,UAAU,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAC3C,CAAC;YAAS,CAAC;QACT,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAC5C,CAAC;IACD,MAAM,MAAM,GAAG,CAAC,MAAM,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC;IACpF,OAAO,EAAE,OAAO,EAAE,UAAU,QAAQ,aAAa,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;AAChG,CAAC"}