Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,45 @@ These are binding on this derivative work — see
- **Incident callouts** must be self-contained for learners who haven't read the
OWASP docs; incident references include hyperlinked sources.

## Progress and completion: what learners and the LMS see

- **In the course**: a header progress bar (percentage of pages opened, with a
marker per quiz), contents-page ticks that mean *opened* for content pages
and *submitted* for quizzes (an opened-but-unsubmitted quiz shows a
half-filled circle), and a **Your progress** panel on the contents page and
the Course complete page that lists the required pages still to visit, each
quiz result, the weighted score, and the exact status the LMS holds. See
[`docs/PROJECT_CONTEXT.md`](docs/PROJECT_CONTEXT.md#learner-facing-progress-status-panel-honest-quiz-ticks-progress-bar).
- **In the LMS**: SCORM 1.2 carries no progress figure, only
`cmi.core.lesson_status`. The course reports `incomplete` until every
required page (Welcome through the final quiz) has been visited, then
`passed` or `failed` against the 80% mark. Status and score are committed as
the learner goes, so the flip is visible as soon as it happens, but nothing
in between can move an LMS percentage.

### Moodle settings

If your LMS is Moodle (the one this course was first deployed to), three
settings on the SCORM activity matter:

| Setting | Recommended | Why |
| --- | --- | --- |
| **Grading method** | Learning objects (the default) | Grades by counting SCOs whose status is completed/passed. This package is one SCO, so the grade is 0% until the learner passes, then 100%. That is the "0% until the end" learners see, and it is honest. "Highest grade" / "Average grade" would show the running quiz score instead, which reads as 100% after one perfect quiz with 40 pages still to go. |
| **Completion tracking** | Require status: *Passed* (optionally a minimum score) | Makes Moodle's activity completion mean the same thing as the course's own pass logic. Do not use "view" or "grade" alone. |
| **Display attempt status** | Dashboard and entry page | Lets learners see the status and grade Moodle holds without opening the course. |

Moodle applies no mastery-score override here (the Xerte manifest declares no
`adlcp:masteryscore`), so the status the course sends is the status Moodle
stores. Administrators can see live per-question progress under the activity's
**Reports → Interactions report** and **Track details**, because the course
commits after every page and every answered question.

Two things that do **not** help with Moodle: exporting as SCORM 2004 (Moodle
stores `cmi.progress_measure` but never displays it as progress, and Xerte's
2004 tracker does not set it anyway) and splitting the course into one SCO per
theme (the only way to get a Moodle-native partial grade, at the cost of eight
Xerte projects, a hand-built manifest and no shared final score).

## Tests

The tracking tests build the SCORM package from `source/data.xml` with a real
Expand Down
18 changes: 14 additions & 4 deletions docs/COURSE_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ Model, version 1.2)** package with **LMS (Learning Management System)-reported s
- **Scoring**: a **Configure Scores** page sets project scoring; theme quizzes (25%) + final quiz
(75%) **all count** toward the LMS score. Final quiz pass mark **80%**.
- **SCORM 1.2** export → `cmi.core.score.raw`, `cmi.core.lesson_status`, `cmi.interactions.n`.
- **Progress indicators**: header progress bar (`progressBarType="header2"`, milestones on each
quiz, label "{x}% of pages viewed"); contents-page ticks mean *opened* for content pages and
*submitted* for quizzes; a **Your progress** panel (`xtStatusPanel`, filled by the root
script) on the contents page (`menuText`) and on the Course complete page shows required
pages done/still to visit, quiz results, weighted score and the status reported to the LMS.
- **Option order**: every quiz question sets `answerOrder="random"` so the answer options
shuffle on each attempt (Nottingham per-question "Answer Order" property).
- **Question types used** (all selection-based, no free text): Multiple Choice (single),
Expand Down Expand Up @@ -68,6 +73,9 @@ Real-world incident callout (where Appendix D has one) · Secure-coding mitigati
agentic/AI (Artificial Intelligence) apps (OWASP Agentic Top 10 2026), interleaved by theme.
- Structure: 7 themes, a quiz per theme, a final assessment. Time ~3–4 h.
- How scoring works: theme quizzes count 25%, final quiz counts 75%; final pass mark 80%.
- Tracking your progress: what the progress bar, the ticks (opened vs. submitted) and the
**Your progress** panel mean; every page from Welcome to the final quiz must be visited and
every quiz submitted for the course to count as complete.

### Page 2 — About, licensing, attributions & provenance [Content]
- CC BY-SA 4.0 (Creative Commons Attribution-ShareAlike 4.0) notice +
Expand Down Expand Up @@ -563,10 +571,12 @@ All framed with full descriptive names (no item codes as the question subject).
Security Misconfiguration = Classic; Cascading Failures = Agentic.

### Page 44 — Course complete [Content]
- A closing page so it is unambiguous that the training is over: congratulates the learner,
reminds them their result is recorded by the LMS (Learning Management System) and that the
80% pass mark / last-attempt rule applies, and tells them they may close the window. Plain
Text page type.
- A closing page so it is unambiguous that the training is over: opens with the **Your
progress** panel (required pages done, quiz results, weighted score, and the status the LMS
currently holds), congratulates the learner, explains that status and score are sent to the
LMS as they go (so an "incomplete" means going back to the contents page for the pages it
lists), that the 80% pass mark / last-attempt rule applies, and tells them they may close the
window. Plain Text page type.

### [Scores] — Configure Scores
- Project scoring enabled; theme quizzes (25%) + final quiz (75%) all count; final pass mark
Expand Down
72 changes: 72 additions & 0 deletions docs/PROJECT_CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,78 @@ limit is respected). Because `scoremode` is `last`, re-taking a quiz still
overwrites the remembered score, and a page never counted twice
(`findPage()` matches the first page-level entry per `page_nr`).

## Learner-facing progress (status panel, honest quiz ticks, progress bar)

**Symptom**: learners could not tell how far through the course they were or
whether they had done what completion requires. Two things misled them:

- the contents-page **tick means "opened"**, nothing more. `x_pageLoaded()`
sets `x_pageInfo[i].viewed = true` the moment a page finishes loading and
`XENITH.PAGEMENU.tickViewed()` removes the `notvisited` class from any viewed
page; tracking never feeds into it, so a quiz ticks before a single question
is answered;
- the **LMS shows 0% until the very end**. SCORM 1.2 has no progress field.
`getSuccessStatus()` returns `incomplete` until every one of the 43 tracked
pages has been *left* (after more than 100 ms), then `passed`/`failed`
against the pass mark. Moodle's default SCORM grading method, "Learning
objects", counts SCOs whose status is completed/passed, and this package is
one SCO, so the grade is 0 then 1. See "Moodle settings" in the README.

The LMS side is structural (see the README for why SCORM 2004 and multi-SCO
were rejected), so the fix is to make the course itself honest. Three parts,
all content-level (no engine patch):

1. **Status panel** (`source/scorm_progress.js`, part 5). Every element with
the class `xtStatusPanel` is filled with: required pages completed
(`state.completedPages` against `state.toCompletePages`), the names of the
pages still to visit (capped at 6), each quiz page's result or "not yet
submitted", the weighted score so far against `state.lo_passed`, and the
exact status `state.getSuccessStatus()` would report to the LMS, in plain
language. Two placeholders exist: the root `menuText` attribute (shown in
the text column of the contents page) and a `<div>` at the top of the
**Course complete** page. The panel is refreshed from `x_pageLoaded`,
`XENITH.PAGEMENU.tickViewed`, `XTExitPage`, `XTExitInteraction` and
`XTSetPageScore` (all wrapped). Outside an LMS it says tracking is not
active. `window.xtRefreshProgressUi()` re-renders it on demand.
2. **Quiz ticks mean "submitted"**. The engine never reads the page-level
interaction's `complete` field, so the `XTSetPageScore` wrapper (the quiz
results screen is its only caller) sets it to `true`, and it is persisted as
a sixth element of each `xtPages` row (`1`/`0`) and restored by `setVars`.
Records written before the flag existed have no `row[5]`; a non-zero score
can only come from the results screen, so it is treated as submitted. After
`tickViewed`, the tick of a quiz page that is viewed but not submitted is
swapped for `fa-adjust` (a half-filled circle) with the class `xtPending`
and the label "Opened, quiz not yet submitted"; it reverts on submission.
3. **Header progress bar** (Xerte's own, editor-supported): root attributes
`progressBarType="header2"` ("below titles"; `header1`, "above titles",
is inserted after the header logo icon, and this course has no logo, so
jQuery's `insertAfter` on an empty set silently builds nothing),
`progressSub="milestones"`,
`progressBarPercentage="true"`, `progressBarTxt="{x}% of pages viewed"`,
`progressBarSubLink="true"`, plus `milestone="true"` on each of the 8 quiz
pages. It is viewed-based like the ticks, hence the label wording.

The Welcome page explains the three indicators; the Course complete page no
longer claims the result "has been recorded" unconditionally and points at the
panel instead.

**Deliberately not changed**: what "required" means. All 43 tracked pages
(Welcome through the final quiz) still count, including About, Mapping,
Glossary and References. Exempting a page needs care: because of the
off-by-one described under "SCORM completion bug fix", `unmarkForCompletion`
on page *N* actually exempts page *N-1*; the clean route is for the root script
to rebuild `state.toCompletePages` from the live page list before
`XTInitialise`, and the tests' `TRACKED_PAGES` would follow. Also unchanged:
the engine's pass check is `getdScaledScore() > lo_passed / 100`, strictly
greater, so a score of exactly 80% reports `failed` (Moodle applies no mastery
override because the Xerte manifest declares no `adlcp:masteryscore`).

Tests: `test_status_panel_tells_the_learner_what_is_left`,
`test_quiz_tick_means_submitted_not_opened`,
`test_older_records_infer_submission_from_the_score`,
`test_header_progress_bar_counts_viewed_pages`, and the panel assertions at the
end of `test_full_walkthrough_reports_passed`.

## Build version stamping (`{{BUILD_VERSION}}` placeholder)

So an operator can tell **which version is running** in an LMS, the SCORM
Expand Down
Loading