From 0b7b83525ac600e5f38badae897238f357c70d87 Mon Sep 17 00:00:00 2001 From: jvcByte Date: Thu, 11 Jun 2026 13:14:24 +0100 Subject: [PATCH 1/3] fix: recalculate scores no longer closes active sessions --- .../instructor/exercises/[id]/recalculate-scores/route.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/api/instructor/exercises/[id]/recalculate-scores/route.ts b/app/api/instructor/exercises/[id]/recalculate-scores/route.ts index 745b4b6..a39cf4b 100644 --- a/app/api/instructor/exercises/[id]/recalculate-scores/route.ts +++ b/app/api/instructor/exercises/[id]/recalculate-scores/route.ts @@ -54,11 +54,8 @@ export async function POST( ) `; - // 2. Close open sessions - await sql` - UPDATE sessions SET closed_at = now() - WHERE exercise_id = ${exerciseId} AND closed_at IS NULL AND started_at IS NOT NULL - `; + // 2. Close open sessions — intentionally removed. + // Recalculate should only rescore; it must not force-close active sessions. // 3. Re-evaluate paste flags with threshold if (ex.max_paste_chars !== null) { From 28cdc429d6bc1126cb162b17425dbaf469fd12cd Mon Sep 17 00:00:00 2001 From: jvcByte Date: Thu, 11 Jun 2026 13:44:48 +0100 Subject: [PATCH 2/3] fix: exclude skipped submissions from scoring --- .../instructor/exercises/[id]/recalculate-scores/route.ts | 5 +++-- lib/scoring.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/api/instructor/exercises/[id]/recalculate-scores/route.ts b/app/api/instructor/exercises/[id]/recalculate-scores/route.ts index a39cf4b..78aa290 100644 --- a/app/api/instructor/exercises/[id]/recalculate-scores/route.ts +++ b/app/api/instructor/exercises/[id]/recalculate-scores/route.ts @@ -42,12 +42,13 @@ export async function POST( const minResponseLength = ex.min_response_length ?? DEFAULT_MIN_LENGTH; const hasConstraints = ex.pass_mark !== null || ex.min_questions_required !== null || ex.flag_fails || ex.max_paste_chars !== null || ex.max_focus_loss !== null; - // 1. Mark submissions as final only if actually attempted + // 1. Mark submissions as final only if actually attempted (not skipped) // (non-empty response OR has edit events — handles both written and code questions) await sql` UPDATE submissions SET is_final = true WHERE session_id IN (SELECT id FROM sessions WHERE exercise_id = ${exerciseId}) AND is_final = false + AND status != 'skipped' AND ( LENGTH(TRIM(COALESCE(response_text, ''))) > 0 OR EXISTS (SELECT 1 FROM edit_events ee WHERE ee.submission_id = submissions.id) @@ -171,7 +172,7 @@ export async function POST( const sessionData = await sql` SELECT s.id AS session_id, - SUM(CASE WHEN sub.is_final THEN 1 ELSE 0 END)::int AS final_count, + SUM(CASE WHEN sub.is_final AND sub.status != 'skipped' THEN 1 ELSE 0 END)::int AS final_count, COALESCE(SUM( GREATEST(0, COALESCE(array_length(sub.flag_reasons, 1), 0) diff --git a/lib/scoring.ts b/lib/scoring.ts index 72f9f41..59cb8d8 100644 --- a/lib/scoring.ts +++ b/lib/scoring.ts @@ -51,7 +51,7 @@ export async function recalculateSessionScore(sessionId: string): Promise 0 From feb97ef9099bbecb8eaa63259ff45967c00d15f7 Mon Sep 17 00:00:00 2001 From: jvcByte Date: Thu, 11 Jun 2026 13:48:05 +0100 Subject: [PATCH 3/3] fix: don't count starter-only responses as answered questions --- .../exercises/[id]/recalculate-scores/route.ts | 18 +++++++++++++++++- lib/scoring.ts | 4 ++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/api/instructor/exercises/[id]/recalculate-scores/route.ts b/app/api/instructor/exercises/[id]/recalculate-scores/route.ts index 78aa290..7a1e0de 100644 --- a/app/api/instructor/exercises/[id]/recalculate-scores/route.ts +++ b/app/api/instructor/exercises/[id]/recalculate-scores/route.ts @@ -172,7 +172,23 @@ export async function POST( const sessionData = await sql` SELECT s.id AS session_id, - SUM(CASE WHEN sub.is_final AND sub.status != 'skipped' THEN 1 ELSE 0 END)::int AS final_count, + SUM(CASE + WHEN sub.is_final AND sub.status != 'skipped' AND ( + ( + SELECT CASE + WHEN q.test_cases IS NOT NULL THEN (CASE WHEN sub.tests_passed = true THEN 1 ELSE 0 END) + ELSE (CASE WHEN + LENGTH(TRIM(REPLACE(COALESCE(sub.response_text, ''), COALESCE(q.starter, ''), ''))) > 0 + OR EXISTS (SELECT 1 FROM edit_events ee WHERE ee.submission_id = sub.id) + THEN 1 ELSE 0 END) + END + FROM questions q + JOIN sessions s2 ON s2.exercise_id = q.exercise_id + WHERE s2.id = s.id AND q.question_index = sub.question_index + LIMIT 1 + ) = 1 + ) THEN 1 ELSE 0 + END)::int AS final_count, COALESCE(SUM( GREATEST(0, COALESCE(array_length(sub.flag_reasons, 1), 0) diff --git a/lib/scoring.ts b/lib/scoring.ts index 59cb8d8..bf10b6c 100644 --- a/lib/scoring.ts +++ b/lib/scoring.ts @@ -52,9 +52,9 @@ export async function recalculateSessionScore(sessionId: string): Promise 0 + LENGTH(TRIM(REPLACE(COALESCE(sub.response_text, ''), COALESCE(q.starter, ''), ''))) > 0 OR EXISTS (SELECT 1 FROM edit_events ee WHERE ee.submission_id = sub.id) )) OR