feat(progress-tracker): prerequisites, learner stats, completion percentage, quiz retakes - #364
Merged
Merged
Conversation
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
force-pushed
the
feat/234-quiz-retake
branch
from
August 30, 2026 00:00
98f4eb8 to
49aff20
Compare
|
@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! 🚀 |
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four progress-tracker features, one commit each. Rationale is in the commit messages.
set_prerequisites/get_prerequisites;enrollrequires every prerequisite course to be credential-eligible for that learner.create_courseis unchanged — new courses start with no prerequisites.get_learner_statsreturns enrolled/completed counts, quizzes submitted, summed and averaged score, and rewards earned atBASE_REWARD_PER_POINT.enrollnow writes a per-learnerLearnerCoursesindex, also exposed asget_learner_courses.get_completion_percentagereturns the storedoverall_progressalone — one read, no recomputation, no state change.retake_quizreplaces a quiz result in place and shiftstotal_quiz_scoreby the delta, soquizzes_submittedis unchanged. New score must be strictly higher. Emitsquiz_retaken, pluscredential_eligiblewhen a retake crosses the threshold.Closes #231
Closes #232
Closes #233
Closes #234
One unrelated fix
.github/workflows/ci.ymlusedhashFiles("…")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 --workspacepasses (77 progress-tracker unit tests),cargo clippy -p progress-tracker --all-targets -- -D warningsis clean, and all three contracts build forwasm32-unknown-unknown.