Implement enrollment and progress tracking so org managers can assign courses to users and users can track their learning progress.
class UserProgress(SQLModel, table=True):
__tablename__ = "user_progress"
user_id: str = Field(primary_key=True) # keycloak_id
course_id: str = Field(primary_key=True) # MongoDB course _id (str)
progress_data: dict = Field(default={}, sa_column=Column(JSONB))
completed_sections: list[str] = Field(default=[], sa_column=Column(ARRAY(String)))
total_completed_tasks: int = Field(default=0)
is_certified: bool = Field(default=False)
deadline: datetime | None = Field(default=None)
expired: bool = Field(default=False)
updated_at: datetime = Field(default_factory=datetime.utcnow)- Export from api/src/models/init.py
Add 1 new endpoint using the existing OAuth2 pattern:
| Method | Path | Role | Description |
|---|---|---|---|
POST |
/{realm}/users/{user_id}/enroll |
ORG_MANAGER | Assign one or more courses to a user — creates a UserProgress row per course |
Request body: { course_ids: list[str], deadline?: datetime }
Default deadline: 30 days from enrollment date.
| Method | Path | Role | Description |
|---|---|---|---|
GET |
/users/{user_id}/progress |
ORG_MANAGER, DEFAULT_USER | All course progress for a user |
GET |
/users/{user_id}/progress/{course_id} |
ORG_MANAGER, DEFAULT_USER | Single course progress |
PUT |
/users/{user_id}/progress/{course_id} |
DEFAULT_USER | Update progress (add completed task or section); auto-certify when all sections done |
POST |
/users/{user_id}/progress/{course_id}/expire |
ORG_MANAGER | Mark course as expired |
Service functions called by the router:
get_all_progress(user_id, session)get_course_progress(user_id, course_id, session)update_progress(user_id, course_id, section_id, task_id, session)— adds task toprogress_data[section_id], and if all tasks in that section are done adds section tocompleted_sections. Recalculatestotal_completed_tasks. If all sections complete, setsis_certified=True.mark_expired(user_id, course_id, session)
Add to userLinks (gated to ORG_MANAGER, feature lms, group "Learning"):
{ href: '/courses/manage', label: 'Courses', icon: BookOpen }
{ href: '/courses/assign', label: 'Assign', icon: GraduationCap }Typed functions for enrollUser, unenrollUser, getUserProgress, getCourseProgress, updateProgress.
Renders the existing <CourseList /> component (from content-manager) but with showProgress={false} and no Edit/Delete controls visible.
A new page with a 3-step stepper:
- Select Courses — show the course grid; user picks one or more courses.
- Select Recipients — pick individual users or a user group (fetch from
/{realm}/users). - Set Deadline — date picker defaulting to +30 days. Submit calls
POST /{realm}/users/{user_id}/enrollwith all selectedcourse_idsfor each selected user.
- Fetch enrolled courses via
GET /users/{me}/progress→ extractcourse_idlist → fetch each course fromGET /courses/{id}. - Show progress badge on each CourseCard using
progressfromUserProgress.total_completed_tasks / (total tasks in course). - Keep existing
UniversalFiltersfor search/category/difficulty.
- Fetch course from
GET /courses/{courseId}. - Fetch full module objects.
- Fetch progress from
GET /users/{me}/progress/{courseId}. - Render ModuleCard for each module, with a completion percentage derived from
progress_data. - Completed modules go to the bottom and are rendered grey/dimmed.
- Fetch module from
GET /modules/{moduleId}. - Fetch user progress from
GET /users/{me}/progress/{courseId}. - After each task completion, call
PUT /users/{me}/progress/{courseId}with{ section_id, task_id }. - Mark the task as visually completed immediately (optimistic update).