feat: add GET /practices/:id/full for practice with question set and questions
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c9f4a9c428
commit
0e31a08903
|
|
@ -131,23 +131,86 @@ func (s *Service) applySharedUpdates(
|
|||
}
|
||||
|
||||
if synced == nil && in.Questions == nil {
|
||||
items, err := s.questions.GetQuestionSetItems(ctx, questionSetID)
|
||||
_, synced, err = s.loadQuestionsInSet(ctx, questionSetID)
|
||||
if err != nil {
|
||||
return domain.QuestionSet{}, nil, err
|
||||
}
|
||||
synced = make([]domain.QuestionWithDetails, 0, len(items))
|
||||
for _, item := range items {
|
||||
q, err := s.questions.GetQuestionWithDetails(ctx, item.QuestionID)
|
||||
if err != nil {
|
||||
return domain.QuestionSet{}, nil, err
|
||||
}
|
||||
synced = append(synced, q)
|
||||
}
|
||||
}
|
||||
|
||||
return set, synced, nil
|
||||
}
|
||||
|
||||
func (s *Service) loadQuestionsInSet(ctx context.Context, questionSetID int64) ([]domain.QuestionSetItemWithQuestion, []domain.QuestionWithDetails, error) {
|
||||
items, err := s.questions.GetQuestionSetItems(ctx, questionSetID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
questions := make([]domain.QuestionWithDetails, 0, len(items))
|
||||
for _, item := range items {
|
||||
q, err := s.questions.GetQuestionWithDetails(ctx, item.QuestionID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
questions = append(questions, q)
|
||||
}
|
||||
return items, questions, nil
|
||||
}
|
||||
|
||||
func (s *Service) loadPracticeFullContent(ctx context.Context, questionSetID int64) (domain.QuestionSet, []domain.QuestionWithDetails, error) {
|
||||
set, err := s.questions.GetQuestionSetByID(ctx, questionSetID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.QuestionSet{}, nil, ErrQuestionSetNotFound
|
||||
}
|
||||
return domain.QuestionSet{}, nil, err
|
||||
}
|
||||
_, questions, err := s.loadQuestionsInSet(ctx, questionSetID)
|
||||
if err != nil {
|
||||
return domain.QuestionSet{}, nil, err
|
||||
}
|
||||
return set, questions, nil
|
||||
}
|
||||
|
||||
// GetLmsPracticeFull returns an LMS practice shell, its linked question set, and all questions in the set.
|
||||
func (s *Service) GetLmsPracticeFull(ctx context.Context, practiceID int64) (domain.FullUpdatePracticeResult, error) {
|
||||
practice, err := s.lms.GetLmsPracticeByID(ctx, practiceID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.FullUpdatePracticeResult{}, ErrPracticeNotFound
|
||||
}
|
||||
return domain.FullUpdatePracticeResult{}, err
|
||||
}
|
||||
set, questions, err := s.loadPracticeFullContent(ctx, practice.QuestionSetID)
|
||||
if err != nil {
|
||||
return domain.FullUpdatePracticeResult{}, err
|
||||
}
|
||||
return domain.FullUpdatePracticeResult{
|
||||
Practice: practice,
|
||||
QuestionSet: set,
|
||||
Questions: questions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetExamPrepPracticeFull returns an exam-prep practice shell, its linked question set, and all questions in the set.
|
||||
func (s *Service) GetExamPrepPracticeFull(ctx context.Context, practiceID int64) (domain.FullUpdatePracticeResult, error) {
|
||||
practice, err := s.examPrep.GetExamPrepLessonPracticeByID(ctx, practiceID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.FullUpdatePracticeResult{}, ErrPracticeNotFound
|
||||
}
|
||||
return domain.FullUpdatePracticeResult{}, err
|
||||
}
|
||||
set, questions, err := s.loadPracticeFullContent(ctx, practice.QuestionSetID)
|
||||
if err != nil {
|
||||
return domain.FullUpdatePracticeResult{}, err
|
||||
}
|
||||
return domain.FullUpdatePracticeResult{
|
||||
Practice: practice,
|
||||
QuestionSet: set,
|
||||
Questions: questions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateLmsPracticeFull updates an LMS practice shell, linked question set, and questions.
|
||||
func (s *Service) UpdateLmsPracticeFull(ctx context.Context, practiceID int64, in domain.FullUpdatePracticeInput) (domain.FullUpdatePracticeResult, error) {
|
||||
practice, err := s.lms.GetLmsPracticeByID(ctx, practiceID)
|
||||
|
|
|
|||
|
|
@ -3,26 +3,52 @@ package handlers
|
|||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/services/practicecontent"
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func fullUpdatePracticeQuestionResponses(questions []domain.QuestionWithDetails, catalog []domain.QuestionTypeDefinition) []questionRes {
|
||||
func fullPracticeQuestionResponses(
|
||||
questions []domain.QuestionWithDetails,
|
||||
displayOrderByQuestionID map[int64]int32,
|
||||
catalog []domain.QuestionTypeDefinition,
|
||||
) []questionRes {
|
||||
out := make([]questionRes, 0, len(questions))
|
||||
for _, question := range questions {
|
||||
out = append(out, buildQuestionRes(question, catalog))
|
||||
res := buildQuestionRes(question, catalog)
|
||||
if displayOrderByQuestionID != nil {
|
||||
if order, ok := displayOrderByQuestionID[question.ID]; ok {
|
||||
res.DisplayOrder = &order
|
||||
}
|
||||
}
|
||||
out = append(out, res)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func fullUpdatePracticeResponse(result domain.FullUpdatePracticeResult, catalog []domain.QuestionTypeDefinition) fiber.Map {
|
||||
func (h *Handler) buildFullPracticeResponse(
|
||||
ctx context.Context,
|
||||
result domain.FullUpdatePracticeResult,
|
||||
catalog []domain.QuestionTypeDefinition,
|
||||
) (fiber.Map, error) {
|
||||
items, err := h.questionsSvc.GetQuestionSetItems(ctx, result.QuestionSet.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
displayOrderByQuestionID := make(map[int64]int32, len(items))
|
||||
for _, item := range items {
|
||||
displayOrderByQuestionID[item.QuestionID] = item.DisplayOrder
|
||||
}
|
||||
questionCount := int64(len(items))
|
||||
qsRes := questionSetResFromDomain(result.QuestionSet)
|
||||
qsRes.QuestionCount = &questionCount
|
||||
return fiber.Map{
|
||||
"practice": result.Practice,
|
||||
"question_set": questionSetResFromDomain(result.QuestionSet),
|
||||
"questions": fullUpdatePracticeQuestionResponses(result.Questions, catalog),
|
||||
}
|
||||
"question_set": qsRes,
|
||||
"questions": fullPracticeQuestionResponses(result.Questions, displayOrderByQuestionID, catalog),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func questionSetResFromDomain(set domain.QuestionSet) questionSetRes {
|
||||
|
|
@ -44,7 +70,7 @@ func questionSetResFromDomain(set domain.QuestionSet) questionSetRes {
|
|||
}
|
||||
}
|
||||
|
||||
func mapFullUpdatePracticeError(err error) (int, string) {
|
||||
func mapPracticeFullError(err error, action string) (int, string) {
|
||||
switch {
|
||||
case errors.Is(err, practicecontent.ErrPracticeNotFound):
|
||||
return fiber.StatusNotFound, "Practice not found"
|
||||
|
|
@ -57,10 +83,144 @@ func mapFullUpdatePracticeError(err error) (int, string) {
|
|||
case errors.Is(err, practicecontent.ErrInvalidQuestionItem):
|
||||
return fiber.StatusBadRequest, "Invalid question item"
|
||||
default:
|
||||
if action == "load" {
|
||||
return fiber.StatusInternalServerError, "Failed to load practice"
|
||||
}
|
||||
return fiber.StatusBadRequest, "Failed to update practice"
|
||||
}
|
||||
}
|
||||
|
||||
// GetLmsPracticeFull godoc
|
||||
// @Summary Get LMS practice with question set and questions
|
||||
// @Description Returns practice metadata, linked question set settings, and all questions in the set (same shape as PUT /practices/{id}/full).
|
||||
// @Tags practices
|
||||
// @Produce json
|
||||
// @Param id path int true "Practice ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 404 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/practices/{id}/full [get]
|
||||
func (h *Handler) GetLmsPracticeFull(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 practice id",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
result, err := h.practiceContentSvc.GetLmsPracticeFull(c.Context(), id)
|
||||
if err != nil {
|
||||
status, msg := mapPracticeFullError(err, "load")
|
||||
return c.Status(status).JSON(domain.ErrorResponse{
|
||||
Message: msg,
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
practice, ok := result.Practice.(domain.Practice)
|
||||
if !ok {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load practice",
|
||||
})
|
||||
}
|
||||
if !practice.VisibleToLearners() && !h.canManageLMSPractices(c) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Practice not found"})
|
||||
}
|
||||
if err := h.applyPracticeAccess(c.Context(), c, []domain.Practice{practice}); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to build practice",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
result.Practice = practice
|
||||
|
||||
catalog, err := h.activeQuestionTypeDefinitionCatalog(c.Context())
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load question type catalog",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
data, err := h.buildFullPracticeResponse(c.Context(), result, catalog)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load practice questions",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Practice retrieved successfully",
|
||||
Data: data,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// GetExamPrepPracticeFull godoc
|
||||
// @Summary Get exam-prep practice with question set and questions
|
||||
// @Description Returns exam-prep practice metadata, linked question set settings, and all questions in the set.
|
||||
// @Tags exam-prep
|
||||
// @Produce json
|
||||
// @Param id path int true "Exam prep practice ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 404 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/exam-prep/practices/{id}/full [get]
|
||||
func (h *Handler) GetExamPrepPracticeFull(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 practice id",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
result, err := h.practiceContentSvc.GetExamPrepPracticeFull(c.Context(), id)
|
||||
if err != nil {
|
||||
status, msg := mapPracticeFullError(err, "load")
|
||||
return c.Status(status).JSON(domain.ErrorResponse{
|
||||
Message: msg,
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
practice, ok := result.Practice.(domain.ExamPrepPractice)
|
||||
if !ok {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load practice",
|
||||
})
|
||||
}
|
||||
if !practice.VisibleToLearners() && !h.canManageExamPrepPractices(c) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Practice not found"})
|
||||
}
|
||||
|
||||
catalog, err := h.activeQuestionTypeDefinitionCatalog(c.Context())
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load question type catalog",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
data, err := h.buildFullPracticeResponse(c.Context(), result, catalog)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load practice questions",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Practice retrieved successfully",
|
||||
Data: data,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateLmsPracticeFull godoc
|
||||
// @Summary Full update LMS practice with question set and questions
|
||||
// @Description Updates practice metadata, linked question set settings, and syncs questions in one request. Questions omitted from the questions array are removed from the set (not deleted from the bank).
|
||||
|
|
@ -92,7 +252,7 @@ func (h *Handler) UpdateLmsPracticeFull(c *fiber.Ctx) error {
|
|||
|
||||
result, err := h.practiceContentSvc.UpdateLmsPracticeFull(c.Context(), id, req)
|
||||
if err != nil {
|
||||
status, msg := mapFullUpdatePracticeError(err)
|
||||
status, msg := mapPracticeFullError(err, "update")
|
||||
return c.Status(status).JSON(domain.ErrorResponse{
|
||||
Message: msg,
|
||||
Error: err.Error(),
|
||||
|
|
@ -107,9 +267,17 @@ func (h *Handler) UpdateLmsPracticeFull(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
data, err := h.buildFullPracticeResponse(c.Context(), result, catalog)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load practice questions",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Practice updated successfully",
|
||||
Data: fullUpdatePracticeResponse(result, catalog),
|
||||
Data: data,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
|
|
@ -146,7 +314,7 @@ func (h *Handler) UpdateExamPrepPracticeFull(c *fiber.Ctx) error {
|
|||
|
||||
result, err := h.practiceContentSvc.UpdateExamPrepPracticeFull(c.Context(), id, req)
|
||||
if err != nil {
|
||||
status, msg := mapFullUpdatePracticeError(err)
|
||||
status, msg := mapPracticeFullError(err, "update")
|
||||
return c.Status(status).JSON(domain.ErrorResponse{
|
||||
Message: msg,
|
||||
Error: err.Error(),
|
||||
|
|
@ -161,9 +329,17 @@ func (h *Handler) UpdateExamPrepPracticeFull(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
data, err := h.buildFullPracticeResponse(c.Context(), result, catalog)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load practice questions",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Practice updated successfully",
|
||||
Data: fullUpdatePracticeResponse(result, catalog),
|
||||
Data: data,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ func (a *App) initAppRoutes() {
|
|||
examPrep.Post("/lessons/:lessonId/practices", a.RequirePermission("exam_prep.practices.create"), h.CreateExamPrepPractice)
|
||||
examPrep.Get("/lessons/:lessonId/practices", a.RequirePermission("exam_prep.practices.list_by_lesson"), h.ListExamPrepPracticesByLesson)
|
||||
examPrep.Get("/practices/:id", a.RequirePermission("exam_prep.practices.get"), h.GetExamPrepPracticeByID)
|
||||
examPrep.Get("/practices/:id/full", a.RequirePermission("exam_prep.practices.get"), h.GetExamPrepPracticeFull)
|
||||
examPrep.Put("/practices/:id", a.RequirePermission("exam_prep.practices.update"), h.UpdateExamPrepPractice)
|
||||
examPrep.Put("/practices/:id/full", a.RequirePermission("exam_prep.practices.update"), h.UpdateExamPrepPracticeFull)
|
||||
examPrep.Delete("/practices/:id", a.RequirePermission("exam_prep.practices.delete"), h.DeleteExamPrepPractice)
|
||||
|
|
@ -160,6 +161,7 @@ func (a *App) initAppRoutes() {
|
|||
|
||||
groupV1.Post("/practices", a.authMiddleware, a.RequirePermission("practices.create"), h.CreatePractice)
|
||||
groupV1.Get("/practices/:id", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("practices.get"), h.GetPractice)
|
||||
groupV1.Get("/practices/:id/full", a.authMiddleware, a.RequireLMSSubscriptionUnlessFree(), a.RequirePermission("practices.get"), h.GetLmsPracticeFull)
|
||||
groupV1.Put("/practices/:id", a.authMiddleware, a.RequirePermission("practices.update"), h.UpdatePractice)
|
||||
groupV1.Put("/practices/:id/full", a.authMiddleware, a.RequirePermission("practices.update"), h.UpdateLmsPracticeFull)
|
||||
groupV1.Delete("/practices/:id", a.authMiddleware, a.RequirePermission("practices.delete"), h.DeletePractice)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user