Files
BoostAI/Backend/internal/handlers/api/assignments/handler_helpers.go
2026-05-25 17:05:06 +01:00

376 lines
12 KiB
Go

package assignments
import (
"boostai-backend/internal/assignmentgen"
"boostai-backend/internal/handlers/api/shared"
"boostai-backend/internal/sqlc"
"encoding/json"
"fmt"
"math"
"strings"
"github.com/jackc/pgx/v5/pgtype"
)
func mapAssignment(assignment sqlc.Assignment) AssignmentResponse {
return AssignmentResponse{
ID: assignment.ID,
ClassroomID: assignment.ClassroomID,
TeacherID: assignment.TeacherID,
Title: assignment.Title,
Instructions: shared.TextPointer(assignment.Instructions),
PassThreshold: shared.NumericPointer(assignment.PassThreshold),
Status: string(assignment.Status),
DueAt: shared.TimePointer(assignment.DueAt),
PublishedAt: shared.TimePointer(assignment.PublishedAt),
CreatedAt: shared.TimePointer(assignment.CreatedAt),
UpdatedAt: shared.TimePointer(assignment.UpdatedAt),
}
}
func parseQuestionTopicValue(value string) (sqlc.QuestionTopic, error) {
switch strings.TrimSpace(strings.ToLower(value)) {
case string(sqlc.QuestionTopicPlaceValue):
return sqlc.QuestionTopicPlaceValue, nil
case string(sqlc.QuestionTopicArithmetic):
return sqlc.QuestionTopicArithmetic, nil
case string(sqlc.QuestionTopicNegativeNumbers):
return sqlc.QuestionTopicNegativeNumbers, nil
case string(sqlc.QuestionTopicBidmas):
return sqlc.QuestionTopicBidmas, nil
case string(sqlc.QuestionTopicFractions):
return sqlc.QuestionTopicFractions, nil
case string(sqlc.QuestionTopicAlgebra):
return sqlc.QuestionTopicAlgebra, nil
case string(sqlc.QuestionTopicGeometry):
return sqlc.QuestionTopicGeometry, nil
case string(sqlc.QuestionTopicData):
return sqlc.QuestionTopicData, nil
default:
return "", fmt.Errorf("primary_topic must be one of place_value, arithmetic, negative_numbers, bidmas, fractions, algebra, geometry, or data")
}
}
func parseQuestionDifficultyValue(value string) (sqlc.QuestionDifficulty, error) {
switch strings.TrimSpace(strings.ToLower(value)) {
case string(sqlc.QuestionDifficultyEasy):
return sqlc.QuestionDifficultyEasy, nil
case string(sqlc.QuestionDifficultyMedium):
return sqlc.QuestionDifficultyMedium, nil
case string(sqlc.QuestionDifficultyHard):
return sqlc.QuestionDifficultyHard, nil
default:
return "", fmt.Errorf("difficulty must be one of easy, medium, or hard")
}
}
func parseQuestionStatusValue(value string) (sqlc.QuestionStatus, error) {
switch strings.TrimSpace(strings.ToLower(value)) {
case string(sqlc.QuestionStatusDraft):
return sqlc.QuestionStatusDraft, nil
case string(sqlc.QuestionStatusPublished):
return sqlc.QuestionStatusPublished, nil
case string(sqlc.QuestionStatusArchived):
return sqlc.QuestionStatusArchived, nil
default:
return "", fmt.Errorf("question_status must be one of draft, published, or archived")
}
}
func mapAssignmentGenerationWeaknessSummary(summary assignmentgen.WeaknessSummary) mixedPlanWeaknessSummaryResponse {
topicScores := make(map[string]float64, len(summary.TopicScores))
for topic, score := range summary.TopicScores {
topicScores[string(topic)] = score
}
return mixedPlanWeaknessSummaryResponse{
TopicScores: topicScores,
WeakTags: append([]string(nil), summary.WeakTags...),
RecentIssues: append([]string(nil), summary.RecentIssues...),
}
}
func questionTopicPointer(topic sqlc.NullQuestionTopic) *string {
if !topic.Valid {
return nil
}
value := string(topic.QuestionTopic)
return &value
}
func questionDifficultyPointer(difficulty sqlc.NullQuestionDifficulty) *string {
if !difficulty.Valid {
return nil
}
value := string(difficulty.QuestionDifficulty)
return &value
}
func personalizedRatioValue(value *float64) float64 {
if value == nil || *value == 0 {
return 0.30
}
return *value
}
func int64Value(value *int64) int64 {
if value == nil {
return 0
}
return *value
}
func int64Pointer(value int64) *int64 {
return &value
}
func stringPointer(value string) *string {
return &value
}
func trimmedPointerValue(value *string) string {
if value == nil {
return ""
}
return strings.TrimSpace(*value)
}
func mapAssignmentQuestion(question sqlc.ListQuestionsForAssignmentRow) AssignmentQuestionResponse {
return AssignmentQuestionResponse{
AssignmentID: question.AssignmentID,
QuestionID: question.QuestionID,
Position: question.Position,
AuthorTeacherID: question.AuthorTeacherID,
Title: question.Title,
Prompt: question.Prompt,
Subject: shared.TextPointer(question.Subject),
Source: shared.TextPointer(question.Source),
QuestionStatus: string(question.Status),
QuestionCreatedAt: shared.TimePointer(question.CreatedAt),
QuestionUpdatedAt: shared.TimePointer(question.UpdatedAt),
}
}
func mapAssignmentStudentQuestionDetail(row sqlc.ListQuestionDetailsForAssignmentStudentRow, studentID int64) AssignmentStudentQuestionDetailResponse {
var answerStatus *string
if row.AnswerStatus.Valid {
status := string(row.AnswerStatus.AnswerStatus)
answerStatus = &status
}
var passStatus *string
if row.PassStatus.Valid {
status := string(row.PassStatus.AssignmentPassStatus)
passStatus = &status
}
var passStatusOverride *string
if row.PassStatusOverride.Valid {
status := string(row.PassStatusOverride.AssignmentPassStatus)
passStatusOverride = &status
}
var nextStepOutcome *string
if row.NextStepOutcome.Valid {
outcome := string(row.NextStepOutcome.AssignmentNextStepOutcome)
nextStepOutcome = &outcome
}
var reviewNeedsAttention *bool
if row.AnswerID.Valid {
reviewNeedsAttention = shared.BoolPointer(row.ReviewNeedsAttention)
}
return AssignmentStudentQuestionDetailResponse{
AssignmentID: row.AssignmentID,
StudentID: studentID,
QuestionID: row.QuestionID,
Position: row.Position,
Title: row.Title,
Prompt: row.Prompt,
Subject: shared.TextPointer(row.Subject),
Source: shared.TextPointer(row.Source),
QuestionTags: row.QuestionTags,
QuestionStatus: string(row.QuestionStatus),
CorrectAnswer: shared.TextPointer(row.CorrectAnswer),
AssignmentAiFeedback: shared.TextPointer(row.AssignmentAiFeedback),
AssignmentTeacherFeedback: shared.TextPointer(row.AssignmentTeacherFeedback),
OverallScore: shared.NumericPointer(row.OverallScore),
PassThreshold: shared.NumericPointer(row.PassThreshold),
NextStepOutcome: nextStepOutcome,
PassStatusOverride: passStatusOverride,
PassStatus: passStatus,
AnswerID: shared.Int64Pointer(row.AnswerID),
AnswerText: shared.TextPointer(row.AnswerText),
SolveMode: shared.TextPointer(row.SolveMode),
WorkingSteps: shared.TextPointer(row.WorkingSteps),
IsCorrect: shared.BoolPointer(row.IsCorrect),
AiFeedback: shared.TextPointer(row.AiFeedback),
TeacherFeedback: shared.TextPointer(row.TeacherFeedback),
AnswerStatus: answerStatus,
ReviewNeedsAttention: reviewNeedsAttention,
ReviewIssueReason: shared.TextPointer(row.ReviewIssueReason),
ReviewCorrectnessScore: shared.NumericPointer(row.ReviewCorrectnessScore),
ReviewUnderstandingScore: shared.NumericPointer(row.ReviewUnderstandingScore),
ReviewQuestionScore: shared.NumericPointer(row.ReviewQuestionScore),
ReviewConfidence: shared.NumericPointer(row.ReviewConfidence),
ReviewTags: row.ReviewTags,
SubmittedAt: shared.TimePointer(row.SubmittedAt),
ReviewedAt: shared.TimePointer(row.ReviewedAt),
AnswerCreatedAt: shared.TimePointer(row.AnswerCreatedAt),
AnswerUpdatedAt: shared.TimePointer(row.AnswerUpdatedAt),
}
}
func mapAssignmentReviewSummary(summary sqlc.GetAssignmentReviewSummaryRow) AssignmentReviewSummaryResponse {
return AssignmentReviewSummaryResponse{
AssignmentID: summary.AssignmentID,
TotalQuestions: summary.TotalQuestions,
TotalAssigned: summary.TotalAssigned,
NotStarted: summary.NotStarted,
InProgress: summary.InProgress,
Submitted: summary.Submitted,
Reviewed: summary.Reviewed,
}
}
func mapAssignmentReviewQueueItem(row sqlc.ListAssignmentReviewQueueRow) AssignmentReviewQueueItemResponse {
var nextStepOutcome *string
if row.NextStepOutcome.Valid {
outcome := string(row.NextStepOutcome.AssignmentNextStepOutcome)
nextStepOutcome = &outcome
}
return AssignmentReviewQueueItemResponse{
AssignmentID: row.AssignmentID,
StudentID: row.StudentID,
NextStepOutcome: nextStepOutcome,
StudentName: row.StudentName,
StudentEmail: row.StudentEmail,
TotalQuestions: row.TotalQuestions,
AnsweredQuestions: row.AnsweredQuestions,
ReviewedQuestions: row.ReviewedQuestions,
SubmittedQuestions: row.SubmittedQuestions,
InProgressQuestions: row.InProgressQuestions,
ReviewStatus: string(row.ReviewStatus),
LatestSubmittedAt: shared.TimePointer(row.LatestSubmittedAt),
LatestReviewedAt: shared.TimePointer(row.LatestReviewedAt),
}
}
func buildAssignmentCloseReadiness(queue []sqlc.ListAssignmentReviewQueueRow) assignmentCloseReadiness {
blockers := make([]string, 0)
if len(queue) == 0 {
return assignmentCloseReadiness{
CanClose: false,
Blockers: []string{"No students have been assigned yet."},
}
}
for _, item := range queue {
name := strings.TrimSpace(item.StudentName)
if name == "" {
name = fmt.Sprintf("Student %d", item.StudentID)
}
switch {
case item.SubmittedQuestions > 0 || item.ReviewStatus == sqlc.AnswerStatusSubmitted:
blockers = append(blockers, fmt.Sprintf("%s still has submitted work waiting for review.", name))
case item.InProgressQuestions > 0 || item.ReviewStatus == sqlc.AnswerStatusInProgress:
blockers = append(blockers, fmt.Sprintf("%s still has work in progress.", name))
case item.AnsweredQuestions == 0 || item.ReviewStatus == sqlc.AnswerStatusNotStarted:
blockers = append(blockers, fmt.Sprintf("%s has not started this assignment yet.", name))
case !item.NextStepOutcome.Valid:
blockers = append(blockers, fmt.Sprintf("%s still needs a next-step decision.", name))
}
}
return assignmentCloseReadiness{
CanClose: len(blockers) == 0,
Blockers: blockers,
}
}
func parseStoredRedoPlan(value string) (storedRedoPlan, error) {
var payload storedRedoPlan
if err := json.Unmarshal([]byte(value), &payload); err != nil {
return storedRedoPlan{}, err
}
return payload, nil
}
func mapWeaknessSummary(studentID int64, summary weaknessSummary) StudentWeaknessSummaryResponse {
return StudentWeaknessSummaryResponse{
StudentID: studentID,
TopicScores: summary.TopicScores,
WeakTags: summary.WeakTags,
RecentIssues: summary.RecentIssues,
}
}
func planningScore(isCorrect pgtype.Bool, understanding pgtype.Numeric) float64 {
understandingValue := 0.0
if value := shared.NumericPointer(understanding); value != nil {
understandingValue = *value
}
correctnessValue := 0.0
if isCorrect.Valid && isCorrect.Bool {
correctnessValue = 1.0
}
return (correctnessValue + understandingValue) / 2
}
func roundToOneDecimal(value float64) float64 {
return math.Round(value*10) / 10
}
func allowedQuestionTopics() []string {
return []string{
string(sqlc.QuestionTopicPlaceValue),
string(sqlc.QuestionTopicArithmetic),
string(sqlc.QuestionTopicNegativeNumbers),
string(sqlc.QuestionTopicBidmas),
string(sqlc.QuestionTopicFractions),
string(sqlc.QuestionTopicAlgebra),
string(sqlc.QuestionTopicGeometry),
string(sqlc.QuestionTopicData),
}
}
func emptyStringPointer(value string) *string {
value = strings.TrimSpace(value)
if value == "" {
return nil
}
return &value
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
func isValidAssignmentPassStatus(value string) bool {
switch value {
case string(sqlc.AssignmentPassStatusPending), string(sqlc.AssignmentPassStatusPass), string(sqlc.AssignmentPassStatusNoPass):
return true
default:
return false
}
}
func isValidAssignmentNextStepOutcome(value string) bool {
switch value {
case "redo", "accept", "support":
return true
default:
return false
}
}
func pointerToFloat64(value float64) *float64 {
return &value
}