Add lesson detail retrieval endpoints.

Expose APIs to list lessons by submodule and fetch a single lesson by ID, including title, description, intro video URL, and question count.

Made-with: Cursor
This commit is contained in:
Yared Yemane 2026-04-16 02:42:21 -07:00
parent d686bdf8bd
commit 01914cb81e
4 changed files with 143 additions and 0 deletions

View File

@ -60,6 +60,25 @@ WHERE smp.sub_module_id = $1
AND qs.set_type = 'QUIZ' AND qs.set_type = 'QUIZ'
ORDER BY smp.display_order ASC, smp.id ASC; ORDER BY smp.display_order ASC, smp.id ASC;
-- name: GetSubModuleLessonByID :one
SELECT
smp.id,
smp.sub_module_id,
smp.question_set_id,
smp.intro_video_url,
smp.display_order,
smp.is_active,
qs.title,
qs.description,
qs.status,
qs.set_type,
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
FROM sub_module_lessons smp
JOIN question_sets qs ON qs.id = smp.question_set_id
WHERE smp.id = $1
AND smp.is_active = TRUE
AND qs.set_type = 'QUIZ';
-- name: GetSubModulePractices :many -- name: GetSubModulePractices :many
SELECT SELECT
smp.id, smp.id,

View File

@ -544,6 +544,59 @@ func (q *Queries) GetModulesByLevelID(ctx context.Context, levelID int64) ([]Mod
return items, nil return items, nil
} }
const GetSubModuleLessonByID = `-- name: GetSubModuleLessonByID :one
SELECT
smp.id,
smp.sub_module_id,
smp.question_set_id,
smp.intro_video_url,
smp.display_order,
smp.is_active,
qs.title,
qs.description,
qs.status,
qs.set_type,
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
FROM sub_module_lessons smp
JOIN question_sets qs ON qs.id = smp.question_set_id
WHERE smp.id = $1
AND smp.is_active = TRUE
AND qs.set_type = 'QUIZ'
`
type GetSubModuleLessonByIDRow struct {
ID int64 `json:"id"`
SubModuleID int64 `json:"sub_module_id"`
QuestionSetID int64 `json:"question_set_id"`
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
DisplayOrder int32 `json:"display_order"`
IsActive bool `json:"is_active"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
Status string `json:"status"`
SetType string `json:"set_type"`
QuestionCount int64 `json:"question_count"`
}
func (q *Queries) GetSubModuleLessonByID(ctx context.Context, id int64) (GetSubModuleLessonByIDRow, error) {
row := q.db.QueryRow(ctx, GetSubModuleLessonByID, id)
var i GetSubModuleLessonByIDRow
err := row.Scan(
&i.ID,
&i.SubModuleID,
&i.QuestionSetID,
&i.IntroVideoUrl,
&i.DisplayOrder,
&i.IsActive,
&i.Title,
&i.Description,
&i.Status,
&i.SetType,
&i.QuestionCount,
)
return i, err
}
const GetSubModuleLessons = `-- name: GetSubModuleLessons :many const GetSubModuleLessons = `-- name: GetSubModuleLessons :many
SELECT SELECT
smp.id, smp.id,

View File

@ -945,6 +945,75 @@ func (h *Handler) AttachSubModuleLesson(c *fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Lesson attached to sub-module", Data: attached}) return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Lesson attached to sub-module", Data: attached})
} }
// GetSubModuleLessons godoc
// @Summary Get lessons under sub-module
// @Description Returns all active lessons attached to a sub-module with question-set details
// @Tags course-management
// @Accept json
// @Produce json
// @Param subModuleId path int true "Sub-module ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/sub-modules/{subModuleId}/lessons [get]
func (h *Handler) GetSubModuleLessons(c *fiber.Ctx) error {
subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64)
if err != nil || subModuleID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid sub-module ID",
Error: "subModuleId must be a valid positive integer",
})
}
lessons, err := h.analyticsDB.GetSubModuleLessons(c.Context(), subModuleID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get sub-module lessons",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Sub-module lessons retrieved successfully",
Data: lessons,
})
}
// GetSubModuleLessonByID godoc
// @Summary Get lesson detail
// @Description Returns one active lesson detail by lesson ID
// @Tags course-management
// @Accept json
// @Produce json
// @Param lessonId path int true "Lesson ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/sub-module-lessons/{lessonId} [get]
func (h *Handler) GetSubModuleLessonByID(c *fiber.Ctx) error {
lessonID, err := strconv.ParseInt(c.Params("lessonId"), 10, 64)
if err != nil || lessonID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid lesson ID",
Error: "lessonId must be a valid positive integer",
})
}
lesson, err := h.analyticsDB.GetSubModuleLessonByID(c.Context(), lessonID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Lesson not found",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Lesson detail retrieved successfully",
Data: lesson,
})
}
// CreateSubModulePractice godoc // CreateSubModulePractice godoc
// @Summary Create practice under sub-module // @Summary Create practice under sub-module
// @Description Creates a sub-module practice with metadata and linked question set // @Description Creates a sub-module practice with metadata and linked question set

View File

@ -103,6 +103,8 @@ func (a *App) initAppRoutes() {
groupV1.Post("/course-management/sub-module-videos", a.authMiddleware, a.RequirePermission("videos.create"), h.CreateSubModuleVideo) groupV1.Post("/course-management/sub-module-videos", a.authMiddleware, a.RequirePermission("videos.create"), h.CreateSubModuleVideo)
groupV1.Put("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.update"), h.UpdateSubModuleVideo) groupV1.Put("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.update"), h.UpdateSubModuleVideo)
groupV1.Delete("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.delete"), h.DeleteSubModuleVideo) groupV1.Delete("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.delete"), h.DeleteSubModuleVideo)
groupV1.Get("/course-management/sub-modules/:subModuleId/lessons", a.authMiddleware, a.RequirePermission("question_sets.list"), h.GetSubModuleLessons)
groupV1.Get("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("question_sets.get"), h.GetSubModuleLessonByID)
groupV1.Post("/course-management/sub-module-lessons", a.authMiddleware, a.RequirePermission("question_sets.update"), h.AttachSubModuleLesson) groupV1.Post("/course-management/sub-module-lessons", a.authMiddleware, a.RequirePermission("question_sets.update"), h.AttachSubModuleLesson)
groupV1.Post("/course-management/sub-module-practices", a.authMiddleware, a.RequirePermission("question_sets.update"), h.CreateSubModulePractice) groupV1.Post("/course-management/sub-module-practices", a.authMiddleware, a.RequirePermission("question_sets.update"), h.CreateSubModulePractice)
groupV1.Put("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdatePractice) groupV1.Put("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdatePractice)