Yimaru-BackEnd/internal/web_server/handlers/initial_assessment.go
Yared Yemane 33355a4b23 feat: PDF_ATTACHMENT stimulus, dynamic question_text rules, admin builder docs
Add PDF_ATTACHMENT stimulus kind and MinIO pdf upload (media_type=pdf) for question-side PDFs.

Reject top-level question_text on DYNAMIC create/update; omit it from API responses and derive stored text from stimulus only.

Expand DYNAMIC_QUESTION_TYPE_BUILDER_ADMIN_INTEGRATION.md with full API request/response reference and workflows.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 11:07:02 -07:00

218 lines
6.7 KiB
Go

package handlers
import (
"Yimaru-Backend/internal/domain"
"strconv"
"github.com/gofiber/fiber/v2"
)
// CreateAssessmentQuestion godoc
// @Summary Create assessment question
// @Description Creates a new assessment question using the unified questions system
// @Tags assessment-question
// @Accept json
// @Produce json
// @Param body body createQuestionReq true "Create question payload"
// @Success 201 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/assessment/questions [post]
func (h *Handler) CreateAssessmentQuestion(c *fiber.Ctx) error {
var req createQuestionReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
questionType := normalizeRuntimeQuestionType(req.QuestionType)
questionText, err := resolveStoredQuestionText(questionType, req.QuestionText, req.DynamicPayload, "")
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid question_text",
Error: err.Error(),
})
}
// Build options input
var options []domain.CreateQuestionOptionInput
for _, opt := range req.Options {
options = append(options, domain.CreateQuestionOptionInput{
OptionText: opt.OptionText,
OptionOrder: opt.OptionOrder,
IsCorrect: opt.IsCorrect,
})
}
// Build short answers input
var shortAnswers []domain.CreateShortAnswerInput
for _, sa := range req.ShortAnswers {
shortAnswers = append(shortAnswers, domain.CreateShortAnswerInput{
AcceptableAnswer: sa.AcceptableAnswer,
MatchType: sa.MatchType,
})
}
input := domain.CreateQuestionInput{
QuestionText: questionText,
QuestionType: questionType,
DifficultyLevel: req.DifficultyLevel,
Points: req.Points,
Explanation: req.Explanation,
Tips: req.Tips,
VoicePrompt: req.VoicePrompt,
SampleAnswerVoicePrompt: req.SampleAnswerVoicePrompt,
Status: req.Status,
Options: options,
ShortAnswers: shortAnswers,
}
question, err := h.assessmentSvc.CreateQuestion(c.Context(), input)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to create assessment question",
Error: err.Error(),
})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Assessment question created successfully",
StatusCode: fiber.StatusCreated,
Success: true,
Data: questionRes{
ID: question.ID,
QuestionText: questionTextField(question.QuestionType, question.QuestionText),
QuestionType: question.QuestionType,
Status: question.Status,
CreatedAt: question.CreatedAt.String(),
},
})
}
// ListAssessmentQuestions godoc
// @Summary List assessment questions
// @Description Returns all active assessment questions from the initial assessment set
// @Tags assessment-question
// @Produce json
// @Success 200 {array} domain.QuestionWithDetails
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/assessment/questions [get]
func (h *Handler) ListAssessmentQuestions(c *fiber.Ctx) error {
questions, err := h.assessmentSvc.ListQuestions(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch assessment questions",
Error: err.Error(),
})
}
var questionResponses []questionRes
for _, q := range questions {
var options []optionRes
for _, opt := range q.Options {
options = append(options, optionRes{
ID: opt.ID,
OptionText: opt.OptionText,
OptionOrder: opt.OptionOrder,
IsCorrect: opt.IsCorrect,
})
}
var shortAnswers []shortAnswerRes
for _, sa := range q.ShortAnswers {
shortAnswers = append(shortAnswers, shortAnswerRes{
ID: sa.ID,
AcceptableAnswer: sa.AcceptableAnswer,
MatchType: sa.MatchType,
})
}
questionResponses = append(questionResponses, questionRes{
ID: q.ID,
QuestionText: questionTextField(q.QuestionType, q.QuestionText),
QuestionType: q.QuestionType,
DifficultyLevel: q.DifficultyLevel,
Points: q.Points,
Status: q.Status,
CreatedAt: q.CreatedAt.String(),
Options: options,
ShortAnswers: shortAnswers,
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Questions fetched successfully",
Data: questionResponses,
Success: true,
StatusCode: 200,
})
}
// GetAssessmentQuestionByID godoc
// @Summary Get assessment question by ID
// @Description Returns a single assessment question with its options or answer
// @Tags assessment-question
// @Produce json
// @Param id path int true "Question ID"
// @Success 200 {object} domain.QuestionWithDetails
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/assessment/questions/{id} [get]
func (h *Handler) GetAssessmentQuestionByID(c *fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid question ID",
Error: "question ID must be a positive integer",
})
}
question, err := h.assessmentSvc.GetQuestionByID(c.Context(), id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch assessment question",
Error: err.Error(),
})
}
var options []optionRes
for _, opt := range question.Options {
options = append(options, optionRes{
ID: opt.ID,
OptionText: opt.OptionText,
OptionOrder: opt.OptionOrder,
IsCorrect: opt.IsCorrect,
})
}
var shortAnswers []shortAnswerRes
for _, sa := range question.ShortAnswers {
shortAnswers = append(shortAnswers, shortAnswerRes{
ID: sa.ID,
AcceptableAnswer: sa.AcceptableAnswer,
MatchType: sa.MatchType,
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Question fetched successfully",
Data: questionRes{
ID: question.ID,
QuestionText: questionTextField(question.QuestionType, question.QuestionText),
QuestionType: question.QuestionType,
DifficultyLevel: question.DifficultyLevel,
Points: question.Points,
Status: question.Status,
CreatedAt: question.CreatedAt.String(),
Options: options,
ShortAnswers: shortAnswers,
},
Success: true,
StatusCode: 200,
})
}