Skip to content

feat(courses): add GET /api/admin/courses/:id/enrolled-users endpoint - #448

Merged
DeFiVC merged 2 commits into
ChainLearnOfficial:mainfrom
oluwagbemigaphilip:fix/chainlearn-357-342-341-340
Aug 31, 2026
Merged

feat(courses): add GET /api/admin/courses/:id/enrolled-users endpoint#448
DeFiVC merged 2 commits into
ChainLearnOfficial:mainfrom
oluwagbemigaphilip:fix/chainlearn-357-342-341-340

Conversation

@oluwagbemigaphilip

@oluwagbemigaphilip oluwagbemigaphilip commented Aug 31, 2026

Copy link
Copy Markdown

What changed

Adds GET /api/admin/courses/:id/enrolled-users — an admin-only, paginated endpoint returning a course's enrolled users with their quiz progress (userId, displayName, stellarAddress, enrolledAt, completedAt, quizCount, averageScore).

Follows the existing getCourseReviews pattern exactly: same pagination shape, same cache-aside (cacheGet/cacheSet) approach, 30s TTL per the issue's acceptance criteria. Route lives in admin-course.routes.ts alongside the other admin course routes, inheriting the file-level authGuard + adminGuard hooks — no new auth wiring needed.

A units note on averageScore

quiz_submissions.score is a raw correct-answer count (see QuizService.submitQuiz), not a percentage. Since quizzes in this codebase aren't fixed-length, averaging raw scores isn't comparable across quizzes with different question counts. I documented this explicitly on EnrolledUserSummary rather than silently computing a normalized percentage — that would need each submission's quiz question count folded into the aggregation, which felt like a bigger, separate decision than this issue asks for. Happy to add it if that's actually wanted.

How verified

Tested against real Postgres + Redis (started via docker run, schema pushed with drizzle-kit push) — not just type-checked:

  • 4 new tests in src/test/course-enrolled-users.test.ts: quizCount/averageScore correct for both a user with a submission and one without; pagination + ordering (most-recently-enrolled first); cache-aside behavior (a row added after the first call isn't reflected until the cache entry expires); NotFoundError for a nonexistent course.
  • eslint clean on all touched files.
  • npm run typecheck was not usable to verify this — it fails with pre-existing syntax errors in three unrelated files (admin-users.service.ts, course.routes.ts, user.routes.ts). Confirmed via git stash that these are present on main before this change, unrelated to it.

Closes #340
Closes #341
Closes #342
Closes #357

Admin-only, paginated endpoint returning a course's enrolled users with
per-user quiz progress: userId, displayName, stellarAddress, enrolledAt,
completedAt, quizCount, averageScore.

- Mirrors the existing getCourseReviews pagination/caching pattern in
  course.service.ts (count + paginated select, cache-aside via
  cacheGet/cacheSet, 30s TTL per the acceptance criteria).
- Route added to admin-course.routes.ts alongside the other admin course
  routes, inheriting the file-level authGuard + adminGuard hooks.
- quizCount/averageScore are computed from quiz_submissions joined to
  quizzes scoped to this course (quizzes.courseId), non-superseded
  submissions only, for just the userIds on the current page.
- averageScore is an average of quiz_submissions.score, which is a raw
  correct-answer count (see QuizService.submitQuiz), not a percentage —
  documented on EnrolledUserSummary since quizzes aren't fixed-length in
  this codebase, so it isn't comparable across quizzes with different
  question counts. Computing a normalized percentage average would need
  each submission's quiz question count folded into the aggregation,
  which is a larger change than this issue asks for; noted as a
  possible follow-up rather than silently approximated here.

Tested against real Postgres + Redis (docker): 4 new tests in
src/test/course-enrolled-users.test.ts — quizCount/averageScore correct
for both a user with a submission and one without, pagination + ordering
(most-recently-enrolled first), cache-aside behavior (a row added after
the first call isn't reflected until the cache entry expires), and
NotFoundError for a nonexistent course. eslint clean on all touched
files. `npm run typecheck` was not usable to verify this — it fails with
pre-existing syntax errors in three unrelated files, confirmed present
on main before this change (via git stash).

Closes ChainLearnOfficial#340
@drips-wave

drips-wave Bot commented Aug 31, 2026

Copy link
Copy Markdown

@oluwagbemigaphilip 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

@DeFiVC
DeFiVC merged commit 5695a50 into ChainLearnOfficial:main Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants