@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.
- package/LICENSE +21 -0
- package/README.md +133 -2
- package/dist/areas/activities/build.d.ts +98 -0
- package/dist/areas/activities/build.js +246 -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/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 +222 -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 +2203 -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 +217 -0
- package/dist/client.js +179 -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 +40 -0
- package/dist/index.js +51 -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 +77 -4
|
@@ -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":"AACA,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,MAAM,UAAU,GAAG,MAAM,CAAC;AAgB1B,oGAAoG;AACpG,MAAM,WAAW,GAAmD,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC;AAE9G;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAe,EAAE,YAA0B,KAAK;IACnF,MAAM,UAAU,GAAG,+BAA+B,CAAC;IACnD,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,IAAI,GAAG,CAAC,qCAAqC,UAAU,EAAE,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE;QACzG,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QAC1D,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC;KACxC,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,oBAAoB,CAAe,UAAU,EAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACtG,2FAA2F;IAC3F,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACvH,OAAO;QACL,IAAI,EAAE,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC;QACrC,GAAG,EAAE,MAAM,CAAC,OAAO;QACnB,QAAQ,EAAE,MAAM,CAAC,IAAI;QACrB,GAAG,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9C,KAAK,EAAE;YACL,GAAG,CAAC,MAAM,CAAC,WAAW,KAAK,SAAS,IAAI,WAAW,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7H,WAAW,EAAE,MAAM,CAAC,UAAU,KAAK,CAAC;YACpC,iBAAiB,EAAE,CAAC,MAAM,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;SACvF;QACD,GAAG,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,cAAc,CAAC,MAAM,CAAC,kBAAkB,IAAI,EAAE,CAAC,IAAI,kCAAkC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5I,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACnE,CAAC;AACJ,CAAC;AAED,oGAAoG;AACpG,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,OAAuB,EAAE,OAAe,EAAE,QAAgB;IAClG,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,GAAG,MAAM,WAAW,CAAyE,OAAO,EAAE,OAAO,EAAE,sCAAsC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC7M,wDAAwD;IACxD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,OAAO,KAAK;SACT,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;SAC7D,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AACjI,CAAC"}
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"choice.js","sourceRoot":"","sources":["../../../src/areas/surveys/choice.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from "playwright-core";
|
|
2
|
+
import { MoodleError } from "../../core/errors.js";
|
|
3
|
+
/** The two survey modules: the questionnaire plugin (mod_questionnaire) and Moodle's feedback. */
|
|
4
|
+
export type SurveyType = "questionnaire" | "feedback";
|
|
5
|
+
export declare class SurveyError extends MoodleError {
|
|
6
|
+
name: string;
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
9
|
+
/** One field of a survey question, by its form name. */
|
|
10
|
+
export interface SurveyField {
|
|
11
|
+
name: string;
|
|
12
|
+
kind: "choice" | "checkbox" | "text" | "select";
|
|
13
|
+
label?: string;
|
|
14
|
+
options?: {
|
|
15
|
+
value: string;
|
|
16
|
+
label: string;
|
|
17
|
+
}[];
|
|
18
|
+
value?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface SurveyQuestion {
|
|
21
|
+
page: number;
|
|
22
|
+
number?: string;
|
|
23
|
+
text: string;
|
|
24
|
+
required: boolean;
|
|
25
|
+
fields: SurveyField[];
|
|
26
|
+
}
|
|
27
|
+
/** The questions on a survey page, with their fields read from the form (names, options, labels). */
|
|
28
|
+
export declare function readSurveyPage(page: Page, type: SurveyType, pageNumber: number): Promise<SurveyQuestion[]>;
|
|
29
|
+
/** Which survey module an activity is, from the page each module answers with its own view (body id). */
|
|
30
|
+
export declare function surveyType(context: BrowserContext, siteUrl: string, activityId: number): Promise<SurveyType>;
|
|
31
|
+
/** A survey (questionnaire or feedback): whether it can be answered, its intro and messages, and its questions page by page. Its module is found when not given. */
|
|
32
|
+
export declare function getSurvey(context: BrowserContext, siteUrl: string, activityId: number, type?: SurveyType): Promise<{
|
|
33
|
+
questions: SurveyQuestion[];
|
|
34
|
+
canAnswer: boolean;
|
|
35
|
+
intro: string;
|
|
36
|
+
messages: string[];
|
|
37
|
+
}>;
|
|
38
|
+
/**
|
|
39
|
+
* Fills the survey page by page with `responses` (field name → value) and submits it on the last
|
|
40
|
+
* page. Every name is checked against the form first, so nothing is sent with a wrong one.
|
|
41
|
+
*/
|
|
42
|
+
export declare function submitSurvey(context: BrowserContext, siteUrl: string, activityId: number, type: SurveyType, responses: Record<string, string>): Promise<{
|
|
43
|
+
changed: string;
|
|
44
|
+
messages: string[];
|
|
45
|
+
}>;
|
|
46
|
+
export declare function submitSurvey(context: BrowserContext, siteUrl: string, activityId: number, responses: Record<string, string>, options?: {
|
|
47
|
+
type?: SurveyType;
|
|
48
|
+
}): Promise<{
|
|
49
|
+
changed: string;
|
|
50
|
+
messages: string[];
|
|
51
|
+
}>;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { setFields } from "../../core/forms.js";
|
|
2
|
+
import { inPage } from "../../core/inPage.js";
|
|
3
|
+
import { openSitePage } from "../../core/session.js";
|
|
4
|
+
import { MoodleError } from "../../core/errors.js";
|
|
5
|
+
export class SurveyError extends MoodleError {
|
|
6
|
+
name = "SurveyError";
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message, "survey");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Where each module keeps its form, its questions, their text and their "required" mark. */
|
|
12
|
+
const LAYOUT = {
|
|
13
|
+
questionnaire: { form: "#phpesp_response", question: "fieldset.qn-container", text: ".qn-question", required: "img.req", number: ".qn-number", next: "next", finish: "submit" },
|
|
14
|
+
feedback: { form: "form.feedback_form", question: ".feedback_itemlist", text: ".col-form-label", required: ".col-form-label .text-danger", number: ".itemnr", next: "gonextpage", finish: "savevalues" },
|
|
15
|
+
};
|
|
16
|
+
/** The questions on a survey page, with their fields read from the form (names, options, labels). */
|
|
17
|
+
export function readSurveyPage(page, type, pageNumber) {
|
|
18
|
+
return inPage(page, ({ layout, pageNumber }) => {
|
|
19
|
+
const doc = globalThis.document;
|
|
20
|
+
// Text as shown, without what only screen readers get.
|
|
21
|
+
const text = (el) => {
|
|
22
|
+
if (!el)
|
|
23
|
+
return "";
|
|
24
|
+
const copy = el.cloneNode(true);
|
|
25
|
+
for (const hidden of Array.from(copy.querySelectorAll(".accesshide, .visually-hidden, .sr-only, input")))
|
|
26
|
+
hidden.remove();
|
|
27
|
+
doc.body.appendChild(copy);
|
|
28
|
+
const shown = copy.innerText ?? "";
|
|
29
|
+
copy.remove();
|
|
30
|
+
return shown.replace(/\s+/g, " ").trim();
|
|
31
|
+
};
|
|
32
|
+
// An option's label: its own <label for>, the label around it, or (a rating row) nothing.
|
|
33
|
+
const labelOf = (input) => {
|
|
34
|
+
const own = input.id ? doc.querySelector(`label[for="${input.id}"]:not(.accesshide)`) : null;
|
|
35
|
+
const around = input.closest("label");
|
|
36
|
+
return text(own ?? around) || undefined;
|
|
37
|
+
};
|
|
38
|
+
const form = doc.querySelector(layout.form);
|
|
39
|
+
if (!form)
|
|
40
|
+
return [];
|
|
41
|
+
return Array.from(form.querySelectorAll(layout.question)).map((question) => {
|
|
42
|
+
const fields = [];
|
|
43
|
+
for (const input of Array.from(question.querySelectorAll("input:not([type=hidden]), select, textarea"))) {
|
|
44
|
+
if (!input.name)
|
|
45
|
+
continue;
|
|
46
|
+
if (input.type === "radio") {
|
|
47
|
+
let field = fields.find((f) => f.name === input.name);
|
|
48
|
+
if (!field) {
|
|
49
|
+
// A rating row's item is the row's first cell.
|
|
50
|
+
const row = input.closest("tr.raterow");
|
|
51
|
+
field = { name: input.name, kind: "choice", options: [], ...(row ? { label: text(row.querySelector("td")) } : {}) };
|
|
52
|
+
fields.push(field);
|
|
53
|
+
}
|
|
54
|
+
const label = labelOf(input);
|
|
55
|
+
field.options.push({ value: input.value, label: label && !label.includes(",") ? label : input.value === "-999" ? "(no answer)" : (label ?? input.value) });
|
|
56
|
+
if (input.checked)
|
|
57
|
+
field.value = input.value;
|
|
58
|
+
}
|
|
59
|
+
else if (input.type === "checkbox") {
|
|
60
|
+
fields.push({ name: input.name, kind: "checkbox", label: labelOf(input), value: input.checked ? "1" : "0" });
|
|
61
|
+
}
|
|
62
|
+
else if (input.tagName === "SELECT") {
|
|
63
|
+
const options = Array.from(input.options);
|
|
64
|
+
fields.push({ name: input.name, kind: "select", options: options.map((o) => ({ value: o.value, label: o.text.trim() })), value: options.find((o) => o.selected)?.value });
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
fields.push({ name: input.name, kind: "text", ...(input.value ? { value: input.value } : {}) });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
page: pageNumber,
|
|
72
|
+
number: text(question.querySelector(layout.number)) || undefined,
|
|
73
|
+
text: text(question.querySelector(layout.text)),
|
|
74
|
+
required: question.querySelector(layout.required) !== null,
|
|
75
|
+
fields,
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
}, { layout: LAYOUT[type], pageNumber });
|
|
79
|
+
}
|
|
80
|
+
/** Whether the person can answer now, and the survey's page message (completed, closed…). */
|
|
81
|
+
async function surveyStatus(context, siteUrl, activityId, type) {
|
|
82
|
+
const page = await openSitePage(context, siteUrl, `mod/${type}/view.php?id=${activityId}`);
|
|
83
|
+
try {
|
|
84
|
+
if ((await page.locator(`body#page-mod-${type}-view`).count()) === 0)
|
|
85
|
+
throw new SurveyError(`Activity ${activityId} isn't a ${type} you can open. Take its id and module from the course structure.`);
|
|
86
|
+
return await page.evaluate(() => {
|
|
87
|
+
const doc = globalThis.document;
|
|
88
|
+
return {
|
|
89
|
+
canAnswer: doc.querySelector('#region-main a[href*="complete.php"]') !== null,
|
|
90
|
+
intro: (doc.querySelector(".activity-description, #intro")?.innerText ?? "").trim(),
|
|
91
|
+
messages: Array.from(doc.querySelectorAll("#region-main .alert, #region-main .notifymessage, #region-main .complete, #region-main .generalbox p"))
|
|
92
|
+
.map((e) => e.innerText.replace(/\s+/g, " ").trim())
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.slice(0, 6),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await page.close().catch(() => undefined);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Every page of the survey's form: feedback pages through `gopage`, questionnaire sections through `sec`. */
|
|
103
|
+
async function readAllPages(context, siteUrl, activityId, type) {
|
|
104
|
+
const layout = LAYOUT[type];
|
|
105
|
+
const questions = [];
|
|
106
|
+
const page = await openSitePage(context, siteUrl, `mod/${type}/complete.php?id=${activityId}`);
|
|
107
|
+
try {
|
|
108
|
+
for (let p = 0; p < 50; p++) {
|
|
109
|
+
if (p > 0) {
|
|
110
|
+
const next = type === "feedback" ? `mod/feedback/complete.php?id=${activityId}&gopage=${p}` : `mod/questionnaire/complete.php?id=${activityId}&sec=${p + 1}`;
|
|
111
|
+
await page.goto(new URL(next, siteUrl).href, { waitUntil: "domcontentloaded" });
|
|
112
|
+
}
|
|
113
|
+
const found = await readSurveyPage(page, type, p);
|
|
114
|
+
if (found.length === 0)
|
|
115
|
+
break;
|
|
116
|
+
questions.push(...found);
|
|
117
|
+
if ((await page.locator(`${layout.form} [name="${layout.next}"]`).count()) === 0)
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
return questions;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await page.close().catch(() => undefined);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// A course module's module never changes: one look per survey and site, for the process.
|
|
127
|
+
const surveyTypes = new Map();
|
|
128
|
+
/** Which survey module an activity is, from the page each module answers with its own view (body id). */
|
|
129
|
+
export async function surveyType(context, siteUrl, activityId) {
|
|
130
|
+
const key = `${siteUrl}|${activityId}`;
|
|
131
|
+
const known = surveyTypes.get(key);
|
|
132
|
+
if (known)
|
|
133
|
+
return known;
|
|
134
|
+
for (const type of ["questionnaire", "feedback"]) {
|
|
135
|
+
const page = await openSitePage(context, siteUrl, `mod/${type}/view.php?id=${activityId}`, { follow: false });
|
|
136
|
+
try {
|
|
137
|
+
if ((await page.locator(`body#page-mod-${type}-view`).count()) > 0) {
|
|
138
|
+
surveyTypes.set(key, type);
|
|
139
|
+
return type;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
await page.close().catch(() => undefined);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
throw new SurveyError(`Activity ${activityId} isn't a questionnaire or a feedback activity you can open. Take its id and module from the course structure.`);
|
|
147
|
+
}
|
|
148
|
+
/** A survey (questionnaire or feedback): whether it can be answered, its intro and messages, and its questions page by page. Its module is found when not given. */
|
|
149
|
+
export async function getSurvey(context, siteUrl, activityId, type) {
|
|
150
|
+
type ??= await surveyType(context, siteUrl, activityId);
|
|
151
|
+
const status = await surveyStatus(context, siteUrl, activityId, type);
|
|
152
|
+
return { ...status, questions: status.canAnswer ? await readAllPages(context, siteUrl, activityId, type) : [] };
|
|
153
|
+
}
|
|
154
|
+
export async function submitSurvey(context, siteUrl, activityId, typeOrResponses, responsesOrOptions) {
|
|
155
|
+
// (activityId, type, responses), as before 0.2, or (activityId, responses, { type }), the module found when not given.
|
|
156
|
+
const responses = (typeof typeOrResponses === "string" ? responsesOrOptions : typeOrResponses);
|
|
157
|
+
const type = typeof typeOrResponses === "string" ? typeOrResponses : (responsesOrOptions?.type ?? (await surveyType(context, siteUrl, activityId)));
|
|
158
|
+
const layout = LAYOUT[type];
|
|
159
|
+
const status = await surveyStatus(context, siteUrl, activityId, type);
|
|
160
|
+
if (!status.canAnswer)
|
|
161
|
+
throw new SurveyError(`You can't answer ${type} ${activityId} now: ${status.messages.join(" ") || "it's completed, closed or not open yet"}.`);
|
|
162
|
+
const known = new Set((await readAllPages(context, siteUrl, activityId, type)).flatMap((q) => q.fields.map((f) => f.name)));
|
|
163
|
+
const unknown = Object.keys(responses).filter((name) => !known.has(name));
|
|
164
|
+
if (unknown.length > 0)
|
|
165
|
+
throw new SurveyError(`The survey has no field ${unknown.join(", ")}: take the names from the survey's questions.`);
|
|
166
|
+
const page = await openSitePage(context, siteUrl, `mod/${type}/complete.php?id=${activityId}`);
|
|
167
|
+
try {
|
|
168
|
+
for (let p = 0; p < 50; p++) {
|
|
169
|
+
const here = new Set((await readSurveyPage(page, type, p)).flatMap((q) => q.fields.map((f) => f.name)));
|
|
170
|
+
const values = Object.fromEntries(Object.entries(responses).filter(([name]) => here.has(name)));
|
|
171
|
+
await setFields(page, values, layout.form);
|
|
172
|
+
const last = (await page.locator(`${layout.form} [name="${layout.next}"]`).count()) === 0;
|
|
173
|
+
const [response] = await Promise.all([page.waitForNavigation({ waitUntil: "domcontentloaded" }), page.click(`${layout.form} [name="${last ? layout.finish : layout.next}"]`)]);
|
|
174
|
+
if (response && response.status() >= 500) {
|
|
175
|
+
throw new SurveyError(`The site answered ${response.status()} ${last ? "on submitting" : "on the way"}; it may or may not have recorded the answers. Read the survey again before trying again.`);
|
|
176
|
+
}
|
|
177
|
+
// Moodle sends the page back when something's missing or wrong.
|
|
178
|
+
const problems = await page.evaluate((form) => {
|
|
179
|
+
const doc = globalThis.document;
|
|
180
|
+
const messages = Array.from(doc.querySelectorAll(`${form} [id^="id_error_"], #region-main .alert-danger`)).map((e) => {
|
|
181
|
+
// Without the notification's close button ("Dismiss this notification").
|
|
182
|
+
const copy = e.cloneNode(true);
|
|
183
|
+
for (const button of Array.from(copy.querySelectorAll("button, .close, .visually-hidden")))
|
|
184
|
+
button.remove();
|
|
185
|
+
const text = (copy.innerText ?? e.innerText).replace(/\s+/g, " ").trim();
|
|
186
|
+
return text ? `${e.id ? e.id.replace("id_error_", "") + ": " : ""}${text}` : "";
|
|
187
|
+
});
|
|
188
|
+
return [...new Set(messages.filter(Boolean))];
|
|
189
|
+
}, layout.form);
|
|
190
|
+
if (problems.length > 0)
|
|
191
|
+
throw new SurveyError(`Moodle didn't accept the answers on page ${p + 1}: ${problems.join("; ")}`);
|
|
192
|
+
if (last)
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
await page.close().catch(() => undefined);
|
|
198
|
+
}
|
|
199
|
+
return { changed: `Submitted ${type} ${activityId}.`, messages: (await surveyStatus(context, siteUrl, activityId, type)).messages };
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=surveys.js.map
|