From 27141b7d62d1a518baea5bba0d99f1067a05d4b9 Mon Sep 17 00:00:00 2001 From: Wayne Ngo Date: Mon, 13 Apr 2026 23:41:34 -0700 Subject: [PATCH] handlers and validation --- internal/core/services/assessment_service.go | 6 +- internal/handlers/assessments.go | 213 +++++++++++++++++-- 2 files changed, 201 insertions(+), 18 deletions(-) diff --git a/internal/core/services/assessment_service.go b/internal/core/services/assessment_service.go index 117ef90..c45cd29 100644 --- a/internal/core/services/assessment_service.go +++ b/internal/core/services/assessment_service.go @@ -160,16 +160,16 @@ func validateQuestion(q *models.Question) error { func validateQuestionType(typ string) error { switch typ { - case "mcq", "multi_select", "short_answer", "code": + case "mcq_single", "mcq_multi", "short_answer", "code": return nil default: - return FieldError{Field: "type", Message: "must be mcq, multi_select, short_answer, or code"} + return FieldError{Field: "type", Message: "must be mcq_single, mcq_multi, short_answer, or code"} } } func questionTypeRequiresOptions(typ string) bool { switch typ { - case "mcq", "multi_select": + case "mcq_single", "mcq_multi": return true default: return false diff --git a/internal/handlers/assessments.go b/internal/handlers/assessments.go index 6a1e317..9924955 100644 --- a/internal/handlers/assessments.go +++ b/internal/handlers/assessments.go @@ -1,36 +1,219 @@ package handlers import ( + "CodeSCE/internal/core/models" + "CodeSCE/internal/core/services" + "encoding/json" + "errors" "net/http" + "strconv" + "strings" + "github.com/gin-gonic/gin" ) +var assessmentService *services.AssessmentService + +func SetAssessmentService(s *services.AssessmentService) { + assessmentService = s +} + func CreateAssessment(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"message": "create assessment route wired"}) + if assessmentService == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "assessment service not configured"}) + return + } + + var req struct { + Title string `json:"title"` + Description *string `json:"description"` + DurationMinutes int `json:"duration_minutes"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) + return + } + if strings.TrimSpace(req.Title) == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "title: required"}) + return + } + if req.DurationMinutes <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "duration_minutes: must be greater than 0"}) + return + } + + assessment := models.AssessmentTemplate{ + Title: req.Title, + Description: req.Description, + DurationMinutes: req.DurationMinutes, + } + + if err := assessmentService.CreateAssessment(c.Request.Context(), &assessment); err != nil { + handleAssessmentError(c, err) + return + } + + c.JSON(http.StatusCreated, assessment) } func ListAssessments(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"message": "list assessments route wired"}) + if assessmentService == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "assessment service not configured"}) + return + } + + assessments, err := assessmentService.ListAssessments(c.Request.Context()) + if err != nil { + handleAssessmentError(c, err) + return + } + + c.JSON(http.StatusOK, assessments) } func GetAssessment(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "message": "get assessment route wired", - "id": c.Param("id"), - }) + if assessmentService == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "assessment service not configured"}) + return + } + + assessmentID, err := parseIDParam(c, "id") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + assessment, svcErr := assessmentService.GetAssessment(c.Request.Context(), assessmentID) + if svcErr != nil { + handleAssessmentError(c, svcErr) + return + } + + c.JSON(http.StatusOK, assessment) } func AddQuestion(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "message": "add question route wired", - "id": c.Param("id"), - }) + if assessmentService == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "assessment service not configured"}) + return + } + + assessmentID, err := parseIDParam(c, "id") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var req struct { + Type string `json:"type"` + Prompt string `json:"prompt"` + Options *json.RawMessage `json:"options"` + CorrectAnswer *json.RawMessage `json:"correct_answer"` + Score int `json:"score"` + Language *string `json:"language"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) + return + } + if strings.TrimSpace(req.Type) == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "type: required"}) + return + } + if !isValidQuestionType(req.Type) { + c.JSON(http.StatusBadRequest, gin.H{"error": "type: must be mcq_single, mcq_multi, short_answer, or code"}) + return + } + if strings.TrimSpace(req.Prompt) == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "prompt: required"}) + return + } + if req.Score <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "score: must be greater than 0"}) + return + } + + question := models.Question{ + Type: req.Type, + Prompt: req.Prompt, + Options: req.Options, + CorrectAnswer: req.CorrectAnswer, + Score: req.Score, + Language: req.Language, + } + + if err := assessmentService.AddQuestion(c.Request.Context(), assessmentID, &question); err != nil { + handleAssessmentError(c, err) + return + } + + c.JSON(http.StatusCreated, question) } func AddTestCases(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "message": "add test cases route wired", - "id": c.Param("id"), - "qid": c.Param("qid"), - }) + if assessmentService == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "assessment service not configured"}) + return + } + + assessmentID, err := parseIDParam(c, "id") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + questionID, err := parseIDParam(c, "qid") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var payload struct { + TestCases []models.TestCase `json:"testCases"` + } + if err := c.ShouldBindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) + return + } + + if err := assessmentService.AddTestCases(c.Request.Context(), assessmentID, questionID, payload.TestCases); err != nil { + handleAssessmentError(c, err) + return + } + + c.JSON(http.StatusCreated, gin.H{"testCases": payload.TestCases}) +} + +func parseIDParam(c *gin.Context, name string) (int64, error) { + value := c.Param(name) + id, err := strconv.ParseInt(value, 10, 64) + if err != nil || id <= 0 { + return 0, errors.New("invalid " + name) + } + return id, nil +} + +func handleAssessmentError(c *gin.Context, err error) { + var fieldErr services.FieldError + if errors.As(err, &fieldErr) { + c.JSON(http.StatusBadRequest, gin.H{"error": fieldErr.Error()}) + return + } + + switch { + case errors.Is(err, services.ErrAssessmentNotFound), errors.Is(err, services.ErrQuestionNotFound): + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + case errors.Is(err, services.ErrWrongAssessment): + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + default: + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + } +} + +func isValidQuestionType(questionType string) bool { + switch questionType { + case "mcq_single", "mcq_multi", "short_answer", "code": + return true + default: + return false + } }