Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ These are binding on this derivative work — see
Getting it right shows the usual confirmation and nothing more. See
[`docs/PROJECT_CONTEXT.md`](docs/PROJECT_CONTEXT.md#wrong-answer-help-hint--explanation-on-every-question).
- **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
marker per quiz that lights when that quiz is submitted), 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
Expand Down
4 changes: 2 additions & 2 deletions docs/COURSE_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ 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
- **Progress indicators**: header progress bar (`progressBarType="header2"`, a milestone on each
quiz that lights only when the quiz is submitted, 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.
Expand Down
9 changes: 8 additions & 1 deletion docs/PROJECT_CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,14 @@ all content-level (no engine patch):
`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.
pages. The bar itself is viewed-based, hence the label wording. The
engine also lights a milestone marker as soon as its page is *viewed*,
which made a quiz circle light on opening; `XENITH.PROGRESSBAR.update` is
wrapped so that after every engine update `fixMilestones()` lights a quiz
milestone only when that quiz is submitted (same flag as the tick), and
relabels it "Milestone: <quiz>: Complete" / "(Quiz not yet submitted)".
Marker *i* is the *i*-th non-menu, non-standalone page with
`milestone="true"`, the same order the engine builds them in.

The Welcome page explains the three indicators; the Course complete page no
longer claims the result "has been recorded" unconditionally and points at the
Expand Down
4 changes: 2 additions & 2 deletions source/data.xml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions source/preview.xml

Large diffs are not rendered by default.

67 changes: 66 additions & 1 deletion source/scorm_progress.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@
* 5. marks a quiz page as "submitted" (page interaction .complete, a field
* the engine never reads) when the quiz reports its score, persists that
* flag in xtPages, swaps the menu tick of an opened-but-unsubmitted quiz
* for a half-filled circle, and fills every element with the class
* for a half-filled circle, lights a quiz milestone on the header
* progress bar only once that quiz is submitted (the engine lights a
* milestone as soon as its page is viewed), and fills every element
* with the class
* xtStatusPanel (one is in the menu text, one on the Course complete
* page) with the required-page count, the pages still to do, each quiz
* result, the weighted score and the exact status the LMS holds.
Expand Down Expand Up @@ -541,6 +544,54 @@
});
}

/* ---- header progress bar: quiz milestones light on submission ---- */

var MILESTONE_PENDING = ' (Quiz not yet submitted)';
var MILESTONE_DONE = ': Complete';

/* the pages the engine gave a progress marker, in marker order */
function milestonePages() {
var out = [];
if (!pagesKnown()) { return out; }
if (typeof x_pages === 'undefined') { return out; }
if (!x_pages) { return out; }
x_pageInfo.forEach(function (info, idx) {
if (info.type === 'menu') { return; }
if (info.standalone === true) { return; }
if (!x_pages[idx]) { return; }
if (typeof x_pages[idx].getAttribute !== 'function') { return; }
if (x_pages[idx].getAttribute('milestone') === 'true') { out.push(idx); }
});
return out;
}

function setMarker(marker, done, label) {
var item = jQuery(marker);
if (done) { item.addClass('complete'); } else { item.removeClass('complete'); }
if (typeof item.button === 'function') {
try { item.button({ label: label }); } catch (e) { }
}
item.attr('title', label);
}

/* Runs after XENITH.PROGRESSBAR.update, which marks a milestone complete
as soon as its page has been viewed. A quiz milestone should mean the
quiz was submitted, like the menu tick. */
function fixMilestones() {
if (typeof jQuery !== 'function') { return; }
var markers = jQuery('#x_headerProgress .progressMarker');
if (markers.length === 0) { return; }
milestonePages().forEach(function (idx, i) {
if (pageType(idx) !== 'quiz') { return; }
var marker = markers.get(i);
if (!marker) { return; }
var title = jQuery(marker).data('title');
if (typeof title !== 'string') { return; }
if (quizSubmitted(idx)) { setMarker(marker, true, title + MILESTONE_DONE); }
else { setMarker(marker, false, title + MILESTONE_PENDING); }
});
}

function el(tag, text) {
var node = document.createElement(tag);
if (text !== undefined) { node.textContent = text; }
Expand Down Expand Up @@ -639,6 +690,7 @@

function refreshUi() {
try { fixTicks(); } catch (e) { }
try { fixMilestones(); } catch (e) { }
try { fillPanels(); } catch (e) { }
}

Expand All @@ -656,6 +708,18 @@
};
}

function wrapProgressBar() {
if (typeof XENITH === 'undefined') { return; }
if (!XENITH.PROGRESSBAR) { return; }
if (typeof XENITH.PROGRESSBAR.update !== 'function') { return; }
var orig = XENITH.PROGRESSBAR.update;
XENITH.PROGRESSBAR.update = function () {
var result = orig.apply(this, arguments);
try { fixMilestones(); } catch (e) { }
return result;
};
}

/* every page model calls x_pageLoaded once its content is in the DOM */
function wrapPageLoaded() {
if (typeof window.x_pageLoaded !== 'function') { return; }
Expand Down Expand Up @@ -850,6 +914,7 @@
wrapState();
wrapTerminate();
wrapTickViewed();
wrapProgressBar();
wrapPageLoaded();
wrapPageScore();
if (typeof window.addEventListener === 'function') {
Expand Down
30 changes: 30 additions & 0 deletions tests/test_scorm_tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,18 @@ def lms_status_shown(s: Session, page_nr: int) -> str:
"""


MILESTONE_MARKERS = """
() => window.jQuery('#x_headerProgress .progressMarker').map(function () {
return {complete: window.jQuery(this).hasClass('complete'), title: this.getAttribute('title')};
}).get()
"""


def milestone_markers(s: Session) -> list[dict]:
"""The header progress bar's milestone markers, in order: complete flag and title."""
return s.sco.evaluate(MILESTONE_MARKERS)


def pending_quiz_ticks(s: Session) -> list[int]:
"""Page indexes whose contents-page tick is the 'quiz not yet submitted' marker."""
return s.sco.evaluate(PENDING_QUIZ_TICKS)
Expand Down Expand Up @@ -1118,6 +1130,7 @@ def test_quiz_tick_means_submitted_not_opened(h: Harness):
s.goto(MENU_PAGE)
assert pending_quiz_ticks(s) == [], "the submitted flag was lost on resume"
assert "Theme 1 Quiz: 100%" in panel_text(s, MENU_PAGE), panel_text(s, MENU_PAGE)
assert milestone_markers(s)[0]["complete"] is True, "the quiz milestone was not lit on resume"
assert not s.page_errors, f"page errors: {s.page_errors}"


Expand Down Expand Up @@ -1227,6 +1240,23 @@ def test_header_progress_bar_counts_viewed_pages(h: Harness):
s.goto(FIRST_CONTENT_PAGE + 1)
label = s.sco.inner_text("#x_headerProgress .pbTxt")
assert not label.startswith("0%"), f"progress bar did not move: {label}"
# a quiz milestone lights when the quiz is submitted, not when it is opened
s.goto(THEME1_QUIZ)
s.goto(MENU_PAGE)
markers = milestone_markers(s)
assert [m["complete"] for m in markers] == [False] * len(QUIZ_PAGES), (
f"opening a quiz lit its milestone: {markers}"
)
assert "Theme 1 Quiz" in markers[0]["title"], markers[0]
assert "not yet submitted" in markers[0]["title"], markers[0]
s.goto(THEME1_QUIZ)
s.answer_quiz()
s.goto(MENU_PAGE)
markers = milestone_markers(s)
assert [m["complete"] for m in markers] == [True] + [False] * (len(QUIZ_PAGES) - 1), (
f"submitting Theme 1 Quiz should light only its milestone: {markers}"
)
assert markers[0]["title"].endswith("Complete"), markers[0]
assert not s.page_errors, f"page errors: {s.page_errors}"


Expand Down