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>
This commit is contained in:
parent
08a2886654
commit
33355a4b23
File diff suppressed because it is too large
Load Diff
|
|
@ -65,7 +65,7 @@ If you create/update dynamic definitions:
|
|||
|
||||
## Step 0 (Optional): Upload Media
|
||||
|
||||
Use this when question content references audio/image URLs.
|
||||
Use this when question content references audio/image/PDF URLs (e.g. dynamic `IMAGE`, `AUDIO_CLIP`, or `PDF_ATTACHMENT` stimulus).
|
||||
|
||||
### Endpoint
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ Use this when question content references audio/image URLs.
|
|||
### Form fields
|
||||
|
||||
- `file`: binary
|
||||
- `media_type`: `image` or `audio` or `video`
|
||||
- `media_type`: `image`, `audio`, `video`, or `pdf` (PDF is stored in MinIO; response includes presigned `url` and `object_key`)
|
||||
|
||||
### Example success response (shape)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ const (
|
|||
StimulusSelectMissingWords StimulusComponentKind = "SELECT_MISSING_WORDS"
|
||||
StimulusTable StimulusComponentKind = "TABLE"
|
||||
StimulusFlowChart StimulusComponentKind = "FLOW_CHART"
|
||||
// StimulusPDFAttachment is question-side PDF content (URL from MinIO upload or HTTPS).
|
||||
StimulusPDFAttachment StimulusComponentKind = "PDF_ATTACHMENT"
|
||||
)
|
||||
|
||||
// Response-side components for the question-type builder (Section B — answer types).
|
||||
|
|
@ -75,6 +77,7 @@ var (
|
|||
StimulusSelectMissingWords,
|
||||
StimulusTable,
|
||||
StimulusFlowChart,
|
||||
StimulusPDFAttachment,
|
||||
}
|
||||
stimulusSet map[string]struct{}
|
||||
|
||||
|
|
@ -564,27 +567,48 @@ var stimulusTextKindsForQuestionText = []string{
|
|||
string(StimulusTextPassage),
|
||||
}
|
||||
|
||||
// ResolveQuestionTextForWrite returns the questions.question_text column value for create/update.
|
||||
// explicit is the optional request field; existing is the current DB value on update (empty on create).
|
||||
// For DYNAMIC questions, text may come from stimulus components (QUESTION_TEXT, INSTRUCTION, TEXT_PASSAGE).
|
||||
func ResolveQuestionTextForWrite(explicit string, questionType string, payload *DynamicQuestionPayload, existing string) (string, error) {
|
||||
explicit = strings.TrimSpace(explicit)
|
||||
if explicit != "" {
|
||||
return explicit, nil
|
||||
// UsesDynamicQuestionPayload reports whether the runtime type stores prompt content in dynamic_payload only.
|
||||
func UsesDynamicQuestionPayload(questionType string) bool {
|
||||
return strings.ToUpper(strings.TrimSpace(questionType)) == "DYNAMIC"
|
||||
}
|
||||
|
||||
// ValidateQuestionTextNotAllowedForDynamic rejects top-level question_text on DYNAMIC create/update requests.
|
||||
func ValidateQuestionTextNotAllowedForDynamic(questionType string, explicit string) error {
|
||||
if !UsesDynamicQuestionPayload(questionType) {
|
||||
return nil
|
||||
}
|
||||
if derived := stimulusTextFromPayload(payload); derived != "" {
|
||||
if strings.TrimSpace(explicit) != "" {
|
||||
return fmt.Errorf("question_text is not used for DYNAMIC questions; set prompt text in dynamic_payload stimulus (QUESTION_TEXT, INSTRUCTION, or TEXT_PASSAGE)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveDynamicStoredQuestionText returns the questions.question_text column value for DYNAMIC rows
|
||||
// (search/activity logs only; clients read prompt text from dynamic_payload).
|
||||
func ResolveDynamicStoredQuestionText(payload *DynamicQuestionPayload, existing string) (string, error) {
|
||||
if derived := StimulusTextFromPayload(payload); derived != "" {
|
||||
return derived, nil
|
||||
}
|
||||
if strings.TrimSpace(existing) != "" {
|
||||
return strings.TrimSpace(existing), nil
|
||||
}
|
||||
if strings.ToUpper(strings.TrimSpace(questionType)) == "DYNAMIC" {
|
||||
return "", fmt.Errorf("provide question_text or stimulus text (QUESTION_TEXT, INSTRUCTION, or TEXT_PASSAGE) in dynamic_payload")
|
||||
}
|
||||
return "", fmt.Errorf("question_text is required")
|
||||
return "", fmt.Errorf("dynamic_payload must include prompt text (QUESTION_TEXT, INSTRUCTION, or TEXT_PASSAGE)")
|
||||
}
|
||||
|
||||
func stimulusTextFromPayload(payload *DynamicQuestionPayload) string {
|
||||
// QuestionTextJSONField returns question_text for API responses. Omitted (nil) for DYNAMIC questions.
|
||||
func QuestionTextJSONField(questionType string, stored string) *string {
|
||||
if UsesDynamicQuestionPayload(questionType) {
|
||||
return nil
|
||||
}
|
||||
stored = strings.TrimSpace(stored)
|
||||
if stored == "" {
|
||||
return nil
|
||||
}
|
||||
return &stored
|
||||
}
|
||||
|
||||
// StimulusTextFromPayload extracts the first non-empty prompt string from allowed stimulus kinds.
|
||||
func StimulusTextFromPayload(payload *DynamicQuestionPayload) string {
|
||||
if payload == nil {
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,20 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func TestStimulusComponentCatalog_includesPDFAttachment(t *testing.T) {
|
||||
catalog := StimulusComponentCatalog()
|
||||
found := false
|
||||
for _, k := range catalog {
|
||||
if k == string(StimulusPDFAttachment) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected PDF_ATTACHMENT in stimulus catalog, got %v", catalog)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDynamicQuestionTypeDefinition_valid(t *testing.T) {
|
||||
err := ValidateDynamicQuestionTypeDefinition(
|
||||
[]string{"INSTRUCTION", "IMAGE"},
|
||||
|
|
@ -15,6 +29,16 @@ func TestValidateDynamicQuestionTypeDefinition_valid(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestValidateDynamicQuestionTypeDefinition_pdfAttachmentStimulus(t *testing.T) {
|
||||
err := ValidateDynamicQuestionTypeDefinition(
|
||||
[]string{"QUESTION_TEXT", "PDF_ATTACHMENT"},
|
||||
[]string{"SHORT_ANSWER"},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDynamicQuestionTypeDefinition_unknownStimulus(t *testing.T) {
|
||||
err := ValidateDynamicQuestionTypeDefinition(
|
||||
[]string{"NOT_A_KIND"},
|
||||
|
|
@ -177,58 +201,65 @@ func TestResolveQuestionTypeDefinitionForQuestion_linkedDefinition(t *testing.T)
|
|||
}
|
||||
}
|
||||
|
||||
func TestResolveQuestionTextForWrite_explicit(t *testing.T) {
|
||||
got, err := ResolveQuestionTextForWrite("Hello", "DYNAMIC", nil, "")
|
||||
if err != nil || got != "Hello" {
|
||||
t.Fatalf("expected Hello, got %q err=%v", got, err)
|
||||
func TestValidateQuestionTextNotAllowedForDynamic(t *testing.T) {
|
||||
if err := ValidateQuestionTextNotAllowedForDynamic("DYNAMIC", "nope"); err == nil {
|
||||
t.Fatal("expected error when question_text sent for DYNAMIC")
|
||||
}
|
||||
if err := ValidateQuestionTextNotAllowedForDynamic("DYNAMIC", ""); err != nil {
|
||||
t.Fatalf("expected nil for empty explicit, got %v", err)
|
||||
}
|
||||
if err := ValidateQuestionTextNotAllowedForDynamic("MCQ", "ok"); err != nil {
|
||||
t.Fatalf("expected nil for legacy, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveQuestionTextForWrite_fromQUESTION_TEXTStimulus(t *testing.T) {
|
||||
func TestResolveDynamicStoredQuestionText_fromQUESTION_TEXTStimulus(t *testing.T) {
|
||||
payload := &DynamicQuestionPayload{
|
||||
Stimulus: []DynamicElementInstance{
|
||||
{ID: "prompt", Kind: "QUESTION_TEXT", Value: "Pick the correct answer."},
|
||||
},
|
||||
}
|
||||
got, err := ResolveQuestionTextForWrite("", "DYNAMIC", payload, "")
|
||||
got, err := ResolveDynamicStoredQuestionText(payload, "")
|
||||
if err != nil || got != "Pick the correct answer." {
|
||||
t.Fatalf("expected derived text, got %q err=%v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveQuestionTextForWrite_fromINSTRUCTIONStimulus(t *testing.T) {
|
||||
func TestResolveDynamicStoredQuestionText_fromINSTRUCTIONStimulus(t *testing.T) {
|
||||
payload := &DynamicQuestionPayload{
|
||||
Stimulus: []DynamicElementInstance{
|
||||
{ID: "prompt", Kind: "INSTRUCTION", Value: "Choose true or false."},
|
||||
},
|
||||
}
|
||||
got, err := ResolveQuestionTextForWrite("", "DYNAMIC", payload, "")
|
||||
got, err := ResolveDynamicStoredQuestionText(payload, "")
|
||||
if err != nil || got != "Choose true or false." {
|
||||
t.Fatalf("expected derived text, got %q err=%v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveQuestionTextForWrite_dynamicMissingText(t *testing.T) {
|
||||
_, err := ResolveQuestionTextForWrite("", "DYNAMIC", &DynamicQuestionPayload{}, "")
|
||||
func TestResolveDynamicStoredQuestionText_missingText(t *testing.T) {
|
||||
_, err := ResolveDynamicStoredQuestionText(&DynamicQuestionPayload{}, "")
|
||||
if err == nil {
|
||||
t.Fatal("expected error when no question text source")
|
||||
t.Fatal("expected error when no prompt in payload")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveQuestionTextForWrite_legacyRequiresText(t *testing.T) {
|
||||
_, err := ResolveQuestionTextForWrite("", "MCQ", nil, "")
|
||||
if err == nil || !strings.Contains(err.Error(), "question_text is required") {
|
||||
t.Fatalf("expected legacy required error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveQuestionTextForWrite_updateKeepsExisting(t *testing.T) {
|
||||
got, err := ResolveQuestionTextForWrite("", "DYNAMIC", nil, "Previous title")
|
||||
func TestResolveDynamicStoredQuestionText_updateKeepsExisting(t *testing.T) {
|
||||
got, err := ResolveDynamicStoredQuestionText(nil, "Previous title")
|
||||
if err != nil || got != "Previous title" {
|
||||
t.Fatalf("expected existing text, got %q err=%v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuestionTextJSONField_omitsDynamic(t *testing.T) {
|
||||
if got := QuestionTextJSONField("DYNAMIC", "stored"); got != nil {
|
||||
t.Fatalf("expected nil for DYNAMIC, got %v", got)
|
||||
}
|
||||
if got := QuestionTextJSONField("MCQ", "Pick one"); got == nil || *got != "Pick one" {
|
||||
t.Fatalf("expected MCQ text, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummarizeQuestionSetQuestionTypes_mergesLegacyAndLinked(t *testing.T) {
|
||||
id := int64(10)
|
||||
summary := SummarizeQuestionSetQuestionTypes([]QuestionSetQuestionTypeGroup{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -174,11 +175,11 @@ func (h *Handler) RefreshFileURL(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
// UploadMedia uploads an image/audio/video file and returns its URL and key.
|
||||
// UploadMedia uploads an image/audio/video/pdf file and returns its URL and key.
|
||||
// @Summary Upload media file
|
||||
// @Tags files
|
||||
// @Accept multipart/form-data
|
||||
// @Param media_type formData string true "Media type: image|audio|video"
|
||||
// @Param media_type formData string true "Media type: image|audio|video|pdf"
|
||||
// @Param file formData file true "Media file"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/files/upload [post]
|
||||
|
|
@ -205,10 +206,10 @@ func (h *Handler) UploadMedia(c *fiber.Ctx) error {
|
|||
if mediaType == "" {
|
||||
mediaType = "file"
|
||||
}
|
||||
if mediaType != "image" && mediaType != "audio" && mediaType != "video" {
|
||||
if mediaType != "image" && mediaType != "audio" && mediaType != "video" && mediaType != "pdf" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid media_type",
|
||||
Error: "media_type must be one of: image, audio, video",
|
||||
Error: "media_type must be one of: image, audio, video, pdf",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -218,6 +219,8 @@ func (h *Handler) UploadMedia(c *fiber.Ctx) error {
|
|||
maxSize = 10 * 1024 * 1024
|
||||
case "audio":
|
||||
maxSize = 50 * 1024 * 1024
|
||||
case "pdf":
|
||||
maxSize = 25 * 1024 * 1024
|
||||
case "video":
|
||||
maxSize = 500 * 1024 * 1024
|
||||
}
|
||||
|
|
@ -226,9 +229,9 @@ func (h *Handler) UploadMedia(c *fiber.Ctx) error {
|
|||
Message: "Vimeo service is not available for video uploads",
|
||||
})
|
||||
}
|
||||
if (mediaType == "image" || mediaType == "audio") && h.minioSvc == nil {
|
||||
if (mediaType == "image" || mediaType == "audio" || mediaType == "pdf") && h.minioSvc == nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{
|
||||
Message: "MinIO service is not available for image/audio uploads",
|
||||
Message: "MinIO service is not available for image/audio/pdf uploads",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -396,6 +399,15 @@ func normalizeAndValidateMediaContentType(mediaType, contentType, fileName strin
|
|||
if !strings.HasPrefix(contentType, "video/") {
|
||||
return "", fmt.Errorf("only video files are allowed")
|
||||
}
|
||||
case "pdf":
|
||||
if contentType == "application/octet-stream" {
|
||||
if ext := strings.ToLower(path.Ext(fileName)); ext == ".pdf" {
|
||||
contentType = "application/pdf"
|
||||
}
|
||||
}
|
||||
if contentType != "application/pdf" {
|
||||
return "", fmt.Errorf("only PDF files are allowed")
|
||||
}
|
||||
}
|
||||
|
||||
return contentType, nil
|
||||
|
|
|
|||
20
internal/web_server/handlers/file_handler_media_test.go
Normal file
20
internal/web_server/handlers/file_handler_media_test.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package handlers
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeAndValidateMediaContentType_pdf(t *testing.T) {
|
||||
got, err := normalizeAndValidateMediaContentType("pdf", "application/pdf", "reading-passage.pdf")
|
||||
if err != nil || got != "application/pdf" {
|
||||
t.Fatalf("expected application/pdf, got %q err=%v", got, err)
|
||||
}
|
||||
|
||||
got, err = normalizeAndValidateMediaContentType("pdf", "application/octet-stream", "notes.pdf")
|
||||
if err != nil || got != "application/pdf" {
|
||||
t.Fatalf("expected pdf from extension, got %q err=%v", got, err)
|
||||
}
|
||||
|
||||
_, err = normalizeAndValidateMediaContentType("pdf", "image/png", "file.png")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for non-pdf content")
|
||||
}
|
||||
}
|
||||
|
|
@ -28,12 +28,7 @@ func (h *Handler) CreateAssessmentQuestion(c *fiber.Ctx) error {
|
|||
}
|
||||
|
||||
questionType := normalizeRuntimeQuestionType(req.QuestionType)
|
||||
questionText, err := domain.ResolveQuestionTextForWrite(
|
||||
optionalTrimmedString(req.QuestionText),
|
||||
questionType,
|
||||
req.DynamicPayload,
|
||||
"",
|
||||
)
|
||||
questionText, err := resolveStoredQuestionText(questionType, req.QuestionText, req.DynamicPayload, "")
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid question_text",
|
||||
|
|
@ -88,7 +83,7 @@ func (h *Handler) CreateAssessmentQuestion(c *fiber.Ctx) error {
|
|||
Success: true,
|
||||
Data: questionRes{
|
||||
ID: question.ID,
|
||||
QuestionText: question.QuestionText,
|
||||
QuestionText: questionTextField(question.QuestionType, question.QuestionText),
|
||||
QuestionType: question.QuestionType,
|
||||
Status: question.Status,
|
||||
CreatedAt: question.CreatedAt.String(),
|
||||
|
|
@ -136,7 +131,7 @@ func (h *Handler) ListAssessmentQuestions(c *fiber.Ctx) error {
|
|||
|
||||
questionResponses = append(questionResponses, questionRes{
|
||||
ID: q.ID,
|
||||
QuestionText: q.QuestionText,
|
||||
QuestionText: questionTextField(q.QuestionType, q.QuestionText),
|
||||
QuestionType: q.QuestionType,
|
||||
DifficultyLevel: q.DifficultyLevel,
|
||||
Points: q.Points,
|
||||
|
|
@ -207,7 +202,7 @@ func (h *Handler) GetAssessmentQuestionByID(c *fiber.Ctx) error {
|
|||
Message: "Question fetched successfully",
|
||||
Data: questionRes{
|
||||
ID: question.ID,
|
||||
QuestionText: question.QuestionText,
|
||||
QuestionText: questionTextField(question.QuestionType, question.QuestionText),
|
||||
QuestionType: question.QuestionType,
|
||||
DifficultyLevel: question.DifficultyLevel,
|
||||
Points: question.Points,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ type shortAnswerRes struct {
|
|||
|
||||
type questionRes struct {
|
||||
ID int64 `json:"id"`
|
||||
QuestionText string `json:"question_text"`
|
||||
QuestionText *string `json:"question_text,omitempty"`
|
||||
QuestionType string `json:"question_type"`
|
||||
QuestionTypeDefinitionID *int64 `json:"question_type_definition_id,omitempty"`
|
||||
DynamicPayload *domain.DynamicQuestionPayload `json:"dynamic_payload,omitempty"`
|
||||
|
|
@ -96,9 +96,36 @@ func optionalTrimmedString(s *string) string {
|
|||
return strings.TrimSpace(*s)
|
||||
}
|
||||
|
||||
func resolveStoredQuestionText(questionType string, explicit *string, payload *domain.DynamicQuestionPayload, existing string) (string, error) {
|
||||
exp := optionalTrimmedString(explicit)
|
||||
if err := domain.ValidateQuestionTextNotAllowedForDynamic(questionType, exp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if domain.UsesDynamicQuestionPayload(questionType) {
|
||||
return domain.ResolveDynamicStoredQuestionText(payload, existing)
|
||||
}
|
||||
if exp == "" {
|
||||
return "", fmt.Errorf("question_text is required")
|
||||
}
|
||||
return exp, nil
|
||||
}
|
||||
|
||||
func questionTextField(questionType string, stored string) *string {
|
||||
return domain.QuestionTextJSONField(questionType, stored)
|
||||
}
|
||||
|
||||
func activityLogQuestionSummary(questionType string, stored string, payload *domain.DynamicQuestionPayload) string {
|
||||
if domain.UsesDynamicQuestionPayload(questionType) {
|
||||
if s := domain.StimulusTextFromPayload(payload); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return stored
|
||||
}
|
||||
|
||||
// CreateQuestion godoc
|
||||
// @Summary Create a new question
|
||||
// @Description Creates a new question with options (for MCQ/TRUE_FALSE) or short answers (for SHORT_ANSWER). question_text is optional for DYNAMIC questions when stimulus includes QUESTION_TEXT, INSTRUCTION, or TEXT_PASSAGE; it is required for legacy types (MCQ, TRUE_FALSE, SHORT_ANSWER, AUDIO).
|
||||
// @Description Creates a new question with options (for MCQ/TRUE_FALSE) or short answers (for SHORT_ANSWER). DYNAMIC questions must not send question_text; use dynamic_payload stimulus instead. Legacy types require question_text.
|
||||
// @Tags questions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
|
@ -193,12 +220,7 @@ func (h *Handler) CreateQuestion(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
questionText, err := domain.ResolveQuestionTextForWrite(
|
||||
optionalTrimmedString(req.QuestionText),
|
||||
questionType,
|
||||
req.DynamicPayload,
|
||||
"",
|
||||
)
|
||||
questionText, err := resolveStoredQuestionText(questionType, req.QuestionText, req.DynamicPayload, "")
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid question_text",
|
||||
|
|
@ -240,13 +262,13 @@ func (h *Handler) CreateQuestion(c *fiber.Ctx) error {
|
|||
"question_type": question.QuestionType,
|
||||
"question_type_definition_id": req.QuestionTypeDefinitionID,
|
||||
})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionCreated, domain.ResourceQuestion, &question.ID, "Created question: "+question.QuestionText, meta, &ip, &ua)
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionCreated, domain.ResourceQuestion, &question.ID, "Created question: "+activityLogQuestionSummary(question.QuestionType, question.QuestionText, question.DynamicPayload), meta, &ip, &ua)
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||
Message: "Question created successfully",
|
||||
Data: questionRes{
|
||||
ID: question.ID,
|
||||
QuestionText: question.QuestionText,
|
||||
QuestionText: questionTextField(question.QuestionType, question.QuestionText),
|
||||
QuestionType: question.QuestionType,
|
||||
QuestionTypeDefinitionID: question.QuestionTypeDefinitionID,
|
||||
DynamicPayload: question.DynamicPayload,
|
||||
|
|
@ -319,7 +341,7 @@ func (h *Handler) GetQuestionByID(c *fiber.Ctx) error {
|
|||
Message: "Question retrieved successfully",
|
||||
Data: questionRes{
|
||||
ID: question.ID,
|
||||
QuestionText: question.QuestionText,
|
||||
QuestionText: questionTextField(question.QuestionType, question.QuestionText),
|
||||
QuestionType: question.QuestionType,
|
||||
QuestionTypeDefinitionID: question.QuestionTypeDefinitionID,
|
||||
DynamicPayload: question.DynamicPayload,
|
||||
|
|
@ -386,7 +408,7 @@ func (h *Handler) ListQuestions(c *fiber.Ctx) error {
|
|||
for _, q := range questions {
|
||||
questionResponses = append(questionResponses, questionRes{
|
||||
ID: q.ID,
|
||||
QuestionText: q.QuestionText,
|
||||
QuestionText: questionTextField(q.QuestionType, q.QuestionText),
|
||||
QuestionType: q.QuestionType,
|
||||
QuestionTypeDefinitionID: q.QuestionTypeDefinitionID,
|
||||
DynamicPayload: q.DynamicPayload,
|
||||
|
|
@ -446,7 +468,7 @@ func (h *Handler) SearchQuestions(c *fiber.Ctx) error {
|
|||
for _, q := range questions {
|
||||
questionResponses = append(questionResponses, questionRes{
|
||||
ID: q.ID,
|
||||
QuestionText: q.QuestionText,
|
||||
QuestionText: questionTextField(q.QuestionType, q.QuestionText),
|
||||
QuestionType: q.QuestionType,
|
||||
QuestionTypeDefinitionID: q.QuestionTypeDefinitionID,
|
||||
DynamicPayload: q.DynamicPayload,
|
||||
|
|
@ -604,12 +626,7 @@ func (h *Handler) UpdateQuestion(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
questionText, err := domain.ResolveQuestionTextForWrite(
|
||||
optionalTrimmedString(req.QuestionText),
|
||||
questionType,
|
||||
effectiveDynamicPayload,
|
||||
existingQuestion.QuestionText,
|
||||
)
|
||||
questionText, err := resolveStoredQuestionText(questionType, req.QuestionText, effectiveDynamicPayload, existingQuestion.QuestionText)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid question_text",
|
||||
|
|
@ -1245,7 +1262,7 @@ type questionSetItemRes struct {
|
|||
SetID int64 `json:"set_id"`
|
||||
QuestionID int64 `json:"question_id"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
QuestionText string `json:"question_text"`
|
||||
QuestionText *string `json:"question_text,omitempty"`
|
||||
QuestionType string `json:"question_type"`
|
||||
DynamicPayload *domain.DynamicQuestionPayload `json:"dynamic_payload,omitempty"`
|
||||
DifficultyLevel *string `json:"difficulty_level,omitempty"`
|
||||
|
|
@ -1274,7 +1291,7 @@ func questionSetItemsToRes(items []domain.QuestionSetItemWithQuestion) []questio
|
|||
SetID: item.SetID,
|
||||
QuestionID: item.QuestionID,
|
||||
DisplayOrder: item.DisplayOrder,
|
||||
QuestionText: item.QuestionText,
|
||||
QuestionText: questionTextField(item.QuestionType, item.QuestionText),
|
||||
QuestionType: item.QuestionType,
|
||||
DynamicPayload: item.DynamicPayload,
|
||||
DifficultyLevel: item.DifficultyLevel,
|
||||
|
|
@ -1477,7 +1494,7 @@ func (h *Handler) GetQuestionsInSet(c *fiber.Ctx) error {
|
|||
|
||||
questionResponses = append(questionResponses, questionRes{
|
||||
ID: question.ID,
|
||||
QuestionText: question.QuestionText,
|
||||
QuestionText: questionTextField(question.QuestionType, question.QuestionText),
|
||||
QuestionType: question.QuestionType,
|
||||
QuestionTypeDefinitionID: question.QuestionTypeDefinitionID,
|
||||
DynamicPayload: question.DynamicPayload,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user