Skip to content

feat(progress-tracker): prerequisites, learner stats, completion percentage, quiz retakes - #364

Merged
DeFiVC merged 5 commits into
ChainLearnOfficial:mainfrom
Chidimj:feat/234-quiz-retake
Aug 30, 2026
Merged

feat(progress-tracker): prerequisites, learner stats, completion percentage, quiz retakes#364
DeFiVC merged 5 commits into
ChainLearnOfficial:mainfrom
Chidimj:feat/234-quiz-retake

Conversation

@Chidimj

@Chidimj Chidimj commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Four progress-tracker features, one commit each. Rationale is in the commit messages.

  • set_prerequisites / get_prerequisites; enroll requires every prerequisite course to be credential-eligible for that learner. create_course is unchanged — new courses start with no prerequisites.
  • get_learner_stats returns enrolled/completed counts, quizzes submitted, summed and averaged score, and rewards earned at BASE_REWARD_PER_POINT. enroll now writes a per-learner LearnerCourses index, also exposed as get_learner_courses.
  • get_completion_percentage returns the stored overall_progress alone — one read, no recomputation, no state change.
  • retake_quiz replaces a quiz result in place and shifts total_quiz_score by the delta, so quizzes_submitted is unchanged. New score must be strictly higher. Emits quiz_retaken, plus credential_eligible when a retake crosses the threshold.

Closes #231
Closes #232
Closes #233
Closes #234

One unrelated fix

.github/workflows/ci.yml used hashFiles("…") with double quotes, which is invalid in a GitHub expression. Every run on main failed at parse time with zero jobs scheduled, so no build, test, clippy, fmt or audit has been running on any PR. Fixed here so this branch is actually checked.

Locally: cargo test --workspace passes (77 progress-tracker unit tests), cargo clippy -p progress-tracker --all-targets -- -D warnings is clean, and all three contracts build for wasm32-unknown-unknown.

GitHub expressions only accept single-quoted string literals. The cache
key used double quotes, so every workflow run failed at parse time with
zero jobs scheduled -- no build, test, clippy, fmt or audit step ever
executed on main or on any pull request.
Courses that build on earlier material can now declare the courses a
learner must finish first, and enrollment refuses learners who have not
finished them.

`Course` gains a `prerequisites: Vec<Symbol>` field, empty for every
course created through `create_course`, so existing courses and callers
are unaffected. `set_prerequisites` (admin only) configures the list and
replaces it wholesale; passing an empty list clears the requirement.
It rejects unknown courses, self-references and duplicates so a course
can never be made permanently un-enrollable by a typo.

The check lives in `enroll_checked`, so it applies to both `enroll` and
the content-hash-verifying path. It requires the learner's stored
`ProgressInfo` for each prerequisite to be credential-eligible -- the
contract's existing definition of having finished a course (all modules
completed, all quizzes submitted, average at or above
MIN_CREDENTIAL_SCORE). A learner with no enrollment at all in a
prerequisite is treated the same as an unfinished one.

Prerequisites are queryable both through the dedicated
`get_prerequisites` read and through the `Course` returned by
`get_course`, and `set_prerequisites` emits a `prerequisites_set` event
so indexers do not have to poll for changes.

Closes ChainLearnOfficial#231
Dashboards had to call get_progress once per course and add the numbers
up client-side, and had no way to discover which courses a learner is
even enrolled in. `get_learner_stats(learner)` now returns every
aggregate in one call.

`LearnerStats` carries courses enrolled, courses completed (courses the
learner qualifies for a credential in), total quizzes submitted, the
summed quiz score, the floored average score across all courses, and the
reward tokens those scores are worth at BASE_REWARD_PER_POINT -- the
same rate the token contract mints at in claim_reward.

Aggregation needs to know which courses to read, so enrollment now
appends the course to a per-learner `LearnerCourses` index and the
aggregate walks that list instead of scanning every course in the
contract. The index is also exposed directly as `get_learner_courses`.

The average divides by quizzes submitted rather than by courses, so an
enrolled-but-untouched course does not drag it down, and a learner with
no enrollments gets an all-zero result instead of a panic, so callers
can render a new learner without a special case.

Closes ChainLearnOfficial#232
Frontends that only draw a progress bar had to call get_progress and
pull one field out of the returned ProgressInfo, paying to deserialize
the whole struct.

`get_completion_percentage(learner, course_id)` returns the stored
`overall_progress` on its own. The percentage is already recomputed on
every write that can change it, so the read is a single storage lookup
with no recomputation and no state change.

Closes ChainLearnOfficial#233
Quiz scores were final on first submission: submit_quiz_score rejects a
repeat, so a learner who under-performed could never raise their course
average or reach credential eligibility.

`retake_quiz(learner, course_id, quiz_id, new_score)` replaces the
stored QuizResult in place and moves `total_quiz_score` by the
difference between old and new, leaving `quizzes_submitted` alone -- so
the quiz stays counted exactly once and the average's divisor does not
drift. Overall progress and credential eligibility are recomputed from
the updated aggregates, and a retake that pushes a learner over
MIN_CREDENTIAL_SCORE emits `credential_eligible` the same way
complete_module and submit_quiz_score do (ChainLearnOfficial#96).

The new score must be strictly higher than the recorded one, so a retake
can only move the average up and an old submission cannot be replayed to
undo an improvement. A quiz that was never submitted is rejected --
first attempts still go through submit_quiz_score.

Every retake emits `quiz_retaken` carrying both the previous and new
score so indexers can follow the change without diffing state.

Closes ChainLearnOfficial#234
@Chidimj
Chidimj force-pushed the feat/234-quiz-retake branch from 98f4eb8 to 49aff20 Compare August 30, 2026 00:00
@drips-wave

drips-wave Bot commented Aug 30, 2026

Copy link
Copy Markdown

@Chidimj Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

. Add quiz retake support . Add course completion percentage query . Add learner statistics aggregation . Add course prerequisites

2 participants