1291 lines
43 KiB
Go
1291 lines
43 KiB
Go
package handlers
|
|
|
|
import (
|
|
"Yimaru-Backend/internal/domain"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
)
|
|
|
|
// Request/Response types for Questions
|
|
|
|
type optionInput struct {
|
|
OptionText string `json:"option_text" validate:"required"`
|
|
OptionOrder *int32 `json:"option_order"`
|
|
IsCorrect bool `json:"is_correct"`
|
|
}
|
|
|
|
type shortAnswerInput struct {
|
|
AcceptableAnswer string `json:"acceptable_answer" validate:"required"`
|
|
MatchType *string `json:"match_type"`
|
|
}
|
|
|
|
type createQuestionReq struct {
|
|
QuestionText string `json:"question_text" validate:"required"`
|
|
QuestionType string `json:"question_type" validate:"required,oneof=MCQ TRUE_FALSE SHORT_ANSWER AUDIO"`
|
|
DifficultyLevel *string `json:"difficulty_level"`
|
|
Points *int32 `json:"points"`
|
|
Explanation *string `json:"explanation"`
|
|
Tips *string `json:"tips"`
|
|
VoicePrompt *string `json:"voice_prompt"`
|
|
SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt"`
|
|
ImageURL *string `json:"image_url"`
|
|
Status *string `json:"status"`
|
|
Options []optionInput `json:"options"`
|
|
ShortAnswers []shortAnswerInput `json:"short_answers"`
|
|
AudioCorrectAnswerText *string `json:"audio_correct_answer_text"`
|
|
}
|
|
|
|
type optionRes struct {
|
|
ID int64 `json:"id"`
|
|
OptionText string `json:"option_text"`
|
|
OptionOrder int32 `json:"option_order"`
|
|
IsCorrect bool `json:"is_correct"`
|
|
}
|
|
|
|
type shortAnswerRes struct {
|
|
ID int64 `json:"id"`
|
|
AcceptableAnswer string `json:"acceptable_answer"`
|
|
MatchType string `json:"match_type"`
|
|
}
|
|
|
|
type questionRes struct {
|
|
ID int64 `json:"id"`
|
|
QuestionText string `json:"question_text"`
|
|
QuestionType string `json:"question_type"`
|
|
DifficultyLevel *string `json:"difficulty_level,omitempty"`
|
|
Points int32 `json:"points"`
|
|
Explanation *string `json:"explanation,omitempty"`
|
|
Tips *string `json:"tips,omitempty"`
|
|
VoicePrompt *string `json:"voice_prompt,omitempty"`
|
|
SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt,omitempty"`
|
|
ImageURL *string `json:"image_url,omitempty"`
|
|
Status string `json:"status"`
|
|
CreatedAt string `json:"created_at"`
|
|
Options []optionRes `json:"options,omitempty"`
|
|
ShortAnswers []shortAnswerRes `json:"short_answers,omitempty"`
|
|
AudioCorrectAnswerText *string `json:"audio_correct_answer_text,omitempty"`
|
|
}
|
|
|
|
type listQuestionsRes struct {
|
|
Questions []questionRes `json:"questions"`
|
|
TotalCount int64 `json:"total_count"`
|
|
}
|
|
|
|
// CreateQuestion godoc
|
|
// @Summary Create a new question
|
|
// @Description Creates a new question with options (for MCQ/TRUE_FALSE) or short answers (for SHORT_ANSWER)
|
|
// @Tags questions
|
|
// @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/questions [post]
|
|
func (h *Handler) CreateQuestion(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(),
|
|
})
|
|
}
|
|
|
|
// 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: req.QuestionText,
|
|
QuestionType: req.QuestionType,
|
|
DifficultyLevel: req.DifficultyLevel,
|
|
Points: req.Points,
|
|
Explanation: req.Explanation,
|
|
Tips: req.Tips,
|
|
VoicePrompt: req.VoicePrompt,
|
|
SampleAnswerVoicePrompt: req.SampleAnswerVoicePrompt,
|
|
ImageURL: req.ImageURL,
|
|
Status: req.Status,
|
|
Options: options,
|
|
ShortAnswers: shortAnswers,
|
|
AudioCorrectAnswerText: req.AudioCorrectAnswerText,
|
|
}
|
|
|
|
question, err := h.questionsSvc.CreateQuestion(c.Context(), input)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to create question",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
actorID := c.Locals("user_id").(int64)
|
|
actorRole := string(c.Locals("role").(domain.Role))
|
|
ip := c.IP()
|
|
ua := c.Get("User-Agent")
|
|
meta, _ := json.Marshal(map[string]interface{}{"question_type": question.QuestionType})
|
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionCreated, domain.ResourceQuestion, &question.ID, "Created question: "+question.QuestionText, meta, &ip, &ua)
|
|
|
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
|
Message: "Question created successfully",
|
|
Data: questionRes{
|
|
ID: question.ID,
|
|
QuestionText: question.QuestionText,
|
|
QuestionType: question.QuestionType,
|
|
DifficultyLevel: question.DifficultyLevel,
|
|
Points: question.Points,
|
|
Explanation: question.Explanation,
|
|
Tips: question.Tips,
|
|
VoicePrompt: question.VoicePrompt,
|
|
SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt,
|
|
ImageURL: question.ImageURL,
|
|
Status: question.Status,
|
|
CreatedAt: question.CreatedAt.String(),
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetQuestionByID godoc
|
|
// @Summary Get question by ID
|
|
// @Description Returns a question with its options/short answers
|
|
// @Tags questions
|
|
// @Produce json
|
|
// @Param id path int true "Question ID"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 404 {object} domain.ErrorResponse
|
|
// @Router /api/v1/questions/{id} [get]
|
|
func (h *Handler) GetQuestionByID(c *fiber.Ctx) error {
|
|
idStr := c.Params("id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid question ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
question, err := h.questionsSvc.GetQuestionWithDetails(c.Context(), id)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
Message: "Question not found",
|
|
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,
|
|
})
|
|
}
|
|
|
|
var audioCorrectAnswerText *string
|
|
if question.AudioAnswer != nil {
|
|
audioCorrectAnswerText = &question.AudioAnswer.CorrectAnswerText
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Question retrieved successfully",
|
|
Data: questionRes{
|
|
ID: question.ID,
|
|
QuestionText: question.QuestionText,
|
|
QuestionType: question.QuestionType,
|
|
DifficultyLevel: question.DifficultyLevel,
|
|
Points: question.Points,
|
|
Explanation: question.Explanation,
|
|
Tips: question.Tips,
|
|
VoicePrompt: question.VoicePrompt,
|
|
SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt,
|
|
ImageURL: question.ImageURL,
|
|
Status: question.Status,
|
|
CreatedAt: question.CreatedAt.String(),
|
|
Options: options,
|
|
ShortAnswers: shortAnswers,
|
|
AudioCorrectAnswerText: audioCorrectAnswerText,
|
|
},
|
|
})
|
|
}
|
|
|
|
// ListQuestions godoc
|
|
// @Summary List questions
|
|
// @Description Returns a paginated list of questions with optional filters
|
|
// @Tags questions
|
|
// @Produce json
|
|
// @Param question_type query string false "Question type filter (MCQ, TRUE_FALSE, SHORT_ANSWER)"
|
|
// @Param difficulty query string false "Difficulty level filter (EASY, MEDIUM, HARD)"
|
|
// @Param status query string false "Status filter (DRAFT, PUBLISHED, INACTIVE)"
|
|
// @Param limit query int false "Limit" default(10)
|
|
// @Param offset query int false "Offset" default(0)
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/questions [get]
|
|
func (h *Handler) ListQuestions(c *fiber.Ctx) error {
|
|
questionType := c.Query("question_type")
|
|
difficulty := c.Query("difficulty")
|
|
status := c.Query("status")
|
|
limitStr := c.Query("limit", "10")
|
|
offsetStr := c.Query("offset", "0")
|
|
|
|
limit, _ := strconv.Atoi(limitStr)
|
|
offset, _ := strconv.Atoi(offsetStr)
|
|
|
|
var qTypePtr, diffPtr, statusPtr *string
|
|
if questionType != "" {
|
|
qTypePtr = &questionType
|
|
}
|
|
if difficulty != "" {
|
|
diffPtr = &difficulty
|
|
}
|
|
if status != "" {
|
|
statusPtr = &status
|
|
}
|
|
|
|
questions, totalCount, err := h.questionsSvc.ListQuestions(c.Context(), qTypePtr, diffPtr, statusPtr, int32(limit), int32(offset))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to list questions",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
var questionResponses []questionRes
|
|
for _, q := range questions {
|
|
questionResponses = append(questionResponses, questionRes{
|
|
ID: q.ID,
|
|
QuestionText: q.QuestionText,
|
|
QuestionType: q.QuestionType,
|
|
DifficultyLevel: q.DifficultyLevel,
|
|
Points: q.Points,
|
|
Explanation: q.Explanation,
|
|
Tips: q.Tips,
|
|
VoicePrompt: q.VoicePrompt,
|
|
Status: q.Status,
|
|
CreatedAt: q.CreatedAt.String(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Questions retrieved successfully",
|
|
Data: listQuestionsRes{
|
|
Questions: questionResponses,
|
|
TotalCount: totalCount,
|
|
},
|
|
})
|
|
}
|
|
|
|
// SearchQuestions godoc
|
|
// @Summary Search questions
|
|
// @Description Search questions by text
|
|
// @Tags questions
|
|
// @Produce json
|
|
// @Param q query string true "Search query"
|
|
// @Param limit query int false "Limit" default(10)
|
|
// @Param offset query int false "Offset" default(0)
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/questions/search [get]
|
|
func (h *Handler) SearchQuestions(c *fiber.Ctx) error {
|
|
query := c.Query("q")
|
|
if query == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Search query is required",
|
|
})
|
|
}
|
|
|
|
limitStr := c.Query("limit", "10")
|
|
offsetStr := c.Query("offset", "0")
|
|
limit, _ := strconv.Atoi(limitStr)
|
|
offset, _ := strconv.Atoi(offsetStr)
|
|
|
|
questions, totalCount, err := h.questionsSvc.SearchQuestions(c.Context(), query, int32(limit), int32(offset))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to search questions",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
var questionResponses []questionRes
|
|
for _, q := range questions {
|
|
questionResponses = append(questionResponses, questionRes{
|
|
ID: q.ID,
|
|
QuestionText: q.QuestionText,
|
|
QuestionType: q.QuestionType,
|
|
DifficultyLevel: q.DifficultyLevel,
|
|
Points: q.Points,
|
|
Status: q.Status,
|
|
CreatedAt: q.CreatedAt.String(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Questions retrieved successfully",
|
|
Data: listQuestionsRes{
|
|
Questions: questionResponses,
|
|
TotalCount: totalCount,
|
|
},
|
|
})
|
|
}
|
|
|
|
type updateQuestionReq struct {
|
|
QuestionText *string `json:"question_text"`
|
|
QuestionType *string `json:"question_type"`
|
|
DifficultyLevel *string `json:"difficulty_level"`
|
|
Points *int32 `json:"points"`
|
|
Explanation *string `json:"explanation"`
|
|
Tips *string `json:"tips"`
|
|
VoicePrompt *string `json:"voice_prompt"`
|
|
SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt"`
|
|
ImageURL *string `json:"image_url"`
|
|
Status *string `json:"status"`
|
|
Options []optionInput `json:"options"`
|
|
ShortAnswers []shortAnswerInput `json:"short_answers"`
|
|
AudioCorrectAnswerText *string `json:"audio_correct_answer_text"`
|
|
}
|
|
|
|
// UpdateQuestion godoc
|
|
// @Summary Update a question
|
|
// @Description Updates a question and optionally replaces its options/short answers
|
|
// @Tags questions
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path int true "Question ID"
|
|
// @Param body body updateQuestionReq true "Update question payload"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/questions/{id} [put]
|
|
func (h *Handler) UpdateQuestion(c *fiber.Ctx) error {
|
|
idStr := c.Params("id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid question ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
var req updateQuestionReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid request body",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
var options []domain.CreateQuestionOptionInput
|
|
for _, opt := range req.Options {
|
|
options = append(options, domain.CreateQuestionOptionInput{
|
|
OptionText: opt.OptionText,
|
|
OptionOrder: opt.OptionOrder,
|
|
IsCorrect: opt.IsCorrect,
|
|
})
|
|
}
|
|
|
|
var shortAnswers []domain.CreateShortAnswerInput
|
|
for _, sa := range req.ShortAnswers {
|
|
shortAnswers = append(shortAnswers, domain.CreateShortAnswerInput{
|
|
AcceptableAnswer: sa.AcceptableAnswer,
|
|
MatchType: sa.MatchType,
|
|
})
|
|
}
|
|
|
|
questionText := ""
|
|
if req.QuestionText != nil {
|
|
questionText = *req.QuestionText
|
|
}
|
|
questionType := ""
|
|
if req.QuestionType != nil {
|
|
questionType = *req.QuestionType
|
|
}
|
|
|
|
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,
|
|
ImageURL: req.ImageURL,
|
|
Status: req.Status,
|
|
Options: options,
|
|
ShortAnswers: shortAnswers,
|
|
AudioCorrectAnswerText: req.AudioCorrectAnswerText,
|
|
}
|
|
|
|
err = h.questionsSvc.UpdateQuestion(c.Context(), id, input)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to update question",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
actorID := c.Locals("user_id").(int64)
|
|
actorRole := string(c.Locals("role").(domain.Role))
|
|
ip := c.IP()
|
|
ua := c.Get("User-Agent")
|
|
meta, _ := json.Marshal(map[string]interface{}{"question_id": id})
|
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionUpdated, domain.ResourceQuestion, &id, fmt.Sprintf("Updated question ID: %d", id), meta, &ip, &ua)
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Question updated successfully",
|
|
})
|
|
}
|
|
|
|
// DeleteQuestion godoc
|
|
// @Summary Delete a question
|
|
// @Description Archives a question (soft delete)
|
|
// @Tags questions
|
|
// @Produce json
|
|
// @Param id path int true "Question ID"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/questions/{id} [delete]
|
|
func (h *Handler) DeleteQuestion(c *fiber.Ctx) error {
|
|
idStr := c.Params("id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid question ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
err = h.questionsSvc.ArchiveQuestion(c.Context(), id)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to delete question",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
actorID := c.Locals("user_id").(int64)
|
|
actorRole := string(c.Locals("role").(domain.Role))
|
|
ip := c.IP()
|
|
ua := c.Get("User-Agent")
|
|
meta, _ := json.Marshal(map[string]interface{}{"question_id": id})
|
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionDeleted, domain.ResourceQuestion, &id, fmt.Sprintf("Deleted question ID: %d", id), meta, &ip, &ua)
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Question deleted successfully",
|
|
})
|
|
}
|
|
|
|
// Question Set types
|
|
|
|
type createQuestionSetReq struct {
|
|
Title string `json:"title" validate:"required"`
|
|
Description *string `json:"description"`
|
|
SetType string `json:"set_type" validate:"required,oneof=PRACTICE INITIAL_ASSESSMENT QUIZ EXAM SURVEY"`
|
|
OwnerType *string `json:"owner_type"`
|
|
OwnerID *int64 `json:"owner_id"`
|
|
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"`
|
|
}
|
|
|
|
type questionSetRes struct {
|
|
ID int64 `json:"id"`
|
|
Title string `json:"title"`
|
|
Description *string `json:"description,omitempty"`
|
|
SetType string `json:"set_type"`
|
|
OwnerType *string `json:"owner_type,omitempty"`
|
|
OwnerID *int64 `json:"owner_id,omitempty"`
|
|
BannerImage *string `json:"banner_image,omitempty"`
|
|
Persona *string `json:"persona,omitempty"`
|
|
TimeLimitMinutes *int32 `json:"time_limit_minutes,omitempty"`
|
|
PassingScore *int32 `json:"passing_score,omitempty"`
|
|
ShuffleQuestions bool `json:"shuffle_questions"`
|
|
Status string `json:"status"`
|
|
SubCourseVideoID *int64 `json:"sub_course_video_id,omitempty"`
|
|
CreatedAt string `json:"created_at"`
|
|
QuestionCount *int64 `json:"question_count,omitempty"`
|
|
}
|
|
|
|
type listQuestionSetsRes struct {
|
|
QuestionSets []questionSetRes `json:"question_sets"`
|
|
TotalCount int64 `json:"total_count"`
|
|
}
|
|
|
|
// CreateQuestionSet godoc
|
|
// @Summary Create a new question set
|
|
// @Description Creates a new question set (practice, assessment, quiz, exam, or survey)
|
|
// @Tags question-sets
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body createQuestionSetReq true "Create question set payload"
|
|
// @Success 201 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/question-sets [post]
|
|
func (h *Handler) CreateQuestionSet(c *fiber.Ctx) error {
|
|
var req createQuestionSetReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid request body",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
input := domain.CreateQuestionSetInput{
|
|
Title: req.Title,
|
|
Description: req.Description,
|
|
SetType: req.SetType,
|
|
OwnerType: req.OwnerType,
|
|
OwnerID: req.OwnerID,
|
|
BannerImage: req.BannerImage,
|
|
Persona: req.Persona,
|
|
TimeLimitMinutes: req.TimeLimitMinutes,
|
|
PassingScore: req.PassingScore,
|
|
ShuffleQuestions: req.ShuffleQuestions,
|
|
Status: req.Status,
|
|
SubCourseVideoID: req.SubCourseVideoID,
|
|
}
|
|
|
|
set, err := h.questionsSvc.CreateQuestionSet(c.Context(), input)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to create question set",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
actorID := c.Locals("user_id").(int64)
|
|
actorRole := string(c.Locals("role").(domain.Role))
|
|
ip := c.IP()
|
|
ua := c.Get("User-Agent")
|
|
qsMeta, _ := json.Marshal(map[string]interface{}{"title": set.Title, "set_type": set.SetType})
|
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetCreated, domain.ResourceQuestionSet, &set.ID, "Created question set: "+set.Title, qsMeta, &ip, &ua)
|
|
|
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
|
Message: "Question set created successfully",
|
|
Data: questionSetRes{
|
|
ID: set.ID,
|
|
Title: set.Title,
|
|
Description: set.Description,
|
|
SetType: set.SetType,
|
|
OwnerType: set.OwnerType,
|
|
OwnerID: set.OwnerID,
|
|
BannerImage: set.BannerImage,
|
|
Persona: set.Persona,
|
|
TimeLimitMinutes: set.TimeLimitMinutes,
|
|
PassingScore: set.PassingScore,
|
|
ShuffleQuestions: set.ShuffleQuestions,
|
|
Status: set.Status,
|
|
SubCourseVideoID: set.SubCourseVideoID,
|
|
CreatedAt: set.CreatedAt.String(),
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetQuestionSetByID godoc
|
|
// @Summary Get question set by ID
|
|
// @Description Returns a question set with question count
|
|
// @Tags question-sets
|
|
// @Produce json
|
|
// @Param id path int true "Question Set ID"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 404 {object} domain.ErrorResponse
|
|
// @Router /api/v1/question-sets/{id} [get]
|
|
func (h *Handler) GetQuestionSetByID(c *fiber.Ctx) error {
|
|
idStr := c.Params("id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid question set ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
set, err := h.questionsSvc.GetQuestionSetByID(c.Context(), id)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
Message: "Question set not found",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
count, _ := h.questionsSvc.CountQuestionsInSet(c.Context(), id)
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Question set retrieved successfully",
|
|
Data: questionSetRes{
|
|
ID: set.ID,
|
|
Title: set.Title,
|
|
Description: set.Description,
|
|
SetType: set.SetType,
|
|
OwnerType: set.OwnerType,
|
|
OwnerID: set.OwnerID,
|
|
BannerImage: set.BannerImage,
|
|
Persona: set.Persona,
|
|
TimeLimitMinutes: set.TimeLimitMinutes,
|
|
PassingScore: set.PassingScore,
|
|
ShuffleQuestions: set.ShuffleQuestions,
|
|
Status: set.Status,
|
|
SubCourseVideoID: set.SubCourseVideoID,
|
|
CreatedAt: set.CreatedAt.String(),
|
|
QuestionCount: &count,
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetQuestionSetsByType godoc
|
|
// @Summary Get question sets by type
|
|
// @Description Returns a paginated list of question sets filtered by type
|
|
// @Tags question-sets
|
|
// @Produce json
|
|
// @Param set_type query string true "Set type (PRACTICE, INITIAL_ASSESSMENT, QUIZ, EXAM, SURVEY)"
|
|
// @Param limit query int false "Limit" default(10)
|
|
// @Param offset query int false "Offset" default(0)
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/question-sets [get]
|
|
func (h *Handler) GetQuestionSetsByType(c *fiber.Ctx) error {
|
|
setType := c.Query("set_type")
|
|
if setType == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "set_type query parameter is required",
|
|
})
|
|
}
|
|
|
|
limitStr := c.Query("limit", "10")
|
|
offsetStr := c.Query("offset", "0")
|
|
limit, _ := strconv.Atoi(limitStr)
|
|
offset, _ := strconv.Atoi(offsetStr)
|
|
|
|
sets, totalCount, err := h.questionsSvc.GetQuestionSetsByType(c.Context(), setType, int32(limit), int32(offset))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to get question sets",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
var setResponses []questionSetRes
|
|
for _, s := range sets {
|
|
setResponses = append(setResponses, questionSetRes{
|
|
ID: s.ID,
|
|
Title: s.Title,
|
|
Description: s.Description,
|
|
SetType: s.SetType,
|
|
OwnerType: s.OwnerType,
|
|
OwnerID: s.OwnerID,
|
|
BannerImage: s.BannerImage,
|
|
Persona: s.Persona,
|
|
TimeLimitMinutes: s.TimeLimitMinutes,
|
|
PassingScore: s.PassingScore,
|
|
ShuffleQuestions: s.ShuffleQuestions,
|
|
Status: s.Status,
|
|
SubCourseVideoID: s.SubCourseVideoID,
|
|
CreatedAt: s.CreatedAt.String(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Question sets retrieved successfully",
|
|
Data: listQuestionSetsRes{
|
|
QuestionSets: setResponses,
|
|
TotalCount: totalCount,
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetQuestionSetsByOwner godoc
|
|
// @Summary Get question sets by owner
|
|
// @Description Returns question sets for a specific owner (e.g., sub-course)
|
|
// @Tags question-sets
|
|
// @Produce json
|
|
// @Param owner_type query string true "Owner type (SUB_COURSE, COURSE, CATEGORY, STANDALONE)"
|
|
// @Param owner_id query int true "Owner ID"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/question-sets/by-owner [get]
|
|
func (h *Handler) GetQuestionSetsByOwner(c *fiber.Ctx) error {
|
|
ownerType := c.Query("owner_type")
|
|
ownerIDStr := c.Query("owner_id")
|
|
|
|
if ownerType == "" || ownerIDStr == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "owner_type and owner_id query parameters are required",
|
|
})
|
|
}
|
|
|
|
ownerID, err := strconv.ParseInt(ownerIDStr, 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid owner_id",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
sets, err := h.questionsSvc.GetQuestionSetsByOwner(c.Context(), ownerType, ownerID)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to get question sets",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
var setResponses []questionSetRes
|
|
for _, s := range sets {
|
|
setResponses = append(setResponses, questionSetRes{
|
|
ID: s.ID,
|
|
Title: s.Title,
|
|
Description: s.Description,
|
|
SetType: s.SetType,
|
|
OwnerType: s.OwnerType,
|
|
OwnerID: s.OwnerID,
|
|
BannerImage: s.BannerImage,
|
|
Persona: s.Persona,
|
|
TimeLimitMinutes: s.TimeLimitMinutes,
|
|
PassingScore: s.PassingScore,
|
|
ShuffleQuestions: s.ShuffleQuestions,
|
|
Status: s.Status,
|
|
SubCourseVideoID: s.SubCourseVideoID,
|
|
CreatedAt: s.CreatedAt.String(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Question sets retrieved successfully",
|
|
Data: setResponses,
|
|
})
|
|
}
|
|
|
|
type updateQuestionSetReq struct {
|
|
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"`
|
|
}
|
|
|
|
// UpdateQuestionSet godoc
|
|
// @Summary Update a question set
|
|
// @Description Updates a question set's properties
|
|
// @Tags question-sets
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path int true "Question Set ID"
|
|
// @Param body body updateQuestionSetReq true "Update question set payload"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/question-sets/{id} [put]
|
|
func (h *Handler) UpdateQuestionSet(c *fiber.Ctx) error {
|
|
idStr := c.Params("id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid question set ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
var req updateQuestionSetReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid request body",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
title := ""
|
|
if req.Title != nil {
|
|
title = *req.Title
|
|
}
|
|
|
|
input := domain.CreateQuestionSetInput{
|
|
Title: title,
|
|
Description: req.Description,
|
|
BannerImage: req.BannerImage,
|
|
Persona: req.Persona,
|
|
TimeLimitMinutes: req.TimeLimitMinutes,
|
|
PassingScore: req.PassingScore,
|
|
ShuffleQuestions: req.ShuffleQuestions,
|
|
Status: req.Status,
|
|
SubCourseVideoID: req.SubCourseVideoID,
|
|
}
|
|
|
|
err = h.questionsSvc.UpdateQuestionSet(c.Context(), id, input)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to update question set",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
actorID := c.Locals("user_id").(int64)
|
|
actorRole := string(c.Locals("role").(domain.Role))
|
|
ip := c.IP()
|
|
ua := c.Get("User-Agent")
|
|
meta, _ := json.Marshal(map[string]interface{}{"question_set_id": id})
|
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetUpdated, domain.ResourceQuestionSet, &id, fmt.Sprintf("Updated question set ID: %d", id), meta, &ip, &ua)
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Question set updated successfully",
|
|
})
|
|
}
|
|
|
|
// DeleteQuestionSet godoc
|
|
// @Summary Delete a question set
|
|
// @Description Archives a question set (soft delete)
|
|
// @Tags question-sets
|
|
// @Produce json
|
|
// @Param id path int true "Question Set ID"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/question-sets/{id} [delete]
|
|
func (h *Handler) DeleteQuestionSet(c *fiber.Ctx) error {
|
|
idStr := c.Params("id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid question set ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
err = h.questionsSvc.ArchiveQuestionSet(c.Context(), id)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to delete question set",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
actorID := c.Locals("user_id").(int64)
|
|
actorRole := string(c.Locals("role").(domain.Role))
|
|
ip := c.IP()
|
|
ua := c.Get("User-Agent")
|
|
meta, _ := json.Marshal(map[string]interface{}{"question_set_id": id})
|
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetDeleted, domain.ResourceQuestionSet, &id, fmt.Sprintf("Deleted question set ID: %d", id), meta, &ip, &ua)
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Question set deleted successfully",
|
|
})
|
|
}
|
|
|
|
// Question Set Items
|
|
|
|
type addQuestionToSetReq struct {
|
|
QuestionID int64 `json:"question_id" validate:"required"`
|
|
DisplayOrder *int32 `json:"display_order"`
|
|
}
|
|
|
|
type questionSetItemRes struct {
|
|
ID int64 `json:"id"`
|
|
SetID int64 `json:"set_id"`
|
|
QuestionID int64 `json:"question_id"`
|
|
DisplayOrder int32 `json:"display_order"`
|
|
QuestionText string `json:"question_text"`
|
|
QuestionType string `json:"question_type"`
|
|
DifficultyLevel *string `json:"difficulty_level,omitempty"`
|
|
Points int32 `json:"points"`
|
|
Explanation *string `json:"explanation,omitempty"`
|
|
Tips *string `json:"tips,omitempty"`
|
|
VoicePrompt *string `json:"voice_prompt,omitempty"`
|
|
QuestionStatus string `json:"question_status"`
|
|
}
|
|
|
|
// AddQuestionToSet godoc
|
|
// @Summary Add question to set
|
|
// @Description Links a question to a question set
|
|
// @Tags question-set-items
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param setId path int true "Question Set ID"
|
|
// @Param body body addQuestionToSetReq true "Add question to set payload"
|
|
// @Success 201 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/question-sets/{setId}/questions [post]
|
|
func (h *Handler) AddQuestionToSet(c *fiber.Ctx) error {
|
|
setIDStr := c.Params("setId")
|
|
setID, err := strconv.ParseInt(setIDStr, 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid set ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
var req addQuestionToSetReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid request body",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
item, err := h.questionsSvc.AddQuestionToSet(c.Context(), setID, req.QuestionID, req.DisplayOrder)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to add question to set",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
|
Message: "Question added to set successfully",
|
|
Data: map[string]interface{}{
|
|
"id": item.ID,
|
|
"set_id": item.SetID,
|
|
"question_id": item.QuestionID,
|
|
"display_order": item.DisplayOrder,
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetQuestionSetItems godoc
|
|
// @Summary Get questions in set
|
|
// @Description Returns all questions in a question set with details
|
|
// @Tags question-set-items
|
|
// @Produce json
|
|
// @Param setId path int true "Question Set ID"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/question-sets/{setId}/questions [get]
|
|
func (h *Handler) GetQuestionsInSet(c *fiber.Ctx) error {
|
|
setIDStr := c.Params("setId")
|
|
setID, err := strconv.ParseInt(setIDStr, 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid set ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
items, err := h.questionsSvc.GetQuestionSetItems(c.Context(), setID)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to get questions in set",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
var itemResponses []questionSetItemRes
|
|
for _, item := range items {
|
|
itemResponses = append(itemResponses, questionSetItemRes{
|
|
ID: item.ID,
|
|
SetID: item.SetID,
|
|
QuestionID: item.QuestionID,
|
|
DisplayOrder: item.DisplayOrder,
|
|
QuestionText: item.QuestionText,
|
|
QuestionType: item.QuestionType,
|
|
DifficultyLevel: item.DifficultyLevel,
|
|
Points: item.Points,
|
|
Explanation: item.Explanation,
|
|
Tips: item.Tips,
|
|
VoicePrompt: item.VoicePrompt,
|
|
QuestionStatus: item.QuestionStatus,
|
|
})
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Questions retrieved successfully",
|
|
Data: itemResponses,
|
|
})
|
|
}
|
|
|
|
// RemoveQuestionFromSet godoc
|
|
// @Summary Remove question from set
|
|
// @Description Unlinks a question from a question set
|
|
// @Tags question-set-items
|
|
// @Produce json
|
|
// @Param setId path int true "Question Set ID"
|
|
// @Param questionId path int true "Question ID"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/question-sets/{setId}/questions/{questionId} [delete]
|
|
func (h *Handler) RemoveQuestionFromSet(c *fiber.Ctx) error {
|
|
setIDStr := c.Params("setId")
|
|
setID, err := strconv.ParseInt(setIDStr, 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid set ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
questionIDStr := c.Params("questionId")
|
|
questionID, err := strconv.ParseInt(questionIDStr, 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid question ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
err = h.questionsSvc.RemoveQuestionFromSet(c.Context(), setID, questionID)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to remove question from set",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Question removed from set successfully",
|
|
})
|
|
}
|
|
|
|
type updateQuestionOrderReq struct {
|
|
DisplayOrder int32 `json:"display_order" validate:"required"`
|
|
}
|
|
|
|
// UpdateQuestionOrder godoc
|
|
// @Summary Update question order in set
|
|
// @Description Updates the display order of a question in a set
|
|
// @Tags question-set-items
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param setId path int true "Question Set ID"
|
|
// @Param questionId path int true "Question ID"
|
|
// @Param body body updateQuestionOrderReq true "Update order payload"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/question-sets/{setId}/questions/{questionId}/order [put]
|
|
func (h *Handler) UpdateQuestionOrderInSet(c *fiber.Ctx) error {
|
|
setIDStr := c.Params("setId")
|
|
setID, err := strconv.ParseInt(setIDStr, 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid set ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
questionIDStr := c.Params("questionId")
|
|
questionID, err := strconv.ParseInt(questionIDStr, 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid question ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
var req updateQuestionOrderReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid request body",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
err = h.questionsSvc.UpdateQuestionOrder(c.Context(), setID, questionID, req.DisplayOrder)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to update question order",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Question order updated successfully",
|
|
})
|
|
}
|
|
|
|
// User Persona types for question sets
|
|
|
|
type userPersonaRes struct {
|
|
ID int64 `json:"id"`
|
|
FirstName *string `json:"first_name,omitempty"`
|
|
LastName *string `json:"last_name,omitempty"`
|
|
NickName *string `json:"nick_name,omitempty"`
|
|
ProfilePictureURL *string `json:"profile_picture_url,omitempty"`
|
|
Role string `json:"role"`
|
|
DisplayOrder int32 `json:"display_order"`
|
|
}
|
|
|
|
type addUserPersonaReq struct {
|
|
UserID int64 `json:"user_id" validate:"required"`
|
|
DisplayOrder int32 `json:"display_order"`
|
|
}
|
|
|
|
// GetUserPersonasByQuestionSet godoc
|
|
// @Summary Get user personas for a question set
|
|
// @Description Returns all users assigned as personas to a question set (practice)
|
|
// @Tags question-sets
|
|
// @Produce json
|
|
// @Param setId path int true "Question Set ID"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/question-sets/{setId}/personas [get]
|
|
func (h *Handler) GetUserPersonasByQuestionSet(c *fiber.Ctx) error {
|
|
setIDStr := c.Params("setId")
|
|
setID, err := strconv.ParseInt(setIDStr, 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid set ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
personas, err := h.questionsSvc.GetUserPersonasByQuestionSetID(c.Context(), setID)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to get personas",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
result := make([]userPersonaRes, len(personas))
|
|
for i, p := range personas {
|
|
result[i] = userPersonaRes{
|
|
ID: p.ID,
|
|
FirstName: p.FirstName,
|
|
LastName: p.LastName,
|
|
NickName: p.NickName,
|
|
ProfilePictureURL: p.ProfilePictureURL,
|
|
Role: p.Role,
|
|
DisplayOrder: p.DisplayOrder,
|
|
}
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Personas retrieved successfully",
|
|
Data: result,
|
|
})
|
|
}
|
|
|
|
// AddUserPersonaToQuestionSet godoc
|
|
// @Summary Add a user as persona to a question set
|
|
// @Description Links a user as a persona to a question set (practice)
|
|
// @Tags question-sets
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param setId path int true "Question Set ID"
|
|
// @Param body body addUserPersonaReq true "Add user persona payload"
|
|
// @Success 201 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/question-sets/{setId}/personas [post]
|
|
func (h *Handler) AddUserPersonaToQuestionSet(c *fiber.Ctx) error {
|
|
setIDStr := c.Params("setId")
|
|
setID, err := strconv.ParseInt(setIDStr, 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid set ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
var req addUserPersonaReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid request body",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
err = h.questionsSvc.AddUserPersonaToQuestionSet(c.Context(), setID, req.UserID, req.DisplayOrder)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to add persona to question set",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
|
Message: "Persona added to question set successfully",
|
|
})
|
|
}
|
|
|
|
// RemoveUserPersonaFromQuestionSet godoc
|
|
// @Summary Remove a user persona from a question set
|
|
// @Description Unlinks a user as persona from a question set (practice)
|
|
// @Tags question-sets
|
|
// @Produce json
|
|
// @Param setId path int true "Question Set ID"
|
|
// @Param userId path int true "User ID"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/question-sets/{setId}/personas/{userId} [delete]
|
|
func (h *Handler) RemoveUserPersonaFromQuestionSet(c *fiber.Ctx) error {
|
|
setIDStr := c.Params("setId")
|
|
setID, err := strconv.ParseInt(setIDStr, 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid set ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
userIDStr := c.Params("userId")
|
|
userID, err := strconv.ParseInt(userIDStr, 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid user ID",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
err = h.questionsSvc.RemoveUserPersonaFromQuestionSet(c.Context(), setID, userID)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to remove persona from question set",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(domain.Response{
|
|
Message: "Persona removed from question set successfully",
|
|
})
|
|
}
|