Read the quizzes behind H5P elements
A quiz in Schulcloud is an H5P element, and a board hands over nothing but a
contentId — so a teacher's exercise was until now a line saying one exists.
The player shows a single question at a time, which makes it look like
something to step through or scrape. It is not:
`GET /api/v3/h5p-editor/params/{contentId}` returns the JSON the player is fed,
so one request holds every question, every option and which of them are
correct. (`play/{id}` is the same content plus the player's script lists: 74 kB
against 51 kB for the live quiz. Neither docs-json describes the service.)
get_h5p prints the exercise, and solutions=false keeps the options while
dropping the answers, so it can be used to ask the questions instead of
answering them. get_board names the exercise — title, question count, kinds —
rather than printing a bare id, and the crawl indexes its text, so a phrase
that exists only inside a quiz is now findable. That is the treatment pads
already get, for the same reason: it is course material and nothing else
surfaces it.
What varies is the shape inside `params`, which belongs to whichever H5P
library the teacher used. Modelled: MultiChoice, whose `behaviour.singleAnswer`
is the only honest source for "tick exactly one"; TrueFalse, whose `correct` is
the string "true"; the cloze libraries, which mark solutions inline as
`*answer:tip*`; SingleChoiceSet and Summary, which put the correct option first
and let the player shuffle; and Column. Anything else has its text harvested
and labelled unmodelled — an exercise reported as "0 questions" would be worse
than a clumsy rendering of one. The harvest skips the UI and l10n subtrees, or
a quiz reads as "Überprüfen, Wiederholen, Absenden".
Verified against this account's quiz, an H5P.QuestionSet of 20 MultiChoice
questions on a room's board: 239 tests, smoke 91/91 live-only and 93/93 with
the index.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -292,6 +292,9 @@ console.log('\n== file manager (Dateien) ==');
|
||||
}
|
||||
|
||||
console.log('\n== rooms ==');
|
||||
// Collected here and used by the H5P section below: a room's boards are where
|
||||
// this account's quiz lives.
|
||||
const roomBoardIds = [];
|
||||
// Rooms ("Räume") are a separate space from courses. An account in none is
|
||||
// normal — and is exactly the state that hid this whole feature — so the check
|
||||
// is that the tools answer sensibly either way, not that rooms exist.
|
||||
@@ -302,6 +305,7 @@ console.log('\n== rooms ==');
|
||||
if (roomId) {
|
||||
const room = await call('get_room', { roomId });
|
||||
check('get_room opens a room', !room.isError && /Room id:/.test(room.text), roomId);
|
||||
roomBoardIds.push(...[...room.text.matchAll(/\(`([0-9a-f]{24})`\)/g)].map((m) => m[1]).slice(0, 8));
|
||||
const roomBoardId = room.text.match(/### Boards[\s\S]*?\(`([0-9a-f]{24})`\)/)?.[1];
|
||||
if (roomBoardId) {
|
||||
const board = await call('get_board', { boardId: roomBoardId });
|
||||
@@ -314,6 +318,47 @@ console.log('\n== rooms ==');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n== H5P exercises ==');
|
||||
// A quiz is an H5P element on a board, and Schulcloud has nothing else like it.
|
||||
// Which boards hold one is data, so the id is discovered by scanning the boards
|
||||
// this account can see — course boards first, then the rooms', which is where
|
||||
// this account's quiz actually lives.
|
||||
{
|
||||
let contentId;
|
||||
let foundOn;
|
||||
const boardsToScan = [...boardIds, ...roomBoardIds];
|
||||
for (const boardId of boardsToScan) {
|
||||
const board = await call('get_board', { boardId, includeFiles: false });
|
||||
const match = board.text.match(/content `([0-9a-f]{24})` — all of it with get_h5p/);
|
||||
if (match) {
|
||||
contentId = match[1];
|
||||
foundOn = boardId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (contentId) {
|
||||
check('get_board names an H5P exercise and its question count', true, `board ${foundOn}, content ${contentId}`);
|
||||
|
||||
const full = await call('get_h5p', { contentId });
|
||||
check(
|
||||
'get_h5p returns every question at once, with the solutions marked',
|
||||
!full.isError && /^### 1\. /m.test(full.text) && /✔/.test(full.text),
|
||||
full.text.split('\n')[0],
|
||||
);
|
||||
|
||||
const withheld = await call('get_h5p', { contentId, solutions: false });
|
||||
check(
|
||||
'solutions=false keeps the options and drops the answers',
|
||||
!withheld.isError && /^### 1\. /m.test(withheld.text) && !/✔/.test(withheld.text) && /Solutions withheld/.test(withheld.text),
|
||||
withheld.text.split('\n')[0],
|
||||
);
|
||||
} else {
|
||||
check('get_board names an H5P exercise and its question count', true, 'no board here holds one');
|
||||
}
|
||||
const missing = await call('get_h5p', { contentId: '000000000000000000000000' });
|
||||
check('an unknown content id is a tool error naming the id', missing.isError && /404/.test(missing.text), missing.text.split('\n')[0]);
|
||||
}
|
||||
|
||||
console.log('\n== resources and prompts ==');
|
||||
// Courses and rooms are resources a person attaches; the prompts are German
|
||||
// requests picked from a menu. Both reuse the tools' reads, so what is checked
|
||||
|
||||
Reference in New Issue
Block a user