add dedicated human language backend endpoints

Introduce separate CEFR-based human language lesson APIs for create, update, and level-filtered retrieval while keeping existing non-language course hierarchy endpoints unchanged.

Made-with: Cursor
This commit is contained in:
Yared Yemane 2026-04-07 05:40:15 -07:00
parent 43f79d34ea
commit 9c9ab41f41
2 changed files with 249 additions and 0 deletions

View File

@ -614,6 +614,252 @@ func isValidSubLevelForLevel(level, subLevel string) bool {
} }
} }
func isValidHumanLanguageCEFRLevel(level string) bool {
switch strings.ToUpper(strings.TrimSpace(level)) {
case string(domain.SubCourseSubLevelA1),
string(domain.SubCourseSubLevelA2),
string(domain.SubCourseSubLevelA3),
string(domain.SubCourseSubLevelB1),
string(domain.SubCourseSubLevelB2),
string(domain.SubCourseSubLevelB3),
string(domain.SubCourseSubLevelC1),
string(domain.SubCourseSubLevelC2),
string(domain.SubCourseSubLevelC3):
return true
default:
return false
}
}
func coarseLevelFromCEFR(cefr string) string {
switch strings.ToUpper(strings.TrimSpace(cefr)) {
case string(domain.SubCourseSubLevelA1), string(domain.SubCourseSubLevelA2), string(domain.SubCourseSubLevelA3):
return string(domain.SubCourseLevelBeginner)
case string(domain.SubCourseSubLevelB1), string(domain.SubCourseSubLevelB2), string(domain.SubCourseSubLevelB3):
return string(domain.SubCourseLevelIntermediate)
default:
return string(domain.SubCourseLevelAdvanced)
}
}
func (h *Handler) ensureCourseIsHumanLanguage(ctx context.Context, courseID int64) (domain.Course, error) {
course, err := h.courseMgmtSvc.GetCourseByID(ctx, courseID)
if err != nil {
return domain.Course{}, err
}
category, err := h.courseMgmtSvc.GetCourseCategoryByID(ctx, course.CategoryID)
if err != nil {
return domain.Course{}, err
}
categoryName := strings.ToLower(strings.TrimSpace(category.Name))
if !strings.Contains(categoryName, "language") {
return domain.Course{}, fmt.Errorf("course is not under a human language category")
}
return course, nil
}
type createHumanLanguageLessonReq struct {
CourseID int64 `json:"course_id" validate:"required"`
Title string `json:"title" validate:"required"`
Description *string `json:"description"`
Thumbnail *string `json:"thumbnail"`
DisplayOrder *int32 `json:"display_order"`
CEFRLevel string `json:"cefr_level" validate:"required"`
}
type updateHumanLanguageLessonReq struct {
Title *string `json:"title"`
Description *string `json:"description"`
Thumbnail *string `json:"thumbnail"`
DisplayOrder *int32 `json:"display_order"`
CEFRLevel *string `json:"cefr_level"`
IsActive *bool `json:"is_active"`
}
type humanLanguageLessonRes struct {
ID int64 `json:"id"`
CourseID int64 `json:"course_id"`
Title string `json:"title"`
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
DisplayOrder int32 `json:"display_order"`
Level string `json:"level"`
VideoCount int64 `json:"video_count"`
PracticeCount int64 `json:"practice_count"`
Videos []domain.LearningPathVideo `json:"videos"`
Practices []domain.LearningPathPractice `json:"practices"`
}
type getHumanLanguageLessonsRes struct {
CourseID int64 `json:"course_id"`
CourseTitle string `json:"course_title"`
CEFRLevel string `json:"cefr_level"`
Lessons []humanLanguageLessonRes `json:"lessons"`
}
// CreateHumanLanguageLesson godoc
// @Summary Create human-language lesson unit
// @Description Creates a lesson unit under a human-language course using CEFR level (A1..C3)
// @Tags human-language
// @Accept json
// @Produce json
// @Param body body createHumanLanguageLessonReq true "Create human-language lesson payload"
// @Success 201 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/human-language/lessons [post]
func (h *Handler) CreateHumanLanguageLesson(c *fiber.Ctx) error {
var req createHumanLanguageLessonReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
}
req.CEFRLevel = strings.ToUpper(strings.TrimSpace(req.CEFRLevel))
if !isValidHumanLanguageCEFRLevel(req.CEFRLevel) {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid CEFR level",
Error: "Use one of: A1,A2,A3,B1,B2,B3,C1,C2,C3",
})
}
if _, err := h.ensureCourseIsHumanLanguage(c.Context(), req.CourseID); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid human-language course", Error: err.Error()})
}
created, err := h.courseMgmtSvc.CreateSubCourse(
c.Context(),
req.CourseID,
req.Title,
req.Description,
req.Thumbnail,
req.DisplayOrder,
coarseLevelFromCEFR(req.CEFRLevel),
req.CEFRLevel,
)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create human-language lesson", Error: err.Error()})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Human-language lesson created successfully",
Data: subCourseRes{
ID: created.ID,
CourseID: created.CourseID,
Title: created.Title,
Description: created.Description,
Thumbnail: created.Thumbnail,
DisplayOrder: created.DisplayOrder,
Level: created.SubLevel,
SubLevel: "",
IsActive: created.IsActive,
},
})
}
// UpdateHumanLanguageLesson godoc
// @Summary Update human-language lesson unit
// @Description Updates a human-language lesson unit and CEFR level
// @Tags human-language
// @Accept json
// @Produce json
// @Param id path int true "Lesson (sub-course) ID"
// @Param body body updateHumanLanguageLessonReq true "Update human-language lesson payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/human-language/lessons/{id} [patch]
func (h *Handler) UpdateHumanLanguageLesson(c *fiber.Ctx) error {
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid lesson ID", Error: err.Error()})
}
var req updateHumanLanguageLessonReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
}
var levelPtr *string
var subLevelPtr *string
if req.CEFRLevel != nil {
normalized := strings.ToUpper(strings.TrimSpace(*req.CEFRLevel))
if !isValidHumanLanguageCEFRLevel(normalized) {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid CEFR level",
Error: "Use one of: A1,A2,A3,B1,B2,B3,C1,C2,C3",
})
}
level := coarseLevelFromCEFR(normalized)
levelPtr = &level
subLevelPtr = &normalized
}
if err := h.courseMgmtSvc.UpdateSubCourse(c.Context(), id, req.Title, req.Description, req.Thumbnail, req.DisplayOrder, levelPtr, subLevelPtr, req.IsActive); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update human-language lesson", Error: err.Error()})
}
return c.JSON(domain.Response{Message: "Human-language lesson updated successfully"})
}
// GetHumanLanguageLessonsByCourse godoc
// @Summary Get human-language lessons by CEFR level
// @Description Returns lessons for a human-language course filtered by CEFR level (A1..C3)
// @Tags human-language
// @Produce json
// @Param courseId path int true "Course ID"
// @Param cefr_level query string true "CEFR level (A1..C3)"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/human-language/courses/{courseId}/lessons [get]
func (h *Handler) GetHumanLanguageLessonsByCourse(c *fiber.Ctx) error {
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid course ID", Error: err.Error()})
}
cefrLevel := strings.ToUpper(strings.TrimSpace(c.Query("cefr_level")))
if !isValidHumanLanguageCEFRLevel(cefrLevel) {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid CEFR level",
Error: "Use one of: A1,A2,A3,B1,B2,B3,C1,C2,C3",
})
}
if _, err := h.ensureCourseIsHumanLanguage(c.Context(), courseID); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid human-language course", Error: err.Error()})
}
path, err := h.courseMgmtSvc.GetCourseLearningPath(c.Context(), courseID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to retrieve learning path", Error: err.Error()})
}
lessons := make([]humanLanguageLessonRes, 0)
for _, sc := range path.SubCourses {
if strings.ToUpper(strings.TrimSpace(sc.SubLevel)) != cefrLevel {
continue
}
lessons = append(lessons, humanLanguageLessonRes{
ID: sc.ID,
CourseID: courseID,
Title: sc.Title,
Description: sc.Description,
Thumbnail: sc.Thumbnail,
DisplayOrder: sc.DisplayOrder,
Level: sc.SubLevel,
VideoCount: int64(len(sc.Videos)),
PracticeCount: int64(len(sc.Practices)),
Videos: sc.Videos,
Practices: sc.Practices,
})
}
return c.JSON(domain.Response{
Message: "Human-language lessons retrieved successfully",
Data: getHumanLanguageLessonsRes{
CourseID: path.CourseID,
CourseTitle: path.CourseTitle,
CEFRLevel: cefrLevel,
Lessons: lessons,
},
})
}
// CreateSubCourse godoc // CreateSubCourse godoc
// @Summary Create a new sub-course // @Summary Create a new sub-course
// @Description Creates a new sub-course under a specific course // @Description Creates a new sub-course under a specific course

View File

@ -128,6 +128,9 @@ func (a *App) initAppRoutes() {
// Learning Tree // Learning Tree
groupV1.Get("/course-management/learning-tree", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetFullLearningTree) groupV1.Get("/course-management/learning-tree", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetFullLearningTree)
groupV1.Get("/course-management/courses/:courseId/learning-path", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetCourseLearningPath) groupV1.Get("/course-management/courses/:courseId/learning-path", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetCourseLearningPath)
groupV1.Get("/course-management/human-language/courses/:courseId/lessons", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetHumanLanguageLessonsByCourse)
groupV1.Post("/course-management/human-language/lessons", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateHumanLanguageLesson)
groupV1.Patch("/course-management/human-language/lessons/:id", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateHumanLanguageLesson)
// Questions // Questions
groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion) groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion)