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:
parent
43f79d34ea
commit
9c9ab41f41
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user