add lesson and subcategory retrieval/update endpoints
Introduce dedicated APIs for submodule lesson detail/update and subcategory listing (including Human Language), with SQL/query wiring and handler routing updates. Made-with: Cursor
This commit is contained in:
parent
01914cb81e
commit
343ce470cc
|
|
@ -128,6 +128,44 @@ INSERT INTO course_sub_categories (
|
|||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetCourseSubCategories :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
csc.id,
|
||||
csc.category_id,
|
||||
cc.name AS category_name,
|
||||
csc.name,
|
||||
csc.description,
|
||||
csc.display_order,
|
||||
csc.is_active,
|
||||
csc.created_at
|
||||
FROM course_sub_categories csc
|
||||
JOIN course_categories cc ON cc.id = csc.category_id
|
||||
WHERE csc.is_active = TRUE
|
||||
ORDER BY csc.display_order ASC, csc.id ASC
|
||||
LIMIT sqlc.narg('limit')::INT
|
||||
OFFSET sqlc.narg('offset')::INT;
|
||||
|
||||
-- name: GetHumanLanguageCourseSubCategories :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
csc.id,
|
||||
csc.category_id,
|
||||
cc.name AS category_name,
|
||||
csc.name,
|
||||
csc.description,
|
||||
csc.display_order,
|
||||
csc.is_active,
|
||||
csc.created_at
|
||||
FROM course_sub_categories csc
|
||||
JOIN course_categories cc ON cc.id = csc.category_id
|
||||
WHERE csc.is_active = TRUE
|
||||
AND cc.is_active = TRUE
|
||||
AND lower(trim(cc.name)) = 'human language'
|
||||
ORDER BY csc.display_order ASC, csc.id ASC
|
||||
LIMIT sqlc.narg('limit')::INT
|
||||
OFFSET sqlc.narg('offset')::INT;
|
||||
|
||||
-- name: CreateLevel :one
|
||||
INSERT INTO levels (
|
||||
course_id,
|
||||
|
|
@ -200,6 +238,17 @@ INSERT INTO sub_module_lessons (
|
|||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateSubModuleLesson :one
|
||||
UPDATE sub_module_lessons
|
||||
SET
|
||||
sub_module_id = $1,
|
||||
question_set_id = $2,
|
||||
intro_video_url = $3,
|
||||
display_order = $4,
|
||||
is_active = $5
|
||||
WHERE id = $6
|
||||
RETURNING *;
|
||||
|
||||
-- name: CreateSubModulePractice :one
|
||||
INSERT INTO sub_module_practices (
|
||||
sub_module_id,
|
||||
|
|
|
|||
|
|
@ -364,6 +364,72 @@ func (q *Queries) CreateSubModuleVideo(ctx context.Context, arg CreateSubModuleV
|
|||
return i, err
|
||||
}
|
||||
|
||||
const GetCourseSubCategories = `-- name: GetCourseSubCategories :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
csc.id,
|
||||
csc.category_id,
|
||||
cc.name AS category_name,
|
||||
csc.name,
|
||||
csc.description,
|
||||
csc.display_order,
|
||||
csc.is_active,
|
||||
csc.created_at
|
||||
FROM course_sub_categories csc
|
||||
JOIN course_categories cc ON cc.id = csc.category_id
|
||||
WHERE csc.is_active = TRUE
|
||||
ORDER BY csc.display_order ASC, csc.id ASC
|
||||
LIMIT $2::INT
|
||||
OFFSET $1::INT
|
||||
`
|
||||
|
||||
type GetCourseSubCategoriesParams struct {
|
||||
Offset pgtype.Int4 `json:"offset"`
|
||||
Limit pgtype.Int4 `json:"limit"`
|
||||
}
|
||||
|
||||
type GetCourseSubCategoriesRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetCourseSubCategories(ctx context.Context, arg GetCourseSubCategoriesParams) ([]GetCourseSubCategoriesRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetCourseSubCategories, arg.Offset, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetCourseSubCategoriesRow
|
||||
for rows.Next() {
|
||||
var i GetCourseSubCategoriesRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.CategoryID,
|
||||
&i.CategoryName,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetCoursesWithHierarchy = `-- name: GetCoursesWithHierarchy :many
|
||||
SELECT
|
||||
cc.id AS category_id,
|
||||
|
|
@ -473,6 +539,74 @@ func (q *Queries) GetFullHierarchyByCourseID(ctx context.Context, id int64) ([]G
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const GetHumanLanguageCourseSubCategories = `-- name: GetHumanLanguageCourseSubCategories :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
csc.id,
|
||||
csc.category_id,
|
||||
cc.name AS category_name,
|
||||
csc.name,
|
||||
csc.description,
|
||||
csc.display_order,
|
||||
csc.is_active,
|
||||
csc.created_at
|
||||
FROM course_sub_categories csc
|
||||
JOIN course_categories cc ON cc.id = csc.category_id
|
||||
WHERE csc.is_active = TRUE
|
||||
AND cc.is_active = TRUE
|
||||
AND lower(trim(cc.name)) = 'human language'
|
||||
ORDER BY csc.display_order ASC, csc.id ASC
|
||||
LIMIT $2::INT
|
||||
OFFSET $1::INT
|
||||
`
|
||||
|
||||
type GetHumanLanguageCourseSubCategoriesParams struct {
|
||||
Offset pgtype.Int4 `json:"offset"`
|
||||
Limit pgtype.Int4 `json:"limit"`
|
||||
}
|
||||
|
||||
type GetHumanLanguageCourseSubCategoriesRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetHumanLanguageCourseSubCategories(ctx context.Context, arg GetHumanLanguageCourseSubCategoriesParams) ([]GetHumanLanguageCourseSubCategoriesRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetHumanLanguageCourseSubCategories, arg.Offset, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetHumanLanguageCourseSubCategoriesRow
|
||||
for rows.Next() {
|
||||
var i GetHumanLanguageCourseSubCategoriesRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.CategoryID,
|
||||
&i.CategoryName,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetLevelsByCourseID = `-- name: GetLevelsByCourseID :many
|
||||
SELECT id, course_id, cefr_level, display_order, is_active, created_at
|
||||
FROM levels
|
||||
|
|
@ -819,3 +953,46 @@ func (q *Queries) GetSubModulesByModuleID(ctx context.Context, moduleID int64) (
|
|||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const UpdateSubModuleLesson = `-- name: UpdateSubModuleLesson :one
|
||||
UPDATE sub_module_lessons
|
||||
SET
|
||||
sub_module_id = $1,
|
||||
question_set_id = $2,
|
||||
intro_video_url = $3,
|
||||
display_order = $4,
|
||||
is_active = $5
|
||||
WHERE id = $6
|
||||
RETURNING id, sub_module_id, question_set_id, intro_video_url, display_order, is_active, created_at
|
||||
`
|
||||
|
||||
type UpdateSubModuleLessonParams struct {
|
||||
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"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateSubModuleLesson(ctx context.Context, arg UpdateSubModuleLessonParams) (SubModuleLesson, error) {
|
||||
row := q.db.QueryRow(ctx, UpdateSubModuleLesson,
|
||||
arg.SubModuleID,
|
||||
arg.QuestionSetID,
|
||||
arg.IntroVideoUrl,
|
||||
arg.DisplayOrder,
|
||||
arg.IsActive,
|
||||
arg.ID,
|
||||
)
|
||||
var i SubModuleLesson
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubModuleID,
|
||||
&i.QuestionSetID,
|
||||
&i.IntroVideoUrl,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,29 @@ type attachSubModuleLessonReq struct {
|
|||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type updateLessonQuestionReq struct {
|
||||
QuestionID int64 `json:"question_id"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
}
|
||||
|
||||
type updateSubModuleLessonReq struct {
|
||||
SubModuleID *int64 `json:"sub_module_id"`
|
||||
QuestionSetID *int64 `json:"question_set_id"`
|
||||
IntroVideoURL *string `json:"intro_video_url"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
Title *string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
BannerImage *string `json:"banner_image"`
|
||||
Persona *string `json:"persona"`
|
||||
TimeLimitMinutes *int32 `json:"time_limit_minutes"`
|
||||
PassingScore *int32 `json:"passing_score"`
|
||||
ShuffleQuestions *bool `json:"shuffle_questions"`
|
||||
Status *string `json:"status"`
|
||||
SubCourseVideoID *int64 `json:"sub_course_video_id"`
|
||||
Questions []updateLessonQuestionReq `json:"questions"`
|
||||
}
|
||||
|
||||
type createSubModulePracticeReq struct {
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
Title string `json:"title"`
|
||||
|
|
@ -246,6 +269,90 @@ func (h *Handler) ListCoursesByCategory(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
// ListCourseSubCategories godoc
|
||||
// @Summary List course sub-categories
|
||||
// @Description Returns all active course sub-categories
|
||||
// @Tags course-management
|
||||
// @Produce json
|
||||
// @Param offset query int false "Offset"
|
||||
// @Param limit query int false "Limit"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-categories [get]
|
||||
func (h *Handler) ListCourseSubCategories(c *fiber.Ctx) error {
|
||||
offset := int32(c.QueryInt("offset", 0))
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
limit := int32(c.QueryInt("limit", 10000))
|
||||
if limit <= 0 {
|
||||
limit = 10000
|
||||
}
|
||||
|
||||
rows, err := h.analyticsDB.GetCourseSubCategories(c.Context(), dbgen.GetCourseSubCategoriesParams{
|
||||
Offset: pgtype.Int4{Int32: offset, Valid: true},
|
||||
Limit: pgtype.Int4{Int32: limit, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-categories", Error: err.Error()})
|
||||
}
|
||||
|
||||
total := 0
|
||||
if len(rows) > 0 {
|
||||
total = int(rows[0].TotalCount)
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Sub-categories retrieved successfully",
|
||||
Data: map[string]interface{}{
|
||||
"sub_categories": rows,
|
||||
"total_count": total,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ListHumanLanguageCourseSubCategories godoc
|
||||
// @Summary List Human Language sub-categories
|
||||
// @Description Returns active sub-categories under Human Language category
|
||||
// @Tags course-management
|
||||
// @Produce json
|
||||
// @Param offset query int false "Offset"
|
||||
// @Param limit query int false "Limit"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/human-language/sub-categories [get]
|
||||
func (h *Handler) ListHumanLanguageCourseSubCategories(c *fiber.Ctx) error {
|
||||
offset := int32(c.QueryInt("offset", 0))
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
limit := int32(c.QueryInt("limit", 10000))
|
||||
if limit <= 0 {
|
||||
limit = 10000
|
||||
}
|
||||
|
||||
rows, err := h.analyticsDB.GetHumanLanguageCourseSubCategories(c.Context(), dbgen.GetHumanLanguageCourseSubCategoriesParams{
|
||||
Offset: pgtype.Int4{Int32: offset, Valid: true},
|
||||
Limit: pgtype.Int4{Int32: limit, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load Human Language sub-categories", Error: err.Error()})
|
||||
}
|
||||
|
||||
total := 0
|
||||
if len(rows) > 0 {
|
||||
total = int(rows[0].TotalCount)
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Human Language sub-categories retrieved successfully",
|
||||
Data: map[string]interface{}{
|
||||
"sub_categories": rows,
|
||||
"total_count": total,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// CreateCourseCategory godoc
|
||||
// @Summary Create course category
|
||||
// @Description Legacy-compatible endpoint for creating a course category
|
||||
|
|
@ -1014,6 +1121,233 @@ func (h *Handler) GetSubModuleLessonByID(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
// UpdateSubModuleLesson godoc
|
||||
// @Summary Update lesson detail
|
||||
// @Description Updates lesson metadata, linked question-set metadata, and optionally replaces lesson questions
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param lessonId path int true "Lesson ID"
|
||||
// @Param body body updateSubModuleLessonReq true "Update lesson payload"
|
||||
// @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} [put]
|
||||
func (h *Handler) UpdateSubModuleLesson(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",
|
||||
})
|
||||
}
|
||||
|
||||
var req updateSubModuleLessonReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
currentLesson, 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(),
|
||||
})
|
||||
}
|
||||
|
||||
targetSubModuleID := currentLesson.SubModuleID
|
||||
if req.SubModuleID != nil {
|
||||
if *req.SubModuleID <= 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id must be a positive integer"})
|
||||
}
|
||||
targetSubModuleID = *req.SubModuleID
|
||||
}
|
||||
|
||||
targetQuestionSetID := currentLesson.QuestionSetID
|
||||
if req.QuestionSetID != nil {
|
||||
if *req.QuestionSetID <= 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "question_set_id must be a positive integer"})
|
||||
}
|
||||
targetQuestionSetID = *req.QuestionSetID
|
||||
}
|
||||
|
||||
targetIntroVideoURL := currentLesson.IntroVideoUrl
|
||||
if req.IntroVideoURL != nil {
|
||||
targetIntroVideoURL = toText(req.IntroVideoURL)
|
||||
}
|
||||
|
||||
targetDisplayOrder := currentLesson.DisplayOrder
|
||||
if req.DisplayOrder != nil {
|
||||
targetDisplayOrder = *req.DisplayOrder
|
||||
}
|
||||
|
||||
targetIsActive := currentLesson.IsActive
|
||||
if req.IsActive != nil {
|
||||
targetIsActive = *req.IsActive
|
||||
}
|
||||
|
||||
if _, err := h.analyticsDB.UpdateSubModuleLesson(c.Context(), dbgen.UpdateSubModuleLessonParams{
|
||||
SubModuleID: targetSubModuleID,
|
||||
QuestionSetID: targetQuestionSetID,
|
||||
IntroVideoUrl: targetIntroVideoURL,
|
||||
DisplayOrder: targetDisplayOrder,
|
||||
IsActive: targetIsActive,
|
||||
ID: lessonID,
|
||||
}); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to update lesson",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
currentSet, err := h.questionsSvc.GetQuestionSetByID(c.Context(), targetQuestionSetID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load linked question set",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
shouldUpdateSet := req.Title != nil || req.Description != nil || req.BannerImage != nil ||
|
||||
req.Persona != nil || req.TimeLimitMinutes != nil || req.PassingScore != nil ||
|
||||
req.ShuffleQuestions != nil || req.Status != nil || req.SubCourseVideoID != nil
|
||||
|
||||
if shouldUpdateSet {
|
||||
title := currentSet.Title
|
||||
if req.Title != nil {
|
||||
title = *req.Title
|
||||
}
|
||||
|
||||
input := domain.CreateQuestionSetInput{
|
||||
Title: title,
|
||||
Description: currentSet.Description,
|
||||
BannerImage: currentSet.BannerImage,
|
||||
Persona: currentSet.Persona,
|
||||
TimeLimitMinutes: currentSet.TimeLimitMinutes,
|
||||
PassingScore: currentSet.PassingScore,
|
||||
SubCourseVideoID: currentSet.SubCourseVideoID,
|
||||
IntroVideoURL: req.IntroVideoURL,
|
||||
ShuffleQuestions: ¤tSet.ShuffleQuestions,
|
||||
}
|
||||
|
||||
currentStatus := currentSet.Status
|
||||
input.Status = ¤tStatus
|
||||
|
||||
if req.Description != nil {
|
||||
input.Description = req.Description
|
||||
}
|
||||
if req.BannerImage != nil {
|
||||
input.BannerImage = req.BannerImage
|
||||
}
|
||||
if req.Persona != nil {
|
||||
input.Persona = req.Persona
|
||||
}
|
||||
if req.TimeLimitMinutes != nil {
|
||||
input.TimeLimitMinutes = req.TimeLimitMinutes
|
||||
}
|
||||
if req.PassingScore != nil {
|
||||
input.PassingScore = req.PassingScore
|
||||
}
|
||||
if req.ShuffleQuestions != nil {
|
||||
input.ShuffleQuestions = req.ShuffleQuestions
|
||||
}
|
||||
if req.Status != nil {
|
||||
input.Status = req.Status
|
||||
}
|
||||
if req.SubCourseVideoID != nil {
|
||||
input.SubCourseVideoID = req.SubCourseVideoID
|
||||
}
|
||||
|
||||
if err := h.questionsSvc.UpdateQuestionSet(c.Context(), targetQuestionSetID, input); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to update linked question set",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if req.Questions != nil {
|
||||
seen := make(map[int64]struct{}, len(req.Questions))
|
||||
for idx, q := range req.Questions {
|
||||
if q.QuestionID <= 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Each question_id must be a positive integer"})
|
||||
}
|
||||
if _, exists := seen[q.QuestionID]; exists {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Duplicate question_id values are not allowed"})
|
||||
}
|
||||
seen[q.QuestionID] = struct{}{}
|
||||
|
||||
order := q.DisplayOrder
|
||||
if order == nil {
|
||||
defaultOrder := int32(idx)
|
||||
order = &defaultOrder
|
||||
}
|
||||
|
||||
if _, err := h.questionsSvc.GetQuestionByID(c.Context(), q.QuestionID); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid question_id in questions payload",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if _, err := h.questionsSvc.AddQuestionToSet(c.Context(), targetQuestionSetID, q.QuestionID, order); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to upsert lesson question",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
existingItems, err := h.questionsSvc.GetQuestionSetItems(c.Context(), targetQuestionSetID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load existing lesson questions",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
for _, item := range existingItems {
|
||||
if _, keep := seen[item.QuestionID]; keep {
|
||||
continue
|
||||
}
|
||||
if err := h.questionsSvc.RemoveQuestionFromSet(c.Context(), targetQuestionSetID, item.QuestionID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to remove question from lesson",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updatedLesson, err := h.analyticsDB.GetSubModuleLessonByID(c.Context(), lessonID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Lesson updated but failed to fetch latest detail",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
updatedQuestions, err := h.questionsSvc.GetQuestionSetItems(c.Context(), updatedLesson.QuestionSetID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Lesson updated but failed to fetch latest questions",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "Lesson updated successfully",
|
||||
Data: map[string]interface{}{
|
||||
"lesson": updatedLesson,
|
||||
"questions": updatedQuestions,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// CreateSubModulePractice godoc
|
||||
// @Summary Create practice under sub-module
|
||||
// @Description Creates a sub-module practice with metadata and linked question set
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ func (a *App) initAppRoutes() {
|
|||
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/courses/:courseId/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchyByCourse)
|
||||
groupV1.Get("/course-management/sub-categories", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCourseSubCategories)
|
||||
groupV1.Get("/course-management/human-language/sub-categories", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListHumanLanguageCourseSubCategories)
|
||||
groupV1.Post("/course-management/sub-categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseSubCategory)
|
||||
groupV1.Delete("/course-management/sub-categories/:subCategoryId", a.authMiddleware, a.RequirePermission("course_categories.delete"), h.DeleteCourseSubCategory)
|
||||
groupV1.Post("/course-management/levels", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateLevel)
|
||||
|
|
@ -105,6 +107,7 @@ func (a *App) initAppRoutes() {
|
|||
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.Put("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdateSubModuleLesson)
|
||||
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.Put("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdatePractice)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user