376 lines
12 KiB
Go
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
|
|
}
|