refactor(go/handlers): split iace_handler and training_handlers into focused files
iace_handler.go (2706 LOC) split into 9 files: - iace_handler.go: struct, constructor, shared helpers (~156 LOC) - iace_handler_projects.go: project CRUD + InitFromProfile (~310 LOC) - iace_handler_components.go: components + classification (~387 LOC) - iace_handler_hazards.go: hazard library, CRUD, risk assessment (~469 LOC) - iace_handler_mitigations.go: mitigations, evidence, verification plans (~293 LOC) - iace_handler_techfile.go: CE tech file generation/export (~452 LOC) - iace_handler_monitoring.go: monitoring events + audit trail (~134 LOC) - iace_handler_refdata.go: ISO 12100 ref data, patterns, suggestions (~465 LOC) - iace_handler_rag.go: RAG library search + section enrichment (~142 LOC) training_handlers.go (1864 LOC) split into 9 files: - training_handlers.go: struct + constructor (~23 LOC) - training_handlers_modules.go: module CRUD (~226 LOC) - training_handlers_matrix.go: CTM matrix endpoints (~95 LOC) - training_handlers_assignments.go: assignment lifecycle (~243 LOC) - training_handlers_quiz.go: quiz submit/grade/attempts (~185 LOC) - training_handlers_content.go: LLM content/audio/video generation (~274 LOC) - training_handlers_media.go: media, streaming, interactive video (~325 LOC) - training_handlers_blocks.go: block configs + canonical controls (~280 LOC) - training_handlers_stats.go: deadlines, escalation, audit, certificates (~290 LOC) All files remain in package handlers. Zero behavior changes. All exported function names preserved. All files under 500 LOC hard cap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/training"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Quiz Endpoints
|
||||
// ============================================================================
|
||||
|
||||
// GetQuiz returns quiz questions for a module
|
||||
// GET /sdk/v1/training/quiz/:moduleId
|
||||
func (h *TrainingHandlers) GetQuiz(c *gin.Context) {
|
||||
moduleID, err := uuid.Parse(c.Param("moduleId"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid module ID"})
|
||||
return
|
||||
}
|
||||
|
||||
questions, err := h.store.ListQuizQuestions(c.Request.Context(), moduleID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Strip correct_index for the student-facing response
|
||||
type safeQuestion struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Question string `json:"question"`
|
||||
Options []string `json:"options"`
|
||||
Difficulty string `json:"difficulty"`
|
||||
}
|
||||
|
||||
safe := make([]safeQuestion, len(questions))
|
||||
for i, q := range questions {
|
||||
safe[i] = safeQuestion{
|
||||
ID: q.ID,
|
||||
Question: q.Question,
|
||||
Options: q.Options,
|
||||
Difficulty: string(q.Difficulty),
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"questions": safe,
|
||||
"total": len(safe),
|
||||
})
|
||||
}
|
||||
|
||||
// SubmitQuiz submits quiz answers and returns the score
|
||||
// POST /sdk/v1/training/quiz/:moduleId/submit
|
||||
func (h *TrainingHandlers) SubmitQuiz(c *gin.Context) {
|
||||
moduleID, err := uuid.Parse(c.Param("moduleId"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid module ID"})
|
||||
return
|
||||
}
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
|
||||
var req training.SubmitTrainingQuizRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Get the correct answers
|
||||
questions, err := h.store.ListQuizQuestions(c.Request.Context(), moduleID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Build answer map
|
||||
questionMap := make(map[uuid.UUID]training.QuizQuestion)
|
||||
for _, q := range questions {
|
||||
questionMap[q.ID] = q
|
||||
}
|
||||
|
||||
// Score the answers
|
||||
correctCount := 0
|
||||
totalCount := len(req.Answers)
|
||||
scoredAnswers := make([]training.QuizAnswer, len(req.Answers))
|
||||
|
||||
for i, answer := range req.Answers {
|
||||
q, exists := questionMap[answer.QuestionID]
|
||||
correct := exists && answer.SelectedIndex == q.CorrectIndex
|
||||
|
||||
scoredAnswers[i] = training.QuizAnswer{
|
||||
QuestionID: answer.QuestionID,
|
||||
SelectedIndex: answer.SelectedIndex,
|
||||
Correct: correct,
|
||||
}
|
||||
|
||||
if correct {
|
||||
correctCount++
|
||||
}
|
||||
}
|
||||
|
||||
score := float64(0)
|
||||
if totalCount > 0 {
|
||||
score = float64(correctCount) / float64(totalCount) * 100
|
||||
}
|
||||
|
||||
// Get module for pass threshold
|
||||
module, _ := h.store.GetModule(c.Request.Context(), moduleID)
|
||||
threshold := 70
|
||||
if module != nil {
|
||||
threshold = module.PassThreshold
|
||||
}
|
||||
passed := score >= float64(threshold)
|
||||
|
||||
// Record the attempt
|
||||
userID := rbac.GetUserID(c)
|
||||
attempt := &training.QuizAttempt{
|
||||
AssignmentID: req.AssignmentID,
|
||||
UserID: userID,
|
||||
Answers: scoredAnswers,
|
||||
Score: score,
|
||||
Passed: passed,
|
||||
CorrectCount: correctCount,
|
||||
TotalCount: totalCount,
|
||||
DurationSeconds: req.DurationSeconds,
|
||||
}
|
||||
|
||||
if err := h.store.CreateQuizAttempt(c.Request.Context(), attempt); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update assignment quiz result
|
||||
// Count total attempts
|
||||
attempts, _ := h.store.ListQuizAttempts(c.Request.Context(), req.AssignmentID)
|
||||
h.store.UpdateAssignmentQuizResult(c.Request.Context(), req.AssignmentID, score, passed, len(attempts))
|
||||
|
||||
// Audit log
|
||||
h.store.LogAction(c.Request.Context(), &training.AuditLogEntry{
|
||||
TenantID: tenantID,
|
||||
UserID: &userID,
|
||||
Action: training.AuditActionQuizSubmitted,
|
||||
EntityType: training.AuditEntityQuiz,
|
||||
EntityID: &attempt.ID,
|
||||
Details: map[string]interface{}{
|
||||
"module_id": moduleID.String(),
|
||||
"score": score,
|
||||
"passed": passed,
|
||||
"correct_count": correctCount,
|
||||
"total_count": totalCount,
|
||||
},
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, training.SubmitTrainingQuizResponse{
|
||||
AttemptID: attempt.ID,
|
||||
Score: score,
|
||||
Passed: passed,
|
||||
CorrectCount: correctCount,
|
||||
TotalCount: totalCount,
|
||||
Threshold: threshold,
|
||||
})
|
||||
}
|
||||
|
||||
// GetQuizAttempts returns quiz attempts for an assignment
|
||||
// GET /sdk/v1/training/quiz/attempts/:assignmentId
|
||||
func (h *TrainingHandlers) GetQuizAttempts(c *gin.Context) {
|
||||
assignmentID, err := uuid.Parse(c.Param("assignmentId"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"})
|
||||
return
|
||||
}
|
||||
|
||||
attempts, err := h.store.ListQuizAttempts(c.Request.Context(), assignmentID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"attempts": attempts,
|
||||
"total": len(attempts),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user