Add LMS progress summary endpoint.
Expose a single learner endpoint that returns the nested LMS hierarchy with the same access-based progress percentages used across the existing content APIs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
56cc009579
commit
82de00b1e7
26
docs/docs.go
26
docs/docs.go
|
|
@ -3931,6 +3931,32 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/lms/progress-summary": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns the learner's nested LMS hierarchy with the same access progress data exposed on the individual program, course, module, and lesson APIs.",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"lms"
|
||||||
|
],
|
||||||
|
"summary": "Get my LMS progress summary",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/logs": {
|
"/api/v1/logs": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Fetches application logs from MongoDB with pagination, level filtering, and search",
|
"description": "Fetches application logs from MongoDB with pagination, level filtering, and search",
|
||||||
|
|
|
||||||
|
|
@ -3923,6 +3923,32 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/lms/progress-summary": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns the learner's nested LMS hierarchy with the same access progress data exposed on the individual program, course, module, and lesson APIs.",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"lms"
|
||||||
|
],
|
||||||
|
"summary": "Get my LMS progress summary",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/logs": {
|
"/api/v1/logs": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Fetches application logs from MongoDB with pagination, level filtering, and search",
|
"description": "Fetches application logs from MongoDB with pagination, level filtering, and search",
|
||||||
|
|
|
||||||
|
|
@ -5416,6 +5416,25 @@ paths:
|
||||||
summary: Get my LMS completion history
|
summary: Get my LMS completion history
|
||||||
tags:
|
tags:
|
||||||
- lms
|
- lms
|
||||||
|
/api/v1/lms/progress-summary:
|
||||||
|
get:
|
||||||
|
description: Returns the learner's nested LMS hierarchy with the same access
|
||||||
|
progress data exposed on the individual program, course, module, and lesson
|
||||||
|
APIs.
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.Response'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
summary: Get my LMS progress summary
|
||||||
|
tags:
|
||||||
|
- lms
|
||||||
/api/v1/logs:
|
/api/v1/logs:
|
||||||
get:
|
get:
|
||||||
description: Fetches application logs from MongoDB with pagination, level filtering,
|
description: Fetches application logs from MongoDB with pagination, level filtering,
|
||||||
|
|
|
||||||
38
internal/domain/lms_progress_summary.go
Normal file
38
internal/domain/lms_progress_summary.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
// LMSProgressSummary returns the learner's progress tree using the same access
|
||||||
|
// contract exposed by the LMS hierarchy endpoints.
|
||||||
|
type LMSProgressSummary struct {
|
||||||
|
Programs []LMSProgressSummaryProgram `json:"programs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LMSProgressSummaryProgram struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Access *LMSEntityAccess `json:"access,omitempty"`
|
||||||
|
Courses []LMSProgressSummaryCourse `json:"courses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LMSProgressSummaryCourse struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Access *LMSEntityAccess `json:"access,omitempty"`
|
||||||
|
Modules []LMSProgressSummaryModule `json:"modules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LMSProgressSummaryModule struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Access *LMSEntityAccess `json:"access,omitempty"`
|
||||||
|
Lessons []LMSProgressSummaryLesson `json:"lessons"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LMSProgressSummaryLesson struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ModuleID int64 `json:"module_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Access *LMSEntityAccess `json:"access,omitempty"`
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
|
@ -9,6 +10,8 @@ import (
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const lmsProgressSummaryPageSize int32 = 200
|
||||||
|
|
||||||
// GetMyLMSProgress godoc
|
// GetMyLMSProgress godoc
|
||||||
// @Summary Get my LMS completion history
|
// @Summary Get my LMS completion history
|
||||||
// @Description Returns practice-based completed lesson, module, course, and program IDs for the authenticated user (ordered by completion time, then id).
|
// @Description Returns practice-based completed lesson, module, course, and program IDs for the authenticated user (ordered by completion time, then id).
|
||||||
|
|
@ -34,6 +37,32 @@ func (h *Handler) GetMyLMSProgress(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMyLMSProgressSummary godoc
|
||||||
|
// @Summary Get my LMS progress summary
|
||||||
|
// @Description Returns the learner's nested LMS hierarchy with the same access progress data exposed on the individual program, course, module, and lesson APIs.
|
||||||
|
// @Tags lms
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/lms/progress-summary [get]
|
||||||
|
func (h *Handler) GetMyLMSProgressSummary(c *fiber.Ctx) error {
|
||||||
|
uid := c.Locals("user_id").(int64)
|
||||||
|
role := c.Locals("role").(domain.Role)
|
||||||
|
summary, err := h.buildLMSProgressSummary(c.Context(), role, uid, !h.canManageLessons(c))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to load learning progress summary",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "LMS progress summary retrieved successfully",
|
||||||
|
Data: summary,
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// AdminGetUserLMSLearningActivity godoc
|
// AdminGetUserLMSLearningActivity godoc
|
||||||
// @Summary Get a user's nested LMS learning activity (admin)
|
// @Summary Get a user's nested LMS learning activity (admin)
|
||||||
// @Description Returns programs, courses, modules, and lessons with completion details and completed practices. Only persisted completion signals are included (completed lessons, completed published practices, and rollup completion timestamps—not partial or in-progress attempts).
|
// @Description Returns programs, courses, modules, and lessons with completion details and completed practices. Only persisted completion signals are included (completed lessons, completed published practices, and rollup completion timestamps—not partial or in-progress attempts).
|
||||||
|
|
@ -140,3 +169,168 @@ func (h *Handler) AdminGetUserRecentActivity(c *fiber.Ctx) error {
|
||||||
StatusCode: fiber.StatusOK,
|
StatusCode: fiber.StatusOK,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) buildLMSProgressSummary(ctx context.Context, role domain.Role, userID int64, publishedOnly bool) (domain.LMSProgressSummary, error) {
|
||||||
|
programs, err := h.listAllPrograms(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return domain.LMSProgressSummary{}, err
|
||||||
|
}
|
||||||
|
summary := domain.LMSProgressSummary{
|
||||||
|
Programs: make([]domain.LMSProgressSummaryProgram, 0, len(programs)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range programs {
|
||||||
|
if err := h.lmsProgressSvc.ApplyAccessProgram(ctx, role, userID, &programs[i]); err != nil {
|
||||||
|
return domain.LMSProgressSummary{}, err
|
||||||
|
}
|
||||||
|
courses, err := h.listAllCoursesByProgram(ctx, programs[i].ID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.LMSProgressSummary{}, err
|
||||||
|
}
|
||||||
|
programSummary := domain.LMSProgressSummaryProgram{
|
||||||
|
ID: programs[i].ID,
|
||||||
|
Name: programs[i].Name,
|
||||||
|
Access: programs[i].Access,
|
||||||
|
Courses: make([]domain.LMSProgressSummaryCourse, 0, len(courses)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for j := range courses {
|
||||||
|
if err := h.lmsProgressSvc.ApplyAccessCourse(ctx, role, userID, &courses[j]); err != nil {
|
||||||
|
return domain.LMSProgressSummary{}, err
|
||||||
|
}
|
||||||
|
modules, err := h.listAllModulesByCourse(ctx, courses[j].ID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.LMSProgressSummary{}, err
|
||||||
|
}
|
||||||
|
courseSummary := domain.LMSProgressSummaryCourse{
|
||||||
|
ID: courses[j].ID,
|
||||||
|
ProgramID: courses[j].ProgramID,
|
||||||
|
Name: courses[j].Name,
|
||||||
|
Access: courses[j].Access,
|
||||||
|
Modules: make([]domain.LMSProgressSummaryModule, 0, len(modules)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range modules {
|
||||||
|
if err := h.lmsProgressSvc.ApplyAccessModule(ctx, role, userID, &modules[k]); err != nil {
|
||||||
|
return domain.LMSProgressSummary{}, err
|
||||||
|
}
|
||||||
|
lessons, err := h.listAllLessonsByModule(ctx, modules[k].ID, publishedOnly)
|
||||||
|
if err != nil {
|
||||||
|
return domain.LMSProgressSummary{}, err
|
||||||
|
}
|
||||||
|
moduleSummary := domain.LMSProgressSummaryModule{
|
||||||
|
ID: modules[k].ID,
|
||||||
|
ProgramID: modules[k].ProgramID,
|
||||||
|
CourseID: modules[k].CourseID,
|
||||||
|
Name: modules[k].Name,
|
||||||
|
Access: modules[k].Access,
|
||||||
|
Lessons: make([]domain.LMSProgressSummaryLesson, 0, len(lessons)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for m := range lessons {
|
||||||
|
if err := h.lmsProgressSvc.ApplyAccessLesson(ctx, role, userID, &lessons[m]); err != nil {
|
||||||
|
return domain.LMSProgressSummary{}, err
|
||||||
|
}
|
||||||
|
moduleSummary.Lessons = append(moduleSummary.Lessons, domain.LMSProgressSummaryLesson{
|
||||||
|
ID: lessons[m].ID,
|
||||||
|
ModuleID: lessons[m].ModuleID,
|
||||||
|
Title: lessons[m].Title,
|
||||||
|
Access: lessons[m].Access,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
courseSummary.Modules = append(courseSummary.Modules, moduleSummary)
|
||||||
|
}
|
||||||
|
|
||||||
|
programSummary.Courses = append(programSummary.Courses, courseSummary)
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.Programs = append(summary.Programs, programSummary)
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) listAllPrograms(ctx context.Context) ([]domain.Program, error) {
|
||||||
|
var (
|
||||||
|
all []domain.Program
|
||||||
|
offset int32
|
||||||
|
)
|
||||||
|
for {
|
||||||
|
items, total, err := h.programSvc.List(ctx, lmsProgressSummaryPageSize, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
all = append(all, items...)
|
||||||
|
if len(items) == 0 || int64(len(all)) >= total {
|
||||||
|
if all == nil {
|
||||||
|
return []domain.Program{}, nil
|
||||||
|
}
|
||||||
|
return all, nil
|
||||||
|
}
|
||||||
|
offset += lmsProgressSummaryPageSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) listAllCoursesByProgram(ctx context.Context, programID int64) ([]domain.Course, error) {
|
||||||
|
var (
|
||||||
|
all []domain.Course
|
||||||
|
offset int32
|
||||||
|
)
|
||||||
|
for {
|
||||||
|
items, total, err := h.courseSvc.ListByProgram(ctx, programID, lmsProgressSummaryPageSize, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
all = append(all, items...)
|
||||||
|
if len(items) == 0 || int64(len(all)) >= total {
|
||||||
|
if all == nil {
|
||||||
|
return []domain.Course{}, nil
|
||||||
|
}
|
||||||
|
return all, nil
|
||||||
|
}
|
||||||
|
offset += lmsProgressSummaryPageSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) listAllModulesByCourse(ctx context.Context, courseID int64) ([]domain.Module, error) {
|
||||||
|
var (
|
||||||
|
all []domain.Module
|
||||||
|
offset int32
|
||||||
|
)
|
||||||
|
for {
|
||||||
|
items, total, err := h.moduleSvc.ListByCourse(ctx, courseID, lmsProgressSummaryPageSize, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
all = append(all, items...)
|
||||||
|
if len(items) == 0 || int64(len(all)) >= total {
|
||||||
|
if all == nil {
|
||||||
|
return []domain.Module{}, nil
|
||||||
|
}
|
||||||
|
return all, nil
|
||||||
|
}
|
||||||
|
offset += lmsProgressSummaryPageSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) listAllLessonsByModule(ctx context.Context, moduleID int64, publishedOnly bool) ([]domain.Lesson, error) {
|
||||||
|
var (
|
||||||
|
all []domain.Lesson
|
||||||
|
offset int32
|
||||||
|
)
|
||||||
|
for {
|
||||||
|
items, total, err := h.lessonSvc.ListByModule(ctx, moduleID, publishedOnly, lmsProgressSummaryPageSize, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
all = append(all, items...)
|
||||||
|
if len(items) == 0 || int64(len(all)) >= total {
|
||||||
|
if all == nil {
|
||||||
|
return []domain.Lesson{}, nil
|
||||||
|
}
|
||||||
|
return all, nil
|
||||||
|
}
|
||||||
|
offset += lmsProgressSummaryPageSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Get("/programs", a.authMiddleware, a.RequirePermission("programs.list"), h.ListPrograms)
|
groupV1.Get("/programs", a.authMiddleware, a.RequirePermission("programs.list"), h.ListPrograms)
|
||||||
groupV1.Put("/programs/reorder", a.authMiddleware, a.RequirePermission("programs.reorder"), h.ReorderPrograms)
|
groupV1.Put("/programs/reorder", a.authMiddleware, a.RequirePermission("programs.reorder"), h.ReorderPrograms)
|
||||||
groupV1.Get("/lms/progress", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("lms.get_my_progress"), h.GetMyLMSProgress)
|
groupV1.Get("/lms/progress", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("lms.get_my_progress"), h.GetMyLMSProgress)
|
||||||
|
groupV1.Get("/lms/progress-summary", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("lms.get_my_progress"), h.GetMyLMSProgressSummary)
|
||||||
groupV1.Get("/programs/:id", a.authMiddleware, a.RequirePermission("programs.get"), h.GetProgram)
|
groupV1.Get("/programs/:id", a.authMiddleware, a.RequirePermission("programs.get"), h.GetProgram)
|
||||||
groupV1.Put("/programs/:id", a.authMiddleware, a.RequirePermission("programs.update"), h.UpdateProgram)
|
groupV1.Put("/programs/:id", a.authMiddleware, a.RequirePermission("programs.update"), h.UpdateProgram)
|
||||||
groupV1.Delete("/programs/:id", a.authMiddleware, a.RequirePermission("programs.delete"), h.DeleteProgram)
|
groupV1.Delete("/programs/:id", a.authMiddleware, a.RequirePermission("programs.delete"), h.DeleteProgram)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user