@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.
- package/LICENSE +21 -0
- package/README.md +131 -2
- package/dist/areas/activities/build.d.ts +98 -0
- package/dist/areas/activities/build.js +248 -0
- package/dist/areas/activities/build.js.map +1 -0
- package/dist/areas/activities/content.d.ts +53 -0
- package/dist/areas/activities/content.js +113 -0
- package/dist/areas/activities/content.js.map +1 -0
- package/dist/areas/assignments/grader.d.ts +68 -0
- package/dist/areas/assignments/grader.js +169 -0
- package/dist/areas/assignments/grader.js.map +1 -0
- package/dist/areas/assignments/grading.d.ts +86 -0
- package/dist/areas/assignments/grading.js +162 -0
- package/dist/areas/assignments/grading.js.map +1 -0
- package/dist/areas/assignments/student.d.ts +57 -0
- package/dist/areas/assignments/student.js +115 -0
- package/dist/areas/assignments/student.js.map +1 -0
- package/dist/areas/calendar/calendar.d.ts +43 -0
- package/dist/areas/calendar/calendar.js +84 -0
- package/dist/areas/calendar/calendar.js.map +1 -0
- package/dist/areas/completion/completion.d.ts +50 -0
- package/dist/areas/completion/completion.js +112 -0
- package/dist/areas/completion/completion.js.map +1 -0
- package/dist/areas/courses/list.d.ts +59 -0
- package/dist/areas/courses/list.js +74 -0
- package/dist/areas/courses/list.js.map +1 -0
- package/dist/areas/courses/overview.d.ts +83 -0
- package/dist/areas/courses/overview.js +137 -0
- package/dist/areas/courses/overview.js.map +1 -0
- package/dist/areas/courses/state.d.ts +51 -0
- package/dist/areas/courses/state.js +45 -0
- package/dist/areas/courses/state.js.map +1 -0
- package/dist/areas/courses/structure.d.ts +89 -0
- package/dist/areas/courses/structure.js +104 -0
- package/dist/areas/courses/structure.js.map +1 -0
- package/dist/areas/forums/discussions.d.ts +57 -0
- package/dist/areas/forums/discussions.js +119 -0
- package/dist/areas/forums/discussions.js.map +1 -0
- package/dist/areas/forums/posts.d.ts +83 -0
- package/dist/areas/forums/posts.js +114 -0
- package/dist/areas/forums/posts.js.map +1 -0
- package/dist/areas/grades/gradebook.d.ts +104 -0
- package/dist/areas/grades/gradebook.js +250 -0
- package/dist/areas/grades/gradebook.js.map +1 -0
- package/dist/areas/grades/user-report.d.ts +26 -0
- package/dist/areas/grades/user-report.js +71 -0
- package/dist/areas/grades/user-report.js.map +1 -0
- package/dist/areas/h5p/h5p.d.ts +33 -0
- package/dist/areas/h5p/h5p.js +104 -0
- package/dist/areas/h5p/h5p.js.map +1 -0
- package/dist/areas/lessons/lessons.d.ts +60 -0
- package/dist/areas/lessons/lessons.js +145 -0
- package/dist/areas/lessons/lessons.js.map +1 -0
- package/dist/areas/messages/messages.d.ts +68 -0
- package/dist/areas/messages/messages.js +84 -0
- package/dist/areas/messages/messages.js.map +1 -0
- package/dist/areas/participants/participants.d.ts +57 -0
- package/dist/areas/participants/participants.js +135 -0
- package/dist/areas/participants/participants.js.map +1 -0
- package/dist/areas/questions/banks.d.ts +83 -0
- package/dist/areas/questions/banks.js +239 -0
- package/dist/areas/questions/banks.js.map +1 -0
- package/dist/areas/quizzes/attempts.d.ts +123 -0
- package/dist/areas/quizzes/attempts.js +300 -0
- package/dist/areas/quizzes/attempts.js.map +1 -0
- package/dist/areas/quizzes/results.d.ts +36 -0
- package/dist/areas/quizzes/results.js +93 -0
- package/dist/areas/quizzes/results.js.map +1 -0
- package/dist/areas/quizzes/structure.d.ts +42 -0
- package/dist/areas/quizzes/structure.js +100 -0
- package/dist/areas/quizzes/structure.js.map +1 -0
- package/dist/areas/scorm/scorm.d.ts +60 -0
- package/dist/areas/scorm/scorm.js +171 -0
- package/dist/areas/scorm/scorm.js.map +1 -0
- package/dist/areas/site/info.d.ts +25 -0
- package/dist/areas/site/info.js +45 -0
- package/dist/areas/site/info.js.map +1 -0
- package/dist/areas/site/users.d.ts +56 -0
- package/dist/areas/site/users.js +89 -0
- package/dist/areas/site/users.js.map +1 -0
- package/dist/areas/surveys/choice.d.ts +38 -0
- package/dist/areas/surveys/choice.js +105 -0
- package/dist/areas/surveys/choice.js.map +1 -0
- package/dist/areas/surveys/surveys.d.ts +51 -0
- package/dist/areas/surveys/surveys.js +201 -0
- package/dist/areas/surveys/surveys.js.map +1 -0
- package/dist/browser/chromium.d.ts +15 -0
- package/dist/browser/chromium.js +53 -0
- package/dist/browser/chromium.js.map +1 -0
- package/dist/browser/cookies.d.ts +11 -0
- package/dist/browser/cookies.js +31 -0
- package/dist/browser/cookies.js.map +1 -0
- package/dist/browser/manager.d.ts +57 -0
- package/dist/browser/manager.js +177 -0
- package/dist/browser/manager.js.map +1 -0
- package/dist/browser.d.ts +53 -0
- package/dist/browser.js +167 -0
- package/dist/browser.js.map +1 -0
- package/dist/categories.d.ts +28 -0
- package/dist/categories.js +225 -0
- package/dist/categories.js.map +1 -0
- package/dist/chromiumInstaller.d.ts +15 -0
- package/dist/chromiumInstaller.js +52 -0
- package/dist/chromiumInstaller.js.map +1 -0
- package/dist/cli/args.d.ts +33 -0
- package/dist/cli/args.js +194 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/commands.generated.d.ts +4 -0
- package/dist/cli/commands.generated.js +2217 -0
- package/dist/cli/commands.generated.js.map +1 -0
- package/dist/cli/config.d.ts +46 -0
- package/dist/cli/config.js +107 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +19 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/output.d.ts +4 -0
- package/dist/cli/output.js +71 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/run.d.ts +15 -0
- package/dist/cli/run.js +194 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/spec.d.ts +42 -0
- package/dist/cli/spec.js +4 -0
- package/dist/cli/spec.js.map +1 -0
- package/dist/client.d.ts +224 -0
- package/dist/client.js +182 -0
- package/dist/client.js.map +1 -0
- package/dist/core/ajax.d.ts +25 -0
- package/dist/core/ajax.js +85 -0
- package/dist/core/ajax.js.map +1 -0
- package/dist/core/editor.d.ts +18 -0
- package/dist/core/editor.js +51 -0
- package/dist/core/editor.js.map +1 -0
- package/dist/core/errors.d.ts +12 -0
- package/dist/core/errors.js +10 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/files.d.ts +16 -0
- package/dist/core/files.js +58 -0
- package/dist/core/files.js.map +1 -0
- package/dist/core/forms.d.ts +45 -0
- package/dist/core/forms.js +200 -0
- package/dist/core/forms.js.map +1 -0
- package/dist/core/http.d.ts +18 -0
- package/dist/core/http.js +22 -0
- package/dist/core/http.js.map +1 -0
- package/dist/core/inPage.d.ts +8 -0
- package/dist/core/inPage.js +10 -0
- package/dist/core/inPage.js.map +1 -0
- package/dist/core/login.d.ts +40 -0
- package/dist/core/login.js +83 -0
- package/dist/core/login.js.map +1 -0
- package/dist/core/pace.d.ts +4 -0
- package/dist/core/pace.js +21 -0
- package/dist/core/pace.js.map +1 -0
- package/dist/core/session.d.ts +55 -0
- package/dist/core/session.js +157 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/site.d.ts +8 -0
- package/dist/core/site.js +36 -0
- package/dist/core/site.js.map +1 -0
- package/dist/core/text.d.ts +21 -0
- package/dist/core/text.js +113 -0
- package/dist/core/text.js.map +1 -0
- package/dist/home.d.ts +19 -0
- package/dist/home.js +54 -0
- package/dist/home.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/internals.d.ts +11 -0
- package/dist/internals.js +15 -0
- package/dist/internals.js.map +1 -0
- package/dist/moodle/activities.d.ts +94 -0
- package/dist/moodle/activities.js +240 -0
- package/dist/moodle/activities.js.map +1 -0
- package/dist/moodle/ajax.d.ts +22 -0
- package/dist/moodle/ajax.js +91 -0
- package/dist/moodle/ajax.js.map +1 -0
- package/dist/moodle/assign.d.ts +65 -0
- package/dist/moodle/assign.js +162 -0
- package/dist/moodle/assign.js.map +1 -0
- package/dist/moodle/calendar.d.ts +41 -0
- package/dist/moodle/calendar.js +80 -0
- package/dist/moodle/calendar.js.map +1 -0
- package/dist/moodle/choice.d.ts +33 -0
- package/dist/moodle/choice.js +100 -0
- package/dist/moodle/choice.js.map +1 -0
- package/dist/moodle/completion.d.ts +48 -0
- package/dist/moodle/completion.js +108 -0
- package/dist/moodle/completion.js.map +1 -0
- package/dist/moodle/content.d.ts +43 -0
- package/dist/moodle/content.js +105 -0
- package/dist/moodle/content.js.map +1 -0
- package/dist/moodle/course.d.ts +51 -0
- package/dist/moodle/course.js +45 -0
- package/dist/moodle/course.js.map +1 -0
- package/dist/moodle/courses.d.ts +57 -0
- package/dist/moodle/courses.js +70 -0
- package/dist/moodle/courses.js.map +1 -0
- package/dist/moodle/editor.d.ts +18 -0
- package/dist/moodle/editor.js +50 -0
- package/dist/moodle/editor.js.map +1 -0
- package/dist/moodle/files.d.ts +16 -0
- package/dist/moodle/files.js +57 -0
- package/dist/moodle/files.js.map +1 -0
- package/dist/moodle/forms.d.ts +44 -0
- package/dist/moodle/forms.js +199 -0
- package/dist/moodle/forms.js.map +1 -0
- package/dist/moodle/forum.d.ts +55 -0
- package/dist/moodle/forum.js +115 -0
- package/dist/moodle/forum.js.map +1 -0
- package/dist/moodle/forumPosts.d.ts +83 -0
- package/dist/moodle/forumPosts.js +113 -0
- package/dist/moodle/forumPosts.js.map +1 -0
- package/dist/moodle/gradebook.d.ts +102 -0
- package/dist/moodle/gradebook.js +246 -0
- package/dist/moodle/gradebook.js.map +1 -0
- package/dist/moodle/grades.d.ts +24 -0
- package/dist/moodle/grades.js +67 -0
- package/dist/moodle/grades.js.map +1 -0
- package/dist/moodle/grading.d.ts +92 -0
- package/dist/moodle/grading.js +161 -0
- package/dist/moodle/grading.js.map +1 -0
- package/dist/moodle/h5p.d.ts +31 -0
- package/dist/moodle/h5p.js +100 -0
- package/dist/moodle/h5p.js.map +1 -0
- package/dist/moodle/inPage.d.ts +8 -0
- package/dist/moodle/inPage.js +10 -0
- package/dist/moodle/inPage.js.map +1 -0
- package/dist/moodle/lesson.d.ts +57 -0
- package/dist/moodle/lesson.js +140 -0
- package/dist/moodle/lesson.js.map +1 -0
- package/dist/moodle/login.d.ts +37 -0
- package/dist/moodle/login.js +77 -0
- package/dist/moodle/login.js.map +1 -0
- package/dist/moodle/messages.d.ts +66 -0
- package/dist/moodle/messages.js +80 -0
- package/dist/moodle/messages.js.map +1 -0
- package/dist/moodle/overview.d.ts +83 -0
- package/dist/moodle/overview.js +137 -0
- package/dist/moodle/overview.js.map +1 -0
- package/dist/moodle/pace.d.ts +4 -0
- package/dist/moodle/pace.js +19 -0
- package/dist/moodle/pace.js.map +1 -0
- package/dist/moodle/participants.d.ts +55 -0
- package/dist/moodle/participants.js +131 -0
- package/dist/moodle/participants.js.map +1 -0
- package/dist/moodle/questionBank.d.ts +81 -0
- package/dist/moodle/questionBank.js +235 -0
- package/dist/moodle/questionBank.js.map +1 -0
- package/dist/moodle/quiz.d.ts +117 -0
- package/dist/moodle/quiz.js +291 -0
- package/dist/moodle/quiz.js.map +1 -0
- package/dist/moodle/quizResults.d.ts +36 -0
- package/dist/moodle/quizResults.js +93 -0
- package/dist/moodle/quizResults.js.map +1 -0
- package/dist/moodle/quizStructure.d.ts +42 -0
- package/dist/moodle/quizStructure.js +100 -0
- package/dist/moodle/quizStructure.js.map +1 -0
- package/dist/moodle/session.d.ts +39 -0
- package/dist/moodle/session.js +99 -0
- package/dist/moodle/session.js.map +1 -0
- package/dist/moodle/site.d.ts +8 -0
- package/dist/moodle/site.js +36 -0
- package/dist/moodle/site.js.map +1 -0
- package/dist/moodle/siteInfo.d.ts +25 -0
- package/dist/moodle/siteInfo.js +45 -0
- package/dist/moodle/siteInfo.js.map +1 -0
- package/dist/moodle/structure.d.ts +94 -0
- package/dist/moodle/structure.js +103 -0
- package/dist/moodle/structure.js.map +1 -0
- package/dist/moodle/submission.d.ts +48 -0
- package/dist/moodle/submission.js +109 -0
- package/dist/moodle/submission.js.map +1 -0
- package/dist/moodle/survey.d.ts +47 -0
- package/dist/moodle/survey.js +197 -0
- package/dist/moodle/survey.js.map +1 -0
- package/dist/moodle/text.d.ts +21 -0
- package/dist/moodle/text.js +113 -0
- package/dist/moodle/text.js.map +1 -0
- package/dist/moodle/user.d.ts +54 -0
- package/dist/moodle/user.js +78 -0
- package/dist/moodle/user.js.map +1 -0
- package/dist/options.d.ts +30 -0
- package/dist/options.js +35 -0
- package/dist/options.js.map +1 -0
- package/dist/sessionCookies.d.ts +11 -0
- package/dist/sessionCookies.js +31 -0
- package/dist/sessionCookies.js.map +1 -0
- package/dist/testing/fakeMoodle.d.ts +55 -0
- package/dist/testing/fakeMoodle.js +103 -0
- package/dist/testing/fakeMoodle.js.map +1 -0
- package/dist/testing/gradingPanel.d.ts +6 -0
- package/dist/testing/gradingPanel.js +22 -0
- package/dist/testing/gradingPanel.js.map +1 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +18 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +78 -4
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { BrowserContext } from "playwright-core";
|
|
2
|
+
import type { BrowserManager } from "../../browser/manager.js";
|
|
3
|
+
import { MoodleError } from "../../core/errors.js";
|
|
4
|
+
/** The activity isn't a SCORM the person can open, or Moodle didn't open its player. */
|
|
5
|
+
export declare class ScormError extends MoodleError {
|
|
6
|
+
name: string;
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
9
|
+
/** A SCO's status for the person, as Moodle tracks it from what the package reports. */
|
|
10
|
+
export type ScoStatus = "not-attempted" | "incomplete" | "completed" | "passed" | "failed" | "browsed" | "suspended";
|
|
11
|
+
/** A SCO (a launchable part of the package) with the person's status and score in the attempt read. */
|
|
12
|
+
export interface ScormSco {
|
|
13
|
+
id: number;
|
|
14
|
+
title: string;
|
|
15
|
+
status?: ScoStatus;
|
|
16
|
+
score?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface ScormActivity {
|
|
19
|
+
id: number;
|
|
20
|
+
name: string;
|
|
21
|
+
/** The activity's description, as HTML. */
|
|
22
|
+
description?: string;
|
|
23
|
+
/** Attempts the person may make: none means unlimited. */
|
|
24
|
+
attemptsAllowed?: number;
|
|
25
|
+
attemptsMade?: number;
|
|
26
|
+
/** How Moodle grades the activity, in the site's words ("Highest attempt"). */
|
|
27
|
+
gradingMethod?: string;
|
|
28
|
+
/** The grade reported to the gradebook, and each attempt's, as Moodle shows them ("100%"); none before the first attempt. */
|
|
29
|
+
grade?: string;
|
|
30
|
+
attemptGrades: {
|
|
31
|
+
attempt: number;
|
|
32
|
+
grade: string;
|
|
33
|
+
}[];
|
|
34
|
+
/** The SCOs with the person's status in their last attempt (the first, before any). */
|
|
35
|
+
scos: ScormSco[];
|
|
36
|
+
}
|
|
37
|
+
/** Where `openScorm` left the working tab: the player, at a SCO of an attempt. */
|
|
38
|
+
export interface OpenedScorm {
|
|
39
|
+
attempt: number;
|
|
40
|
+
scoId: number;
|
|
41
|
+
/** The SCORM version the player speaks to the package with. */
|
|
42
|
+
version?: "SCORM 1.2" | "SCORM 2004" | "AICC";
|
|
43
|
+
url: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A SCORM activity as the person sees it: its attempts (allowed and made), the grading method, the
|
|
47
|
+
* grade reported and each attempt's, and its SCOs with the person's status and score in the last
|
|
48
|
+
* attempt. Moodle doesn't show a SCORM's version on its page: `openScorm` tells it.
|
|
49
|
+
*/
|
|
50
|
+
export declare function scorm(context: BrowserContext, siteUrl: string, activityId: number): Promise<ScormActivity>;
|
|
51
|
+
/**
|
|
52
|
+
* Opens a SCORM in Moodle's player, in normal mode, on the browser's working tab (ADR-007), for the
|
|
53
|
+
* person (or a program driving the tab) to do it there: the last attempt continues unless `newAttempt`,
|
|
54
|
+
* at the activity's first SCO unless `scoId`. Resolves once the SCO's frame has loaded. Opening the
|
|
55
|
+
* player is what starts an attempt in Moodle.
|
|
56
|
+
*/
|
|
57
|
+
export declare function openScorm(browser: BrowserManager, siteUrl: string, activityId: number, options?: {
|
|
58
|
+
scoId?: number;
|
|
59
|
+
newAttempt?: boolean;
|
|
60
|
+
}): Promise<OpenedScorm>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { inPage } from "../../core/inPage.js";
|
|
2
|
+
import { NotLoggedInError, openSitePage } from "../../core/session.js";
|
|
3
|
+
import { siteFetch } from "../../core/http.js";
|
|
4
|
+
import { decodeEntities } from "../../core/text.js";
|
|
5
|
+
import { MoodleError } from "../../core/errors.js";
|
|
6
|
+
/** The activity isn't a SCORM the person can open, or Moodle didn't open its player. */
|
|
7
|
+
export class ScormError extends MoodleError {
|
|
8
|
+
name = "ScormError";
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message, "scorm");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
// The activity's page, never skipped to the player (which would start an attempt).
|
|
14
|
+
const viewPath = (activityId) => `mod/scorm/view.php?id=${activityId}&preventskip=1`;
|
|
15
|
+
function readView(page) {
|
|
16
|
+
// Through inPage: it has a helper function (see the conventions on page.evaluate).
|
|
17
|
+
return inPage(page, () => {
|
|
18
|
+
const g = globalThis;
|
|
19
|
+
const doc = g.document.body;
|
|
20
|
+
const form = doc.querySelector("#scormviewform");
|
|
21
|
+
const field = (name) => form?.querySelector(`input[name="${name}"]`)?.getAttribute("value") ?? undefined;
|
|
22
|
+
// Moodle's attempt status: one paragraph, its lines separated by <br>.
|
|
23
|
+
const status = doc.querySelector('[role="main"] .generalbox p');
|
|
24
|
+
const intro = doc.querySelector("#intro")?.innerHTML.trim();
|
|
25
|
+
return {
|
|
26
|
+
scorm: doc.id === "page-mod-scorm-view",
|
|
27
|
+
name: doc.querySelector("[data-activityname]")?.getAttribute("data-activityname") ?? "",
|
|
28
|
+
...(intro ? { description: intro } : {}),
|
|
29
|
+
lines: (status?.innerHTML ?? "")
|
|
30
|
+
.split(/<br\s*\/?>/i)
|
|
31
|
+
.map((line) => line.replace(/<[^>]*>/g, "").trim())
|
|
32
|
+
.filter((line) => line !== ""),
|
|
33
|
+
...(field("scoid") ? { scoId: Number(field("scoid")) } : {}),
|
|
34
|
+
...(field("currentorg") !== undefined ? { org: field("currentorg") } : {}),
|
|
35
|
+
...(g.M?.cfg?.sesskey ? { sesskey: g.M.cfg.sesskey } : {}),
|
|
36
|
+
};
|
|
37
|
+
}, null);
|
|
38
|
+
}
|
|
39
|
+
/** The SCOs of a table of contents (`prereqs.php`'s), status by icon and score by its "(label: value)": no wording read. */
|
|
40
|
+
function readScos(page, html) {
|
|
41
|
+
// Through inPage: it has a helper function (see the conventions on page.evaluate).
|
|
42
|
+
return inPage(page, (toc) => {
|
|
43
|
+
const g = globalThis;
|
|
44
|
+
const doc = new g.DOMParser().parseFromString(toc, "text/html");
|
|
45
|
+
// Font Awesome 6's icons (Moodle 4.4+), then 4's, then the images older themes use.
|
|
46
|
+
const ICONS = [
|
|
47
|
+
[/\bfa-square-check\b|\bfa-check-square-o\b/, "completed"],
|
|
48
|
+
[/\bfa-pen-to-square\b|\bfa-pencil-square-o\b/, "incomplete"],
|
|
49
|
+
[/\bfa-square\b|\bfa-square-o\b/, "not-attempted"],
|
|
50
|
+
[/\bfa-xmark\b|\bfa-times\b/, "failed"],
|
|
51
|
+
[/\bfa-check\b/, "passed"],
|
|
52
|
+
[/\bfa-eye\b/, "browsed"],
|
|
53
|
+
[/\bfa-pause\b/, "suspended"],
|
|
54
|
+
];
|
|
55
|
+
const IMAGES = [
|
|
56
|
+
[/notattempted/, "not-attempted"],
|
|
57
|
+
[/incomplete/, "incomplete"],
|
|
58
|
+
[/completed/, "completed"],
|
|
59
|
+
[/passed/, "passed"],
|
|
60
|
+
[/failed/, "failed"],
|
|
61
|
+
[/browsed/, "browsed"],
|
|
62
|
+
[/suspend/, "suspended"],
|
|
63
|
+
];
|
|
64
|
+
const statusOf = (link) => {
|
|
65
|
+
const icon = link.querySelector("i.icon, i[class*='fa-']");
|
|
66
|
+
if (icon)
|
|
67
|
+
return ICONS.find(([pattern]) => pattern.test(icon.className))?.[1];
|
|
68
|
+
const src = link.querySelector("img")?.getAttribute("src") ?? "";
|
|
69
|
+
return IMAGES.find(([pattern]) => pattern.test(src))?.[1];
|
|
70
|
+
};
|
|
71
|
+
return Array.from(doc.querySelectorAll("a[data-scoid]")).map((link) => {
|
|
72
|
+
const text = (link.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
73
|
+
const score = /\(\s*[^():]+:\s*(-?\d+(?:[.,]\d+)?)\s*\)\s*$/.exec(text);
|
|
74
|
+
const status = statusOf(link);
|
|
75
|
+
return {
|
|
76
|
+
id: Number(link.getAttribute("data-scoid")),
|
|
77
|
+
title: (score ? text.slice(0, score.index) : text).trim(),
|
|
78
|
+
...(status ? { status } : {}),
|
|
79
|
+
...(score ? { score: Number(score[1].replace(",", ".")) } : {}),
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
}, html);
|
|
83
|
+
}
|
|
84
|
+
const count = (value) => (value !== undefined && /^\d+$/.test(value) ? Number(value) : undefined);
|
|
85
|
+
// What follows a status line's label ("Number of attempts you have made: 2").
|
|
86
|
+
const valueOf = (line) => (line === undefined ? undefined : decodeEntities(line.slice(line.lastIndexOf(":") + 1)).trim());
|
|
87
|
+
/**
|
|
88
|
+
* A SCORM activity as the person sees it: its attempts (allowed and made), the grading method, the
|
|
89
|
+
* grade reported and each attempt's, and its SCOs with the person's status and score in the last
|
|
90
|
+
* attempt. Moodle doesn't show a SCORM's version on its page: `openScorm` tells it.
|
|
91
|
+
*/
|
|
92
|
+
export async function scorm(context, siteUrl, activityId) {
|
|
93
|
+
const page = await openSitePage(context, siteUrl, viewPath(activityId));
|
|
94
|
+
try {
|
|
95
|
+
const facts = await readView(page);
|
|
96
|
+
if (!facts.scorm)
|
|
97
|
+
throw new ScormError(`Activity ${activityId} isn't a SCORM you can open. Take the id of a SCORM activity (module "scorm") from the course structure.`);
|
|
98
|
+
// Moodle's attempt status, by position: allowed, made, one grade per attempt, method, reported.
|
|
99
|
+
const made = count(valueOf(facts.lines[1]));
|
|
100
|
+
const grades = facts.lines.slice(2, 2 + (made ?? 0));
|
|
101
|
+
const tail = facts.lines.slice(2 + (made ?? 0));
|
|
102
|
+
const reported = made ? valueOf(tail[1]) : undefined;
|
|
103
|
+
const activity = {
|
|
104
|
+
id: activityId,
|
|
105
|
+
name: decodeEntities(facts.name),
|
|
106
|
+
...(facts.description ? { description: facts.description } : {}),
|
|
107
|
+
...(count(valueOf(facts.lines[0])) !== undefined ? { attemptsAllowed: count(valueOf(facts.lines[0])) } : {}),
|
|
108
|
+
...(made !== undefined ? { attemptsMade: made } : {}),
|
|
109
|
+
...(tail[0] ? { gradingMethod: valueOf(tail[0]) } : {}),
|
|
110
|
+
...(reported ? { grade: reported } : {}),
|
|
111
|
+
attemptGrades: grades.map((line, i) => ({ attempt: i + 1, grade: valueOf(line) ?? "" })),
|
|
112
|
+
scos: [],
|
|
113
|
+
};
|
|
114
|
+
// The table of contents the player shows, without opening it (which would start an attempt).
|
|
115
|
+
if (facts.scoId !== undefined && facts.sesskey) {
|
|
116
|
+
const query = new URLSearchParams({ id: String(activityId), scoid: String(facts.scoId), attempt: String(made || 1), mode: "normal", currentorg: facts.org ?? "", sesskey: facts.sesskey });
|
|
117
|
+
const toc = await siteFetch(context, new URL(`mod/scorm/prereqs.php?${query}`, siteUrl).href);
|
|
118
|
+
if (new URL(toc.url).pathname.endsWith("/login/index.php"))
|
|
119
|
+
throw new NotLoggedInError();
|
|
120
|
+
if (toc.status === 200)
|
|
121
|
+
activity.scos = await readScos(page, toc.body);
|
|
122
|
+
}
|
|
123
|
+
return activity;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
await page.close().catch(() => undefined);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Opens a SCORM in Moodle's player, in normal mode, on the browser's working tab (ADR-007), for the
|
|
131
|
+
* person (or a program driving the tab) to do it there: the last attempt continues unless `newAttempt`,
|
|
132
|
+
* at the activity's first SCO unless `scoId`. Resolves once the SCO's frame has loaded. Opening the
|
|
133
|
+
* player is what starts an attempt in Moodle.
|
|
134
|
+
*/
|
|
135
|
+
export async function openScorm(browser, siteUrl, activityId, options = {}) {
|
|
136
|
+
const context = await browser.context();
|
|
137
|
+
const view = await openSitePage(context, siteUrl, viewPath(activityId), { follow: false });
|
|
138
|
+
let facts;
|
|
139
|
+
try {
|
|
140
|
+
facts = await readView(view);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
await view.close().catch(() => undefined);
|
|
144
|
+
}
|
|
145
|
+
if (!facts.scorm)
|
|
146
|
+
throw new ScormError(`Activity ${activityId} isn't a SCORM you can open. Take the id of a SCORM activity (module "scorm") from the course structure.`);
|
|
147
|
+
const scoId = options.scoId ?? facts.scoId;
|
|
148
|
+
if (scoId === undefined)
|
|
149
|
+
throw new ScormError(`SCORM ${activityId} can't be entered: Moodle offers no "Enter" on its page (closed, or no attempts left).`);
|
|
150
|
+
const query = new URLSearchParams({ cm: String(activityId), scoid: String(scoId), currentorg: facts.org ?? "", mode: "normal", newattempt: options.newAttempt ? "on" : "off" });
|
|
151
|
+
const tab = await browser.tab();
|
|
152
|
+
await tab.goto(new URL(`mod/scorm/player.php?${query}`, siteUrl).href, { waitUntil: "domcontentloaded" });
|
|
153
|
+
const html = await tab.content();
|
|
154
|
+
// M.scorm_api.init(…, sesskey, scoid, attempt, mode, …): what the player opened.
|
|
155
|
+
const init = /M\.scorm_api\.init\([\s\S]*?"[^"]*",\s*"?(\d+)"?,\s*"?(\d+)"?,\s*"(normal|browse|review)"/.exec(html);
|
|
156
|
+
if (!init) {
|
|
157
|
+
if (new URL(tab.url()).pathname.endsWith("/login/index.php"))
|
|
158
|
+
throw new NotLoggedInError();
|
|
159
|
+
throw new ScormError(`Moodle didn't open SCORM ${activityId}'s player (no attempts left, or the SCO isn't available yet).`);
|
|
160
|
+
}
|
|
161
|
+
const frame = await tab.waitForSelector("#scorm_object", { timeout: 30_000 }).catch(() => null);
|
|
162
|
+
if (!frame)
|
|
163
|
+
throw new ScormError(`SCORM ${activityId}'s player opened, but no SCO loaded in it.`);
|
|
164
|
+
// The frame opens Moodle's loadSCO.php ("loading, please wait"), which then goes to the SCO itself.
|
|
165
|
+
const sco = await frame.contentFrame();
|
|
166
|
+
await sco?.waitForURL((url) => !url.pathname.endsWith("/mod/scorm/loadSCO.php"), { timeout: 30_000, waitUntil: "load" }).catch(() => undefined);
|
|
167
|
+
const model = /mod\/scorm\/datamodels\/(scorm_12|scorm_13|aicc)\.js/.exec(html)?.[1];
|
|
168
|
+
const version = model === "scorm_12" ? "SCORM 1.2" : model === "scorm_13" ? "SCORM 2004" : model === "aicc" ? "AICC" : undefined;
|
|
169
|
+
return { attempt: Number(init[2]), scoId: Number(init[1]), ...(version ? { version } : {}), url: tab.url() };
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=scorm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scorm.js","sourceRoot":"","sources":["../../../src/areas/scorm/scorm.ts"],"names":[],"mappings":"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"}
|
|
@@ -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 "../../core/ajax.js";
|
|
2
|
+
import { decodeEntities } from "../../core/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=info.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"info.js","sourceRoot":"","sources":["../../../src/areas/site/info.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { BrowserContext } from "playwright-core";
|
|
2
|
+
/** A user, with what Moodle lets the person see of them. */
|
|
3
|
+
export interface User {
|
|
4
|
+
id: number;
|
|
5
|
+
username?: string;
|
|
6
|
+
fullName: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
/** ISO 8601; none when they never came. */
|
|
9
|
+
firstAccess?: string;
|
|
10
|
+
lastAccess?: string;
|
|
11
|
+
}
|
|
12
|
+
/** A user by id, or `undefined` when Moodle gives nobody (no such user, or not one the person may see). */
|
|
13
|
+
export declare function userById(context: BrowserContext, siteUrl: string, userId: number): Promise<User | undefined>;
|
|
14
|
+
/** The site's name, from its public settings; `undefined` where the mobile app support is off. */
|
|
15
|
+
export declare function siteName(context: BrowserContext, siteUrl: string): Promise<string | undefined>;
|
|
16
|
+
/** A user's roles in a course, from their profile there: role links carry the role id in any language. */
|
|
17
|
+
export declare function courseRoles(context: BrowserContext, siteUrl: string, userId: number, courseId: number): Promise<{
|
|
18
|
+
id: number;
|
|
19
|
+
name: string;
|
|
20
|
+
}[]>;
|
|
21
|
+
/**
|
|
22
|
+
* What the person can do in a course, asked of read-only services that need the capability: the
|
|
23
|
+
* activity types they can add (none without `moodle/course:manageactivities`) and the grader
|
|
24
|
+
* report's users (`nopermissions` without `gradereport/grader:view`). Nothing is changed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function courseAbilities(context: BrowserContext, siteUrl: string, courseId: number, sectionId: number): Promise<{
|
|
27
|
+
canEdit: boolean;
|
|
28
|
+
canGrade: boolean;
|
|
29
|
+
}>;
|
|
30
|
+
export interface Me {
|
|
31
|
+
userId: number;
|
|
32
|
+
username?: string;
|
|
33
|
+
fullName: string;
|
|
34
|
+
email?: string;
|
|
35
|
+
/** The interface language in use. */
|
|
36
|
+
language: string;
|
|
37
|
+
timeZone: string;
|
|
38
|
+
firstAccess?: string;
|
|
39
|
+
lastAccess?: string;
|
|
40
|
+
site: {
|
|
41
|
+
name?: string;
|
|
42
|
+
url: string;
|
|
43
|
+
};
|
|
44
|
+
/** With a course: the person's roles there and what they can do. */
|
|
45
|
+
course?: {
|
|
46
|
+
id: number;
|
|
47
|
+
roles: {
|
|
48
|
+
id: number;
|
|
49
|
+
name: string;
|
|
50
|
+
}[];
|
|
51
|
+
canEdit: boolean;
|
|
52
|
+
canGrade: boolean;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Who the logged-in person is; with `courseId`, also their roles and abilities in that course. */
|
|
56
|
+
export declare function whoAmI(context: BrowserContext, siteUrl: string, courseId?: number): Promise<Me>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { MoodleServiceError, callService, readPageConfig } from "../../core/ajax.js";
|
|
2
|
+
import { courseState } from "../courses/state.js";
|
|
3
|
+
import { isoDate } from "../courses/list.js";
|
|
4
|
+
import { openSitePage } from "../../core/session.js";
|
|
5
|
+
import { decodeEntities } from "../../core/text.js";
|
|
6
|
+
/** A user by id, or `undefined` when Moodle gives nobody (no such user, or not one the person may see). */
|
|
7
|
+
export async function userById(context, siteUrl, userId) {
|
|
8
|
+
const [user] = await callService(context, siteUrl, "core_user_get_users_by_field", { field: "id", values: [String(userId)] });
|
|
9
|
+
if (!user)
|
|
10
|
+
return undefined;
|
|
11
|
+
return {
|
|
12
|
+
id: user.id,
|
|
13
|
+
...(user.username !== undefined ? { username: user.username } : {}),
|
|
14
|
+
fullName: decodeEntities(user.fullname),
|
|
15
|
+
...(user.email !== undefined ? { email: user.email } : {}),
|
|
16
|
+
...(user.firstaccess ? { firstAccess: isoDate(user.firstaccess) } : {}),
|
|
17
|
+
...(user.lastaccess ? { lastAccess: isoDate(user.lastaccess) } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** The site's name, from its public settings; `undefined` where the mobile app support is off. */
|
|
21
|
+
export async function siteName(context, siteUrl) {
|
|
22
|
+
try {
|
|
23
|
+
return (await callService(context, siteUrl, "tool_mobile_get_public_config", {})).sitename;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** A user's roles in a course, from their profile there: role links carry the role id in any language. */
|
|
30
|
+
export async function courseRoles(context, siteUrl, userId, courseId) {
|
|
31
|
+
const page = await openSitePage(context, siteUrl, `user/view.php?id=${userId}&course=${courseId}`);
|
|
32
|
+
try {
|
|
33
|
+
return await page.evaluate(() => {
|
|
34
|
+
const links = globalThis.document.querySelectorAll('#region-main a[href*="/user/index.php"][href*="roleid="]');
|
|
35
|
+
const roles = new Map();
|
|
36
|
+
for (const link of Array.from(links)) {
|
|
37
|
+
const id = Number(new URL(link.href).searchParams.get("roleid"));
|
|
38
|
+
if (id > 0)
|
|
39
|
+
roles.set(id, (link.textContent ?? "").trim());
|
|
40
|
+
}
|
|
41
|
+
return Array.from(roles, ([id, name]) => ({ id, name }));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await page.close().catch(() => undefined);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* What the person can do in a course, asked of read-only services that need the capability: the
|
|
50
|
+
* activity types they can add (none without `moodle/course:manageactivities`) and the grader
|
|
51
|
+
* report's users (`nopermissions` without `gradereport/grader:view`). Nothing is changed.
|
|
52
|
+
*/
|
|
53
|
+
export async function courseAbilities(context, siteUrl, courseId, sectionId) {
|
|
54
|
+
const items = await callService(context, siteUrl, "core_courseformat_get_section_content_items", { courseid: courseId, sectionid: sectionId });
|
|
55
|
+
let canGrade = true;
|
|
56
|
+
try {
|
|
57
|
+
await callService(context, siteUrl, "gradereport_grader_get_users_in_report", { courseid: courseId });
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (!(error instanceof MoodleServiceError && error.errorcode === "nopermissions"))
|
|
61
|
+
throw error;
|
|
62
|
+
canGrade = false;
|
|
63
|
+
}
|
|
64
|
+
return { canEdit: items.content_items.length > 0, canGrade };
|
|
65
|
+
}
|
|
66
|
+
/** Who the logged-in person is; with `courseId`, also their roles and abilities in that course. */
|
|
67
|
+
export async function whoAmI(context, siteUrl, courseId) {
|
|
68
|
+
const cfg = await readPageConfig(context, siteUrl);
|
|
69
|
+
const user = await userById(context, siteUrl, cfg.userId);
|
|
70
|
+
const me = {
|
|
71
|
+
userId: cfg.userId,
|
|
72
|
+
username: user?.username,
|
|
73
|
+
fullName: user?.fullName ?? "",
|
|
74
|
+
email: user?.email,
|
|
75
|
+
language: cfg.language,
|
|
76
|
+
timeZone: cfg.timeZone,
|
|
77
|
+
firstAccess: user?.firstAccess,
|
|
78
|
+
lastAccess: user?.lastAccess,
|
|
79
|
+
site: { name: await siteName(context, siteUrl), url: siteUrl },
|
|
80
|
+
};
|
|
81
|
+
if (courseId !== undefined) {
|
|
82
|
+
// The course's state checks that it exists and the person can see it, and gives a section.
|
|
83
|
+
const state = await courseState(context, siteUrl, courseId);
|
|
84
|
+
const abilities = await courseAbilities(context, siteUrl, courseId, Number(state.section[0]?.id ?? 0));
|
|
85
|
+
me.course = { id: courseId, roles: await courseRoles(context, siteUrl, cfg.userId, courseId), ...abilities };
|
|
86
|
+
}
|
|
87
|
+
return me;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=users.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"users.js","sourceRoot":"","sources":["../../../src/areas/site/users.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from "playwright-core";
|
|
2
|
+
import { MoodleError } from "../../core/errors.js";
|
|
3
|
+
/** The choice isn't there, or the person can't answer it now. */
|
|
4
|
+
export declare class ChoiceError extends MoodleError {
|
|
5
|
+
name: string;
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
export interface Choice {
|
|
9
|
+
id: number;
|
|
10
|
+
name: string;
|
|
11
|
+
intro: string;
|
|
12
|
+
/** The options with the ids answers take; full ones can't be chosen. */
|
|
13
|
+
options: {
|
|
14
|
+
id: number;
|
|
15
|
+
text: string;
|
|
16
|
+
selected: boolean;
|
|
17
|
+
full: boolean;
|
|
18
|
+
}[];
|
|
19
|
+
/** Several options at once. */
|
|
20
|
+
multiple: boolean;
|
|
21
|
+
/** The form is there: the choice is open and the person may answer (or change their answer). */
|
|
22
|
+
canAnswer: boolean;
|
|
23
|
+
/** Whether a given answer can be removed or changed. */
|
|
24
|
+
canChange: boolean;
|
|
25
|
+
/** The options chosen, as the page names them, also where the form is gone. */
|
|
26
|
+
answer: string[];
|
|
27
|
+
/** The page's notices: closed, not open yet, results not published… */
|
|
28
|
+
messages: string[];
|
|
29
|
+
}
|
|
30
|
+
/** The choice page, by its form's inputs and the selection block. */
|
|
31
|
+
export declare function readChoice(page: Page): Promise<Omit<Choice, "id">>;
|
|
32
|
+
/** A choice: its options, whether it can be answered or changed, and the person's answer. */
|
|
33
|
+
export declare function getChoice(context: BrowserContext, siteUrl: string, activityId: number): Promise<Choice>;
|
|
34
|
+
/** Answers a choice with one option (several where it allows them) by id, as "Save my choice" does; returns the choice afterwards. */
|
|
35
|
+
export declare function answerChoice(context: BrowserContext, siteUrl: string, activityId: number, optionIds: number[]): Promise<{
|
|
36
|
+
changed: string;
|
|
37
|
+
choice: Choice;
|
|
38
|
+
}>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { inPage } from "../../core/inPage.js";
|
|
2
|
+
import { openSitePage } from "../../core/session.js";
|
|
3
|
+
import { MoodleError } from "../../core/errors.js";
|
|
4
|
+
/** The choice isn't there, or the person can't answer it now. */
|
|
5
|
+
export class ChoiceError extends MoodleError {
|
|
6
|
+
name = "ChoiceError";
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message, "choice");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** The choice page, by its form's inputs and the selection block. */
|
|
12
|
+
export function readChoice(page) {
|
|
13
|
+
// Through inPage: it has a helper function (see the conventions on page.evaluate).
|
|
14
|
+
return inPage(page, () => {
|
|
15
|
+
const doc = globalThis.document;
|
|
16
|
+
const text = (el) => (el?.innerText ?? "").replace(/\s+/g, " ").trim();
|
|
17
|
+
const form = doc.querySelector('#region-main form[action*="/mod/choice/view.php"]');
|
|
18
|
+
const inputs = Array.from(form?.querySelectorAll('input[name="answer"], input[name="answer[]"]') ?? []);
|
|
19
|
+
const options = inputs.map((input) => ({
|
|
20
|
+
id: Number(input.value),
|
|
21
|
+
text: text(doc.querySelector(`label[for="${input.id}"]`)),
|
|
22
|
+
selected: input.checked,
|
|
23
|
+
full: input.disabled,
|
|
24
|
+
}));
|
|
25
|
+
// "Your selection: while" (several joined with "; ")? The label before the colon is the site's language.
|
|
26
|
+
const selection = text(doc.querySelector("#yourselection"));
|
|
27
|
+
const fromBlock = selection.includes(":") ? selection.slice(selection.indexOf(":") + 1).split(/;\s*/).map((s) => s.trim()).filter(Boolean) : [];
|
|
28
|
+
return {
|
|
29
|
+
name: doc.querySelector("[data-activityname]")?.getAttribute("data-activityname") ?? "",
|
|
30
|
+
intro: text(doc.querySelector(".activity-description, #intro")),
|
|
31
|
+
options,
|
|
32
|
+
multiple: inputs.some((input) => input.type === "checkbox"),
|
|
33
|
+
canAnswer: form !== null && options.length > 0,
|
|
34
|
+
canChange: doc.querySelector('#region-main a[href*="action=delchoice"]') !== null,
|
|
35
|
+
answer: options.some((o) => o.selected) ? options.filter((o) => o.selected).map((o) => o.text) : fromBlock,
|
|
36
|
+
messages: Array.from(doc.querySelectorAll("#region-main .alert, #region-main .generalbox"))
|
|
37
|
+
.filter((box) => box.id !== "yourselection" && !box.querySelector("form"))
|
|
38
|
+
.map((box) => {
|
|
39
|
+
// Without the notification's close button, whose label is in the site's language.
|
|
40
|
+
const copy = box.cloneNode(true);
|
|
41
|
+
for (const extra of Array.from(copy.querySelectorAll("button, .visually-hidden")))
|
|
42
|
+
extra.remove();
|
|
43
|
+
return (copy.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
44
|
+
})
|
|
45
|
+
.filter(Boolean),
|
|
46
|
+
};
|
|
47
|
+
}, null);
|
|
48
|
+
}
|
|
49
|
+
async function openChoice(context, siteUrl, activityId) {
|
|
50
|
+
const page = await openSitePage(context, siteUrl, `mod/choice/view.php?id=${activityId}`);
|
|
51
|
+
if ((await page.locator("body#page-mod-choice-view").count()) === 0) {
|
|
52
|
+
await page.close().catch(() => undefined);
|
|
53
|
+
throw new ChoiceError(`Activity ${activityId} isn't a choice you can open. Take the id of a choice (module "choice") from the course structure.`);
|
|
54
|
+
}
|
|
55
|
+
return page;
|
|
56
|
+
}
|
|
57
|
+
/** A choice: its options, whether it can be answered or changed, and the person's answer. */
|
|
58
|
+
export async function getChoice(context, siteUrl, activityId) {
|
|
59
|
+
const page = await openChoice(context, siteUrl, activityId);
|
|
60
|
+
try {
|
|
61
|
+
return { id: activityId, ...(await readChoice(page)) };
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
await page.close().catch(() => undefined);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Answers a choice with one option (several where it allows them) by id, as "Save my choice" does; returns the choice afterwards. */
|
|
68
|
+
export async function answerChoice(context, siteUrl, activityId, optionIds) {
|
|
69
|
+
if (optionIds.length === 0)
|
|
70
|
+
throw new ChoiceError("Give the id of the option to choose.");
|
|
71
|
+
const page = await openChoice(context, siteUrl, activityId);
|
|
72
|
+
try {
|
|
73
|
+
const choice = await readChoice(page);
|
|
74
|
+
if (!choice.canAnswer) {
|
|
75
|
+
// Moodle explains only some cases (closed before answering); an answered, closed choice just shows the answer.
|
|
76
|
+
const answered = choice.answer.length > 0 ? ` Your answer stays: ${choice.answer.join(", ")}.` : "";
|
|
77
|
+
const said = choice.messages.length > 0 ? ` Moodle says: ${choice.messages.join(" ")}` : "";
|
|
78
|
+
throw new ChoiceError(`You can't answer choice ${activityId} now: it's closed, not open yet, or doesn't let answers change.${answered}${said}`);
|
|
79
|
+
}
|
|
80
|
+
if (!choice.multiple && optionIds.length > 1)
|
|
81
|
+
throw new ChoiceError(`Choice ${activityId} takes one option only.`);
|
|
82
|
+
for (const id of optionIds) {
|
|
83
|
+
const option = choice.options.find((o) => o.id === id);
|
|
84
|
+
if (!option)
|
|
85
|
+
throw new ChoiceError(`Choice ${activityId} has no option ${id}; its options are: ${choice.options.map((o) => `${o.id} "${o.text}"`).join(", ")}.`);
|
|
86
|
+
if (option.full)
|
|
87
|
+
throw new ChoiceError(`Option ${id} "${option.text}" is full.`);
|
|
88
|
+
}
|
|
89
|
+
const form = '#region-main form[action*="/mod/choice/view.php"]';
|
|
90
|
+
for (const option of choice.options) {
|
|
91
|
+
const input = page.locator(`${form} input[name^="answer"][value="${option.id}"]`);
|
|
92
|
+
if (optionIds.includes(option.id))
|
|
93
|
+
await input.check();
|
|
94
|
+
else if (choice.multiple)
|
|
95
|
+
await input.uncheck();
|
|
96
|
+
}
|
|
97
|
+
await Promise.all([page.waitForNavigation({ waitUntil: "domcontentloaded" }), page.click(`${form} [type="submit"]`)]);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await page.close().catch(() => undefined);
|
|
101
|
+
}
|
|
102
|
+
const after = await getChoice(context, siteUrl, activityId);
|
|
103
|
+
return { changed: `Answered choice ${activityId}: ${after.answer.join(", ") || optionIds.join(", ")}.`, choice: after };
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=choice.js.map
|