661 lines
20 KiB
Go
661 lines
20 KiB
Go
package assignments
|
|
|
|
import (
|
|
"boostai-backend/internal/aireview"
|
|
"boostai-backend/internal/assignmentgen"
|
|
"boostai-backend/internal/handlers/api/shared"
|
|
"boostai-backend/internal/http/params"
|
|
"boostai-backend/internal/http/respond"
|
|
authmw "boostai-backend/internal/middleware"
|
|
"boostai-backend/internal/sqlc"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
type Handler struct {
|
|
queries *sqlc.Queries
|
|
aiReview *aireview.Service
|
|
assignmentGenerator *assignmentgen.Service
|
|
}
|
|
|
|
const fixedPassThreshold = 6.0
|
|
|
|
func NewHandler(queries *sqlc.Queries, aiReview *aireview.Service, assignmentGenerator *assignmentgen.Service) *Handler {
|
|
return &Handler{queries: queries, aiReview: aiReview, assignmentGenerator: assignmentGenerator}
|
|
}
|
|
|
|
func (h *Handler) ListAssignmentsByTeacher(c *fiber.Ctx) error {
|
|
teacherID, err := params.Int64PathParam(c, "teacherId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := shared.WithTimeout()
|
|
defer cancel()
|
|
|
|
assignments, err := h.queries.ListAssignmentsByTeacher(ctx, teacherID)
|
|
if err != nil {
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
items := make([]AssignmentResponse, 0, len(assignments))
|
|
for _, assignment := range assignments {
|
|
items = append(items, mapAssignment(assignment))
|
|
}
|
|
|
|
return c.JSON(shared.ListResponse[AssignmentResponse]{Data: items})
|
|
}
|
|
|
|
func (h *Handler) ListAssignmentsForStudent(c *fiber.Ctx) error {
|
|
studentID, err := params.Int64PathParam(c, "studentId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := shared.WithTimeout()
|
|
defer cancel()
|
|
|
|
assignments, err := h.queries.ListAssignmentsForStudent(ctx, studentID)
|
|
if err != nil {
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
items := make([]AssignmentResponse, 0, len(assignments))
|
|
for _, assignment := range assignments {
|
|
items = append(items, mapAssignment(assignment))
|
|
}
|
|
|
|
return c.JSON(shared.ListResponse[AssignmentResponse]{Data: items})
|
|
}
|
|
|
|
func (h *Handler) GetAssignmentByID(c *fiber.Ctx) error {
|
|
assignmentID, err := params.Int64PathParam(c, "assignmentId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := shared.WithTimeout()
|
|
defer cancel()
|
|
|
|
assignment, err := h.queries.GetAssignmentByID(ctx, assignmentID)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return respond.Error(c, fiber.StatusNotFound, "not_found", "Assignment not found")
|
|
}
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
return c.JSON(mapAssignment(assignment))
|
|
}
|
|
|
|
func (h *Handler) ListQuestionsForAssignment(c *fiber.Ctx) error {
|
|
assignmentID, err := params.Int64PathParam(c, "assignmentId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := shared.WithTimeout()
|
|
defer cancel()
|
|
|
|
questions, err := h.queries.ListQuestionsForAssignment(ctx, assignmentID)
|
|
if err != nil {
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
items := make([]AssignmentQuestionResponse, 0, len(questions))
|
|
for _, question := range questions {
|
|
items = append(items, mapAssignmentQuestion(question))
|
|
}
|
|
|
|
return c.JSON(shared.ListResponse[AssignmentQuestionResponse]{Data: items})
|
|
}
|
|
|
|
func (h *Handler) ListQuestionDetailsForAssignmentStudent(c *fiber.Ctx) error {
|
|
assignmentID, err := params.Int64PathParam(c, "assignmentId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
studentID, err := params.Int64PathParam(c, "studentId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := shared.WithTimeout()
|
|
defer cancel()
|
|
|
|
rows, err := h.queries.ListQuestionDetailsForAssignmentStudent(ctx, sqlc.ListQuestionDetailsForAssignmentStudentParams{
|
|
AssignmentID: assignmentID,
|
|
StudentID: studentID,
|
|
})
|
|
if err != nil {
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
items := make([]AssignmentStudentQuestionDetailResponse, 0, len(rows))
|
|
for _, row := range rows {
|
|
items = append(items, mapAssignmentStudentQuestionDetail(row, studentID))
|
|
}
|
|
|
|
return c.JSON(shared.ListResponse[AssignmentStudentQuestionDetailResponse]{Data: items})
|
|
}
|
|
|
|
func (h *Handler) GetAssignmentReviewSummary(c *fiber.Ctx) error {
|
|
assignmentID, err := params.Int64PathParam(c, "assignmentId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := shared.WithTimeout()
|
|
defer cancel()
|
|
|
|
summary, err := h.queries.GetAssignmentReviewSummary(ctx, assignmentID)
|
|
if err != nil {
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
return c.JSON(mapAssignmentReviewSummary(summary))
|
|
}
|
|
|
|
func (h *Handler) GetAssignmentRedoPlan(c *fiber.Ctx) error {
|
|
assignmentID, err := params.Int64PathParam(c, "assignmentId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
studentID, err := params.Int64PathParam(c, "studentId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := shared.WithTimeout()
|
|
row, err := h.queries.GetAssignmentRedoPlan(ctx, sqlc.GetAssignmentRedoPlanParams{
|
|
AssignmentID: assignmentID,
|
|
StudentID: studentID,
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return respond.Error(c, fiber.StatusNotFound, "not_found", "Assignment student record not found")
|
|
}
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
summary, err := h.buildStudentWeaknessSummary(studentID)
|
|
if err != nil {
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
response := AssignmentRedoPlanResponse{
|
|
AssignmentID: assignmentID,
|
|
StudentID: studentID,
|
|
RedoPlanGeneratedAt: shared.TimePointer(row.RedoPlanGeneratedAt),
|
|
WeaknessSummary: mapWeaknessSummary(studentID, summary),
|
|
}
|
|
|
|
if row.RedoPlan.Valid {
|
|
stored, err := parseStoredRedoPlan(row.RedoPlan.String)
|
|
if err != nil {
|
|
response.Error = fmt.Sprintf("stored redo plan could not be parsed: %v", err)
|
|
} else {
|
|
response.TeacherFeedback = emptyStringPointer(stored.TeacherFeedback)
|
|
response.Error = stored.Error
|
|
response.Plan = stored.Plan
|
|
if len(stored.WeaknessSummary.TopicScores) > 0 || len(stored.WeaknessSummary.WeakTags) > 0 || len(stored.WeaknessSummary.RecentIssues) > 0 {
|
|
response.WeaknessSummary = mapWeaknessSummary(studentID, stored.WeaknessSummary)
|
|
}
|
|
}
|
|
}
|
|
|
|
return c.JSON(response)
|
|
}
|
|
|
|
func (h *Handler) UpdateAssignmentDraft(c *fiber.Ctx) error {
|
|
assignmentID, err := params.Int64PathParam(c, "assignmentId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var req updateAssignmentDraftRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Unable to parse request body")
|
|
}
|
|
|
|
teacherID := authmw.CurrentUserID(c)
|
|
title := strings.TrimSpace(req.Title)
|
|
if req.ClassroomID == 0 || teacherID == 0 || title == "" {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "classroom_id, teacher authentication, and title are required")
|
|
}
|
|
|
|
passThreshold, err := shared.NullableFloat64AsNumeric(pointerToFloat64(fixedPassThreshold))
|
|
if err != nil {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "pass_threshold must be a valid number")
|
|
}
|
|
|
|
ctx, cancel := shared.WithTimeout()
|
|
defer cancel()
|
|
|
|
assignment, err := h.queries.GetAssignmentByID(ctx, assignmentID)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return respond.Error(c, fiber.StatusNotFound, "not_found", "Assignment not found")
|
|
}
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
if assignment.TeacherID != teacherID {
|
|
return respond.Error(c, fiber.StatusForbidden, "forbidden", "You can only edit your own draft assignments")
|
|
}
|
|
|
|
if assignment.Status != sqlc.AssignmentStatusDraft {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Only draft assignments can be edited here")
|
|
}
|
|
|
|
classrooms, err := h.queries.ListClassroomsByTeacher(ctx, teacherID)
|
|
if err != nil {
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
classroomAllowed := false
|
|
for _, classroom := range classrooms {
|
|
if classroom.ID == req.ClassroomID {
|
|
classroomAllowed = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !classroomAllowed {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Choose one of your classrooms for this draft")
|
|
}
|
|
|
|
updatedAssignment, err := h.queries.UpdateAssignmentDraft(ctx, sqlc.UpdateAssignmentDraftParams{
|
|
ID: assignmentID,
|
|
ClassroomID: req.ClassroomID,
|
|
Title: title,
|
|
Instructions: shared.NullableText(req.Instructions),
|
|
PassThreshold: passThreshold,
|
|
DueAt: shared.NullableTime(req.DueAt),
|
|
})
|
|
if err != nil {
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
return c.JSON(mapAssignment(updatedAssignment))
|
|
}
|
|
|
|
func (h *Handler) UpdateAssignmentTeacherFeedback(c *fiber.Ctx) error {
|
|
assignmentID, err := params.Int64PathParam(c, "assignmentId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
studentID, err := params.Int64PathParam(c, "studentId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var req updateAssignmentTeacherFeedbackRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Unable to parse request body")
|
|
}
|
|
|
|
passStatusOverrideValue := ""
|
|
if req.PassStatusOverride != nil {
|
|
passStatusOverrideValue = strings.TrimSpace(*req.PassStatusOverride)
|
|
if passStatusOverrideValue != "" && !isValidAssignmentPassStatus(passStatusOverrideValue) {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "pass_status_override must be pending, pass, no_pass, or empty")
|
|
}
|
|
}
|
|
|
|
nextStepOutcomeValue := ""
|
|
if req.NextStepOutcome != nil {
|
|
nextStepOutcomeValue = strings.TrimSpace(*req.NextStepOutcome)
|
|
if nextStepOutcomeValue != "" && !isValidAssignmentNextStepOutcome(nextStepOutcomeValue) {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "next_step_outcome must be redo, accept, support, or empty")
|
|
}
|
|
}
|
|
|
|
ctx, cancel := shared.WithTimeout()
|
|
defer cancel()
|
|
|
|
row, err := h.queries.UpdateAssignmentTeacherFeedback(ctx, sqlc.UpdateAssignmentTeacherFeedbackParams{
|
|
AssignmentID: assignmentID,
|
|
StudentID: studentID,
|
|
TeacherFeedback: shared.NullableText(req.TeacherFeedback),
|
|
Column4: passStatusOverrideValue,
|
|
Column5: nextStepOutcomeValue,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return respond.Error(c, fiber.StatusNotFound, "not_found", "Assignment student record not found")
|
|
}
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
if nextStepOutcomeValue == string(sqlc.AssignmentNextStepOutcomeRedo) {
|
|
if err := h.generateAndStoreRedoPlan(assignmentID, studentID, strings.TrimSpace(shared.TextValue(row.TeacherFeedback))); err != nil {
|
|
fmt.Printf("redo plan generation failed for assignment %d student %d: %v\n", assignmentID, studentID, err)
|
|
}
|
|
} else {
|
|
clearCtx, clearCancel := shared.WithTimeout()
|
|
_, clearErr := h.queries.UpdateAssignmentRedoPlan(clearCtx, sqlc.UpdateAssignmentRedoPlanParams{
|
|
AssignmentID: assignmentID,
|
|
StudentID: studentID,
|
|
Column3: "",
|
|
})
|
|
clearCancel()
|
|
if clearErr != nil && !errors.Is(clearErr, pgx.ErrNoRows) {
|
|
fmt.Printf("redo plan clear failed for assignment %d student %d: %v\n", assignmentID, studentID, clearErr)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"assignment_id": assignmentID,
|
|
"student_id": studentID,
|
|
"ai_feedback": shared.TextPointer(row.AiFeedback),
|
|
"teacher_feedback": shared.TextPointer(row.TeacherFeedback),
|
|
"overall_score": shared.NumericPointer(row.OverallScore),
|
|
"pass_threshold": shared.NumericPointer(row.PassThreshold),
|
|
"next_step_outcome": nextStepOutcome,
|
|
"pass_status_override": passStatusOverride,
|
|
"pass_status": string(row.PassStatus),
|
|
})
|
|
}
|
|
|
|
func (h *Handler) CloseAssignment(c *fiber.Ctx) error {
|
|
assignmentID, err := params.Int64PathParam(c, "assignmentId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
teacherID := authmw.CurrentUserID(c)
|
|
if teacherID == 0 {
|
|
return respond.Error(c, fiber.StatusUnauthorized, "unauthorized", "Teacher authentication is required")
|
|
}
|
|
|
|
ctx, cancel := shared.WithTimeout()
|
|
defer cancel()
|
|
|
|
assignment, err := h.queries.GetAssignmentByID(ctx, assignmentID)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return respond.Error(c, fiber.StatusNotFound, "not_found", "Assignment not found")
|
|
}
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
if assignment.TeacherID != teacherID {
|
|
return respond.Error(c, fiber.StatusForbidden, "forbidden", "You can only close your own assignments")
|
|
}
|
|
|
|
if assignment.Status == sqlc.AssignmentStatusDraft {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Draft assignments cannot be closed")
|
|
}
|
|
|
|
if assignment.Status == sqlc.AssignmentStatusClosed {
|
|
return c.JSON(mapAssignment(assignment))
|
|
}
|
|
|
|
queue, err := h.queries.ListAssignmentReviewQueue(ctx, sqlc.ListAssignmentReviewQueueParams{
|
|
AssignmentID: assignmentID,
|
|
Column2: "",
|
|
})
|
|
if err != nil {
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
readiness := buildAssignmentCloseReadiness(queue)
|
|
if !readiness.CanClose {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
"error": "assignment_not_ready_to_close",
|
|
"message": "This assignment still has open review blockers.",
|
|
"blockers": readiness.Blockers,
|
|
})
|
|
}
|
|
|
|
closedAssignment, err := h.queries.CloseAssignment(ctx, assignmentID)
|
|
if err != nil {
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
return c.JSON(mapAssignment(closedAssignment))
|
|
}
|
|
|
|
func (h *Handler) ListAssignmentReviewQueue(c *fiber.Ctx) error {
|
|
assignmentID, err := params.Int64PathParam(c, "assignmentId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
statusFilter := strings.TrimSpace(c.Query("status"))
|
|
if statusFilter != "" && !shared.IsValidAnswerStatus(statusFilter) {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Invalid review status filter")
|
|
}
|
|
|
|
ctx, cancel := shared.WithTimeout()
|
|
defer cancel()
|
|
|
|
rows, err := h.queries.ListAssignmentReviewQueue(ctx, sqlc.ListAssignmentReviewQueueParams{
|
|
AssignmentID: assignmentID,
|
|
Column2: statusFilter,
|
|
})
|
|
if err != nil {
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
items := make([]AssignmentReviewQueueItemResponse, 0, len(rows))
|
|
for _, row := range rows {
|
|
items = append(items, mapAssignmentReviewQueueItem(row))
|
|
}
|
|
|
|
return c.JSON(shared.ListResponse[AssignmentReviewQueueItemResponse]{Data: items})
|
|
}
|
|
|
|
func (h *Handler) CreateAssignment(c *fiber.Ctx) error {
|
|
var req createAssignmentRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Unable to parse request body")
|
|
}
|
|
|
|
teacherID := authmw.CurrentUserID(c)
|
|
if req.ClassroomID == 0 || teacherID == 0 || strings.TrimSpace(req.Title) == "" || strings.TrimSpace(req.Status) == "" {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "classroom_id, teacher authentication, title, and status are required")
|
|
}
|
|
|
|
passThreshold, err := shared.NullableFloat64AsNumeric(pointerToFloat64(fixedPassThreshold))
|
|
if err != nil {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "pass_threshold must be a valid number")
|
|
}
|
|
|
|
ctx, cancel := shared.WithTimeout()
|
|
defer cancel()
|
|
|
|
assignment, err := h.queries.CreateAssignment(ctx, sqlc.CreateAssignmentParams{
|
|
ClassroomID: req.ClassroomID,
|
|
TeacherID: teacherID,
|
|
Title: strings.TrimSpace(req.Title),
|
|
Instructions: shared.NullableText(req.Instructions),
|
|
PassThreshold: passThreshold,
|
|
Status: sqlc.AssignmentStatus(strings.TrimSpace(req.Status)),
|
|
DueAt: shared.NullableTime(req.DueAt),
|
|
PublishedAt: shared.NullableTime(req.PublishedAt),
|
|
})
|
|
if err != nil {
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
return c.Status(fiber.StatusCreated).JSON(mapAssignment(assignment))
|
|
}
|
|
|
|
func (h *Handler) AssignStudentToAssignment(c *fiber.Ctx) error {
|
|
assignmentID, err := params.Int64PathParam(c, "assignmentId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var req assignStudentToAssignmentRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Unable to parse request body")
|
|
}
|
|
|
|
if req.StudentID == 0 {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "student_id is required")
|
|
}
|
|
|
|
teacherID := authmw.CurrentUserID(c)
|
|
if teacherID == 0 {
|
|
return respond.Error(c, fiber.StatusUnauthorized, "unauthorized", "Teacher authentication is required")
|
|
}
|
|
|
|
ctx, cancel := shared.WithTimeout()
|
|
defer cancel()
|
|
|
|
assignment, err := h.queries.GetAssignmentByID(ctx, assignmentID)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return respond.Error(c, fiber.StatusNotFound, "not_found", "Assignment not found")
|
|
}
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
if assignment.TeacherID != teacherID {
|
|
return respond.Error(c, fiber.StatusForbidden, "forbidden", "You can only assign students to your own assignments")
|
|
}
|
|
|
|
err = h.queries.AssignStudentToAssignment(ctx, sqlc.AssignStudentToAssignmentParams{
|
|
AssignmentID: assignmentID,
|
|
StudentID: req.StudentID,
|
|
})
|
|
if err != nil {
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
response := fiber.Map{
|
|
"status": "ok",
|
|
"assignment_id": assignmentID,
|
|
"student_id": req.StudentID,
|
|
}
|
|
|
|
if req.MixedGeneration != nil {
|
|
if h.assignmentGenerator == nil {
|
|
cleanupErr := h.queries.DeleteAssignmentAssignee(ctx, sqlc.DeleteAssignmentAssigneeParams{
|
|
AssignmentID: assignmentID,
|
|
StudentID: req.StudentID,
|
|
})
|
|
if cleanupErr != nil {
|
|
return respond.DatabaseError(c, cleanupErr)
|
|
}
|
|
return respond.Error(c, fiber.StatusServiceUnavailable, "generator_unavailable", "Assignment generator is not configured")
|
|
}
|
|
|
|
generated, generationErr := h.generateMixedStudentQuestionsForAssignmentStudent(ctx, assignmentID, req.StudentID, teacherID, req.MixedGeneration)
|
|
if generationErr != nil {
|
|
cleanupErr := h.queries.DeleteAssignmentAssignee(ctx, sqlc.DeleteAssignmentAssigneeParams{
|
|
AssignmentID: assignmentID,
|
|
StudentID: req.StudentID,
|
|
})
|
|
if cleanupErr != nil {
|
|
return respond.DatabaseError(c, cleanupErr)
|
|
}
|
|
if apiErr, ok := generationErr.(*assignmentAPIError); ok {
|
|
return respond.Error(c, apiErr.status, apiErr.code, apiErr.message)
|
|
}
|
|
return respond.DatabaseError(c, generationErr)
|
|
}
|
|
|
|
response["mixed_generation"] = generated
|
|
}
|
|
|
|
return c.Status(fiber.StatusCreated).JSON(response)
|
|
}
|
|
|
|
func (h *Handler) AddQuestionToAssignment(c *fiber.Ctx) error {
|
|
assignmentID, err := params.Int64PathParam(c, "assignmentId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var req addQuestionToAssignmentRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Unable to parse request body")
|
|
}
|
|
|
|
if req.QuestionID == 0 || req.Position <= 0 {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "question_id and positive position are required")
|
|
}
|
|
|
|
ctx, cancel := shared.WithTimeout()
|
|
defer cancel()
|
|
|
|
err = h.queries.AddQuestionToAssignment(ctx, sqlc.AddQuestionToAssignmentParams{
|
|
AssignmentID: assignmentID,
|
|
QuestionID: req.QuestionID,
|
|
Position: req.Position,
|
|
})
|
|
if err != nil {
|
|
return respond.DatabaseError(c, err)
|
|
}
|
|
|
|
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
|
"status": "ok",
|
|
"assignment_id": assignmentID,
|
|
"question_id": req.QuestionID,
|
|
"position": req.Position,
|
|
})
|
|
}
|
|
|
|
func (h *Handler) GenerateMixedStudentQuestions(c *fiber.Ctx) error {
|
|
assignmentID, err := params.Int64PathParam(c, "assignmentId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
studentID, err := params.Int64PathParam(c, "studentId")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var req generateMixedStudentQuestionsRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Unable to parse request body")
|
|
}
|
|
|
|
teacherID := authmw.CurrentUserID(c)
|
|
if teacherID == 0 {
|
|
return respond.Error(c, fiber.StatusUnauthorized, "unauthorized", "Teacher authentication is required")
|
|
}
|
|
|
|
if h.assignmentGenerator == nil {
|
|
return respond.Error(c, fiber.StatusServiceUnavailable, "generator_unavailable", "Assignment generator is not configured")
|
|
}
|
|
|
|
ctx, cancel := shared.WithTimeout()
|
|
defer cancel()
|
|
|
|
response, generationErr := h.generateMixedStudentQuestionsForAssignmentStudent(ctx, assignmentID, studentID, teacherID, &req)
|
|
if generationErr != nil {
|
|
if apiErr, ok := generationErr.(*assignmentAPIError); ok {
|
|
return respond.Error(c, apiErr.status, apiErr.code, apiErr.message)
|
|
}
|
|
return respond.DatabaseError(c, generationErr)
|
|
}
|
|
|
|
return c.Status(fiber.StatusCreated).JSON(response)
|
|
}
|