add legacy learning-path GET endpoint for flows compatibility

Expose GET /course-management/courses/:courseId/learning-path and build response from unified hierarchy tables so first-time Flows tab loads no longer fail with Cannot GET.

Made-with: Cursor
This commit is contained in:
Yared Yemane 2026-04-14 10:05:53 -07:00
parent 06d86c9098
commit a9c6966820
2 changed files with 128 additions and 0 deletions

View File

@ -157,6 +157,14 @@ func intOrNil(v *int32) interface{} {
return *v return *v
} }
func textPtr(v pgtype.Text) *string {
if !v.Valid {
return nil
}
s := v.String
return &s
}
// ListCourseCategories godoc // ListCourseCategories godoc
// @Summary List course categories // @Summary List course categories
// @Description Legacy-compatible endpoint for listing course categories // @Description Legacy-compatible endpoint for listing course categories
@ -544,6 +552,125 @@ func (h *Handler) UnifiedHierarchyByCourse(c *fiber.Ctx) error {
return c.JSON(domain.Response{Message: "Course hierarchy retrieved successfully", Data: rows}) return c.JSON(domain.Response{Message: "Course hierarchy retrieved successfully", Data: rows})
} }
// CourseLearningPath godoc
// @Summary Get course learning path
// @Description Legacy-compatible endpoint for course learning path
// @Tags course-management
// @Produce json
// @Param courseId path int true "Course ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/courses/{courseId}/learning-path [get]
func (h *Handler) CourseLearningPath(c *fiber.Ctx) error {
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
if err != nil || courseID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid course ID", Error: "courseId must be a positive integer"})
}
course, err := h.analyticsDB.GetCourseByID(c.Context(), courseID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course", Error: err.Error()})
}
category, err := h.analyticsDB.GetCourseCategoryByID(c.Context(), course.CategoryID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course category", Error: err.Error()})
}
rows, err := h.analyticsDB.GetFullHierarchyByCourseID(c.Context(), courseID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course learning path", Error: err.Error()})
}
subCourseByID := map[int64]*domain.LearningPathSubCourse{}
subCourseOrder := make([]int64, 0)
for _, row := range rows {
if !row.SubModuleID.Valid {
continue
}
subModuleID := row.SubModuleID.Int64
if _, exists := subCourseByID[subModuleID]; exists {
continue
}
title := ""
if row.SubModuleTitle.Valid {
title = row.SubModuleTitle.String
}
level := ""
if row.CefrLevel.Valid {
level = row.CefrLevel.String
}
subCourseByID[subModuleID] = &domain.LearningPathSubCourse{
ID: subModuleID,
Title: title,
DisplayOrder: int32(len(subCourseOrder)),
Level: level,
SubLevel: level,
PrerequisiteCount: 0,
Prerequisites: []domain.LearningPathPrerequisite{},
Videos: []domain.LearningPathVideo{},
Practices: []domain.LearningPathPractice{},
}
subCourseOrder = append(subCourseOrder, subModuleID)
}
for _, subModuleID := range subCourseOrder {
node := subCourseByID[subModuleID]
videos, videoErr := h.analyticsDB.GetSubModuleVideos(c.Context(), subModuleID)
if videoErr != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-module videos", Error: videoErr.Error()})
}
for _, v := range videos {
node.Videos = append(node.Videos, domain.LearningPathVideo{
ID: v.ID,
Title: v.Title,
Description: textPtr(v.Description),
VideoURL: v.VideoUrl,
Duration: int32(v.Duration.Int32),
Resolution: textPtr(v.Resolution),
DisplayOrder: v.DisplayOrder,
VimeoID: textPtr(v.VimeoID),
VimeoEmbedURL: textPtr(v.VimeoEmbedUrl),
VideoHostProvider: textPtr(v.VideoHostProvider),
})
}
node.VideoCount = int64(len(node.Videos))
practices, practiceErr := h.analyticsDB.GetSubModulePractices(c.Context(), subModuleID)
if practiceErr != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-module practices", Error: practiceErr.Error()})
}
for _, p := range practices {
node.Practices = append(node.Practices, domain.LearningPathPractice{
ID: p.ID,
Title: p.Title,
Description: textPtr(p.Description),
Status: p.Status,
IntroVideoURL: textPtr(p.IntroVideoUrl),
QuestionCount: p.QuestionCount,
})
}
node.PracticeCount = int64(len(node.Practices))
}
subCourses := make([]domain.LearningPathSubCourse, 0, len(subCourseOrder))
for _, id := range subCourseOrder {
subCourses = append(subCourses, *subCourseByID[id])
}
path := domain.LearningPath{
CourseID: course.ID,
CourseTitle: course.Title,
Description: textPtr(course.Description),
Thumbnail: textPtr(course.Thumbnail),
IntroVideoURL: textPtr(course.IntroVideoUrl),
CategoryID: category.ID,
CategoryName: category.Name,
SubCourses: subCourses,
}
return c.JSON(domain.Response{Message: "Course learning path retrieved successfully", Data: path})
}
func isMissingCourseSubCategoryTableErr(err error) bool { func isMissingCourseSubCategoryTableErr(err error) bool {
if err == nil { if err == nil {
return false return false

View File

@ -87,6 +87,7 @@ func (a *App) initAppRoutes() {
groupV1.Put("/course-management/courses/:courseId", a.authMiddleware, a.RequirePermission("courses.update"), h.UpdateCourse) groupV1.Put("/course-management/courses/:courseId", a.authMiddleware, a.RequirePermission("courses.update"), h.UpdateCourse)
groupV1.Delete("/course-management/courses/:courseId", a.authMiddleware, a.RequirePermission("courses.delete"), h.DeleteCourse) groupV1.Delete("/course-management/courses/:courseId", a.authMiddleware, a.RequirePermission("courses.delete"), h.DeleteCourse)
groupV1.Post("/course-management/courses/:courseId/thumbnail", a.authMiddleware, a.RequirePermission("courses.upload_thumbnail"), h.UpdateCourseThumbnail) groupV1.Post("/course-management/courses/:courseId/thumbnail", a.authMiddleware, a.RequirePermission("courses.upload_thumbnail"), h.UpdateCourseThumbnail)
groupV1.Get("/course-management/courses/:courseId/learning-path", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.CourseLearningPath)
groupV1.Get("/course-management/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchy) groupV1.Get("/course-management/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchy)
groupV1.Get("/course-management/human-language/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchy) groupV1.Get("/course-management/human-language/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchy)
groupV1.Get("/course-management/courses/:courseId/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchyByCourse) groupV1.Get("/course-management/courses/:courseId/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchyByCourse)