From 8dcbb43c39fa81ca77e30d8eaaff5004c80e1e32 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 14:47:24 +0000 Subject: [PATCH] fix(progress): light a quiz milestone on the progress bar only when the quiz is submitted Xerte marks a progress-bar milestone complete as soon as its page has been viewed, so the circle for each quiz lit up on opening the quiz, which contradicted the contents-page tick that now means "submitted". XENITH.PROGRESSBAR.update is wrapped so that after every engine update a quiz milestone is lit only when that quiz has reported its score (the same persisted flag the tick uses), and its label reads "Milestone: : Complete" or "Milestone: (Quiz not yet submitted)". Marker i is the i-th non-menu, non-standalone page with milestone="true", the order the engine builds them in. Also refreshed from the same hooks as the tick (page load, menu tick, page exit, quiz score), so it updates on submission and after a resume. Welcome page wording and docs updated; the header progress bar test now checks that opening a quiz does not light its marker, submitting it does, and the resumed session in the tick test checks the marker is restored. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01MfWFRfRw3sR98Vq48EKEYa --- README.md | 2 +- docs/COURSE_SPEC.md | 4 +-- docs/PROJECT_CONTEXT.md | 9 ++++- source/data.xml | 4 +-- source/preview.xml | 4 +-- source/scorm_progress.js | 67 +++++++++++++++++++++++++++++++++++- tests/test_scorm_tracking.py | 30 ++++++++++++++++ 7 files changed, 111 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 01933cd..2fe57a5 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ These are binding on this derivative work — see ## 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 + 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 diff --git a/docs/COURSE_SPEC.md b/docs/COURSE_SPEC.md index 2872ac0..09676eb 100644 --- a/docs/COURSE_SPEC.md +++ b/docs/COURSE_SPEC.md @@ -37,8 +37,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. diff --git a/docs/PROJECT_CONTEXT.md b/docs/PROJECT_CONTEXT.md index 9925bc7..16c5ecc 100644 --- a/docs/PROJECT_CONTEXT.md +++ b/docs/PROJECT_CONTEXT.md @@ -457,7 +457,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: : 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 diff --git a/source/data.xml b/source/data.xml index 8c87b74..3505e18 100644 --- a/source/data.xml +++ b/source/data.xml @@ -1,5 +1,5 @@ -Welcome to Secure code development +Welcome to Secure code development

This course is for engineers building software with agents and for engineers building agents. Whether you are designing agentic systems, integrating LLMs, or writing the services that agents call, secure code development matters.

@@ -23,7 +23,7 @@

Tracking your progress

-

The progress bar at the top of the page shows how many pages you have opened. On the contents page, a tick means you have opened a page, and a half-filled circle marks a quiz you have opened but not yet submitted. The Your progress panel on the contents page and on the final page lists the pages you still need to visit, your quiz results, and the status your LMS currently holds. Every page from this one through to the final quiz must be visited, and every quiz submitted, for the course to count as complete.

+

The progress bar at the top of the page shows how many pages you have opened; the circles on it light up as you submit each quiz. On the contents page, a tick means you have opened a page, and a half-filled circle marks a quiz you have opened but not yet submitted. The Your progress panel on the contents page and on the final page lists the pages you still need to visit, your quiz results, and the status your LMS currently holds. Every page from this one through to the final quiz must be visited, and every quiz submitted, for the course to count as complete.

]]>
Licensing

This course is licensed under CC BY-SA 4.0 (Creative Commons Attribution-ShareAlike 4.0). You are free to share and adapt the material, provided you give appropriate credit and share any derivatives under the same license. Full license: https://creativecommons.org/licenses/by-sa/4.0/.

diff --git a/source/preview.xml b/source/preview.xml index 8c87b74..3505e18 100644 --- a/source/preview.xml +++ b/source/preview.xml @@ -1,5 +1,5 @@ -Welcome to Secure code development +Welcome to Secure code development

This course is for engineers building software with agents and for engineers building agents. Whether you are designing agentic systems, integrating LLMs, or writing the services that agents call, secure code development matters.

@@ -23,7 +23,7 @@

Tracking your progress

-

The progress bar at the top of the page shows how many pages you have opened. On the contents page, a tick means you have opened a page, and a half-filled circle marks a quiz you have opened but not yet submitted. The Your progress panel on the contents page and on the final page lists the pages you still need to visit, your quiz results, and the status your LMS currently holds. Every page from this one through to the final quiz must be visited, and every quiz submitted, for the course to count as complete.

+

The progress bar at the top of the page shows how many pages you have opened; the circles on it light up as you submit each quiz. On the contents page, a tick means you have opened a page, and a half-filled circle marks a quiz you have opened but not yet submitted. The Your progress panel on the contents page and on the final page lists the pages you still need to visit, your quiz results, and the status your LMS currently holds. Every page from this one through to the final quiz must be visited, and every quiz submitted, for the course to count as complete.

]]>
Licensing

This course is licensed under CC BY-SA 4.0 (Creative Commons Attribution-ShareAlike 4.0). You are free to share and adapt the material, provided you give appropriate credit and share any derivatives under the same license. Full license: https://creativecommons.org/licenses/by-sa/4.0/.

diff --git a/source/scorm_progress.js b/source/scorm_progress.js index 952a4b5..21bae96 100644 --- a/source/scorm_progress.js +++ b/source/scorm_progress.js @@ -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. @@ -528,6 +531,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; } @@ -626,6 +677,7 @@ function refreshUi() { try { fixTicks(); } catch (e) { } + try { fixMilestones(); } catch (e) { } try { fillPanels(); } catch (e) { } } @@ -643,6 +695,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; } @@ -752,6 +816,7 @@ wrapState(); wrapTerminate(); wrapTickViewed(); + wrapProgressBar(); wrapPageLoaded(); wrapPageScore(); if (typeof window.addEventListener === 'function') { diff --git a/tests/test_scorm_tracking.py b/tests/test_scorm_tracking.py index 56d0e55..d30b715 100644 --- a/tests/test_scorm_tracking.py +++ b/tests/test_scorm_tracking.py @@ -379,6 +379,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) @@ -984,6 +996,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}" @@ -1018,6 +1031,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}"