diff --git a/.kiro/specs/batch-test-runner/.config.kiro b/.kiro/specs/batch-test-runner/.config.kiro new file mode 100644 index 0000000..81286f2 --- /dev/null +++ b/.kiro/specs/batch-test-runner/.config.kiro @@ -0,0 +1 @@ +{"specId": "2ae19710-3758-46c5-b3a1-9bade9b7f332", "workflowType": "requirements-first", "specType": "feature"} diff --git a/.kiro/specs/batch-test-runner/design.md b/.kiro/specs/batch-test-runner/design.md new file mode 100644 index 0000000..561f827 --- /dev/null +++ b/.kiro/specs/batch-test-runner/design.md @@ -0,0 +1,253 @@ +# Design Document: Batch Test Runner + +## Overview + +The Batch Test Runner adds automated grading for code questions. It has two parts: + +1. **Test Case Editor** — a UI component embedded in `QuestionManager` that lets instructors define `input`/`expected_output` pairs on any `code`-type question. These are stored in the existing `test_cases` JSONB column on the `questions` table. + +2. **Batch Run API** — a new `POST /api/instructor/exercises/[id]/batch-test-run` endpoint that fetches all final submissions for an exercise, sends each one to the Runner Service against its question's test cases, writes `tests_passed` back to the `submissions` table, then calls the existing `recalculate-scores` endpoint to update session scores. + +The feature is synchronous: the instructor clicks "Run Tests", waits for the response, and sees a summary. No SSE or polling is needed. + +--- + +## Architecture + +```mermaid +sequenceDiagram + participant I as Instructor browser + participant B as BatchTestRun API + participant DB as Database + participant R as Runner Service + participant S as RecalculateScores API + + I->>B: POST /api/instructor/exercises/[id]/batch-test-run + B->>DB: Fetch final submissions with question test_cases + loop For each submission + B->>R: POST RUNNER_URL/run (code, language, stdin=test_case.input) + R-->>B: {stdout, exit_code, ...} + B->>B: Compare stdout.trim() == expected_output.trim() + end + B->>DB: UPDATE submissions SET tests_passed = ... + B->>S: POST /api/instructor/exercises/[id]/recalculate-scores + S-->>B: {recalculated: N} + B-->>I: {processed, passed, failed, recalculated} +``` + +The Test Case Editor is purely a client-side React component layered on top of the existing `PUT /api/instructor/exercises/[id]/questions/[qid]` endpoint — no new API surface is needed for it. + +--- + +## Components and Interfaces + +### 1. `TestCaseEditor` (new React component) + +**Location:** `app/instructor/exercises/[id]/TestCaseEditor.tsx` + +**Props:** +```ts +interface Props { + exerciseId: string; + questionId: string; + initialTestCases: TestCase[]; + onSaved?: (testCases: TestCase[]) => void; +} + +interface TestCase { + input: string; + expected_output: string; +} +``` + +**Behaviour:** +- Only rendered when the parent passes a `code`-type question +- Displays a list of existing test cases (input + expected_output) +- Add row: two text inputs + "Add" button; validates that `input` is non-empty before saving +- Edit in-place: click a row to make it editable +- Delete: remove a row and save immediately +- All mutations send the full updated `test_cases` array via `PUT /api/instructor/exercises/[id]/questions/[qid]` +- Shows a toast on success/failure + +**Integration:** `QuestionManager` renders `` in the expanded question panel, below the starter-code section, only when `q.type === 'code'`. + +### 2. `BatchRunButton` (new React component) + +**Location:** `app/instructor/exercises/[id]/submissions/BatchRunButton.tsx` + +**Props:** +```ts +interface Props { + exerciseId: string; + onComplete?: () => void; // called after successful run so parent can refresh +} +``` + +**States:** idle → loading → success summary | error + +**Behaviour:** +- Renders a "Run Tests" button +- On click: disables button, shows spinner, calls `POST /api/instructor/exercises/[id]/batch-test-run` +- On success: shows `"Processed N submissions — P passed, F failed"` and calls `onComplete` +- On error: shows error message, re-enables button + +**Integration:** Added to `app/instructor/exercises/[id]/submissions/page.tsx` alongside the Export CSV button. `onComplete` triggers a router refresh (`router.refresh()`) to reload updated scores. + +### 3. `POST /api/instructor/exercises/[id]/batch-test-run` (new API route) + +**Location:** `app/api/instructor/exercises/[id]/batch-test-run/route.ts` + +**Request:** No body required. + +**Response (200):** +```ts +{ + processed: number; // total submissions evaluated + passed: number; // submissions where tests_passed = true + failed: number; // submissions where tests_passed = false + recalculated: number; // sessions rescored (from recalculate-scores) +} +``` + +**Error responses:** +- `401` — no session +- `403` — session role is not `instructor` +- `404` — exercise not found +- `503` — `RUNNER_URL` not configured + +--- + +## Data Models + +No schema changes are required. The feature uses three existing columns: + +| Table | Column | Type | Purpose | +|-------|--------|------|---------| +| `questions` | `test_cases` | `JSONB` | Array of `{input, expected_output}` objects | +| `submissions` | `tests_passed` | `boolean` | Set by batch runner; used by scoring | +| `submissions` | `is_final` | `boolean` | Gate: only final submissions are processed | + +The `test_cases` column already exists and is already consumed by `lib/scoring.ts` when computing whether a code submission counts as a passing answer. + +### TestCase shape (stored in `questions.test_cases`): +```json +[ + { "input": "Hello\nWorld", "expected_output": "dlroW\nolleH" }, + { "input": "abc", "expected_output": "cba" } +] +``` + +--- + +## Correctness Properties + +*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* + +### Property 1: Test case validation rejects empty input + +*For any* string composed entirely of whitespace characters (including the empty string), attempting to save it as a test case `input` field SHALL be rejected without issuing a PUT request, and the test case list SHALL remain unchanged. + +**Validates: Requirements 1.5** + +--- + +### Property 2: Test case list mutation preserves unmodified entries + +*For any* existing list of test cases, adding, editing, or deleting a single entry SHALL result in a saved list where all entries not targeted by the mutation are present and unchanged. + +**Validates: Requirements 1.2, 1.3, 1.4** + +--- + +### Property 3: Submission selection filter + +*For any* exercise, the batch runner SHALL process exactly the set of submissions where `is_final = true` AND the corresponding question has at least one test case — no more, no less. + +**Validates: Requirements 2.1, 4.3, 4.4** + +--- + +### Property 4: tests_passed reflects unanimous pass + +*For any* submission run against N test cases, `tests_passed` SHALL equal `true` if and only if every individual test case result has `actual.trim() === expected.trim()`; otherwise it SHALL be `false`. + +**Validates: Requirements 2.3, 2.4, 2.6** + +--- + +### Property 5: Batch run idempotence + +*For any* set of submissions, running the batch runner twice in sequence SHALL produce the same `tests_passed` values on all submissions as running it once — the second run overwrites without accumulation. + +**Validates: Requirements 2.8, 4.1** + +--- + +### Property 6: Response counts are consistent + +*For any* batch run, the returned JSON SHALL satisfy: `passed + failed === processed`, all four fields (`processed`, `passed`, `failed`, `recalculated`) are present and non-negative integers. + +**Validates: Requirements 2.9** + +--- + +### Property 7: Exercise scope isolation + +*For any* batch run targeting exercise A, the `tests_passed` column on submissions belonging to any other exercise SHALL remain unmodified. + +**Validates: Requirements 5.3** + +--- + +## Error Handling + +| Condition | Behaviour | +|-----------|-----------| +| `RUNNER_URL` not set | Return `503` immediately; no submissions processed | +| Runner returns non-2xx | Mark submission `tests_passed = false`; continue to next | +| Runner network error / timeout | Mark submission `tests_passed = false`; continue | +| `response_text` is null or blank | Mark `tests_passed = false`; skip runner call | +| Question has no test cases | Skip all submissions for that question; do not touch `tests_passed` | +| Exercise not found | Return `404` | +| No valid instructor session | Return `403` | +| `recalculate-scores` fails | Log error, return partial result with a warning field; do not fail the whole request | + +Runner errors are per-submission failures, not fatal. The batch run always completes and always calls recalculate-scores (unless the exercise is not found or auth fails). + +--- + +## Testing Strategy + +### Unit tests (example-based) + +- `TestCaseEditor` renders existing test cases from props +- `TestCaseEditor` is hidden when `question.type === 'written'` +- `TestCaseEditor` shows success toast on successful save +- `BatchRunButton` renders in idle state +- `BatchRunButton` disables and shows spinner while loading +- `BatchRunButton` displays summary on success +- `BatchRunButton` shows error and re-enables on failure +- Batch run API returns `403` when called without instructor session +- Batch run API returns `404` for unknown exercise ID +- Batch run API returns `503` when `RUNNER_URL` is not configured + +### Property-based tests + +Use [fast-check](https://github.com/dubzzz/fast-check) (already compatible with the project's TypeScript/Node stack). Each property test runs a minimum of **100 iterations**. + +Tag format: `// Feature: batch-test-runner, Property N: ` + +| Property | What to generate | What to assert | +|----------|-----------------|----------------| +| P1: Empty input rejected | Arbitrary whitespace-only strings (including `""`) | No PUT fired; list unchanged | +| P2: Mutation preserves unmodified entries | Random test case arrays; random mutation target | All non-mutated entries survive intact | +| P3: Submission selection filter | Random mix of final/non-final submissions and questions with/without test cases | Processed set equals expected filtered set | +| P4: tests_passed unanimous pass | Random arrays of `{actual, expected}` pairs | `tests_passed === arr.every(r => r.actual.trim() === r.expected.trim())` | +| P5: Idempotence | Random submissions + deterministic mock runner | Two runs yield identical `tests_passed` for all submissions | +| P6: Response counts consistent | Random submission arrays | `passed + failed === processed`; all four fields present | +| P7: Exercise scope isolation | Two exercises A and B with overlapping users | After running on A, B submissions unchanged | + +### Integration test + +One integration test (requires DB + `RUNNER_URL`): +- Create an exercise with a code question with two test cases, create a session with a final submission, run batch test, verify `tests_passed` is set correctly and `score` is updated on the session. diff --git a/.kiro/specs/batch-test-runner/requirements.md b/.kiro/specs/batch-test-runner/requirements.md new file mode 100644 index 0000000..761a7ad --- /dev/null +++ b/.kiro/specs/batch-test-runner/requirements.md @@ -0,0 +1,98 @@ +# Requirements Document + +## Introduction + +The Batch Test Runner feature allows instructors to define automated test cases on code questions and then batch-execute all student submissions for an exercise against those test cases. After running, it saves each submission's pass/fail result and triggers a score recalculation for all participants. This replaces the current manual-override-only workflow for code exercises with an objective, automated grading path. + +The feature integrates with the existing `/api/run-code` infrastructure (external RUNNER_URL service), the `test_cases` JSONB column on questions, the `tests_passed` boolean column on submissions, and the existing `recalculate-scores` endpoint. + +## Glossary + +- **Batch_Runner**: The backend endpoint (`POST /api/instructor/exercises/[id]/batch-test-run`) that orchestrates running all submissions for an exercise against their question's test cases and saving results. +- **Test_Case_Editor**: The UI component embedded in QuestionManager that allows instructors to add, edit, and delete input/expected_output pairs on a code question. +- **Test_Case**: A JSON object with `input` (string) and `expected_output` (string) representing one automated test for a code question. +- **Exercise**: A collection of questions with associated sessions and submissions on the exam platform. +- **Question**: A single problem within an exercise; may be of type `code` or `written`. Only code questions support test cases. +- **Submission**: A student's response to one question within a session, stored in the `submissions` table. +- **Session**: A student's participation record for an exercise, stored in the `sessions` table. +- **Runner_Service**: The external HTTP service at `RUNNER_URL` that compiles and executes code. +- **Recalculate_Scores**: The existing `POST /api/instructor/exercises/[id]/recalculate-scores` endpoint that recomputes scores for all sessions of an exercise. +- **tests_passed**: The boolean column on the `submissions` table that records whether a submission passed all test cases. +- **Batch_Progress_UI**: The React component on the submissions page that displays real-time progress of a batch run to the instructor. + +--- + +## Requirements + +### Requirement 1: Define Test Cases on Code Questions + +**User Story:** As an instructor, I want to add input/expected_output test cases to a code question, so that submissions can be graded automatically rather than relying solely on manual review. + +#### Acceptance Criteria + +1. WHEN an instructor views a code question in the QuestionManager, THE Test_Case_Editor SHALL display the current list of test cases for that question. +2. WHEN an instructor adds a test case with a non-empty `input` and `expected_output`, THE Test_Case_Editor SHALL save the updated test cases to the question's `test_cases` column via a PUT request to `/api/instructor/exercises/[id]/questions/[qid]`. +3. WHEN an instructor deletes a test case, THE Test_Case_Editor SHALL remove that entry and save the updated list. +4. WHEN an instructor edits an existing test case, THE Test_Case_Editor SHALL update that entry and save the updated list. +5. IF the `input` field is empty when saving a test case, THEN THE Test_Case_Editor SHALL reject the entry and display a validation error message. +6. THE Test_Case_Editor SHALL only be visible for questions where `type === 'code'`. +7. WHEN test cases are saved successfully, THE Test_Case_Editor SHALL display a success notification. + +--- + +### Requirement 2: Batch Test Run API Endpoint + +**User Story:** As an instructor, I want a single endpoint that runs all final submissions for an exercise against their question's test cases, so that I can grade everyone at once without manual intervention. + +#### Acceptance Criteria + +1. WHEN a POST request is made to `/api/instructor/exercises/[id]/batch-test-run` by an instructor, THE Batch_Runner SHALL fetch all final submissions for the exercise that belong to questions with at least one test case. +2. FOR EACH submission fetched, THE Batch_Runner SHALL retrieve the corresponding question's test cases and submit the submission's `response_text` and question's `language` to the Runner_Service. +3. WHEN the Runner_Service returns results for all test cases of a submission, THE Batch_Runner SHALL set `tests_passed = true` on that submission if and only if every test case output matches the expected output. +4. WHEN the Runner_Service returns results where at least one test case output does not match, THE Batch_Runner SHALL set `tests_passed = false` on that submission. +5. WHEN all submissions have been processed, THE Batch_Runner SHALL call the Recalculate_Scores endpoint for the exercise. +6. IF a submission's `response_text` is empty or null, THEN THE Batch_Runner SHALL set `tests_passed = false` without calling the Runner_Service. +7. IF the Runner_Service is unavailable (RUNNER_URL not configured), THEN THE Batch_Runner SHALL return a 503 response with an explanatory error message. +8. THE Batch_Runner SHALL be idempotent — re-running it SHALL update `tests_passed` on all matching submissions, overwriting previous results. +9. WHEN the batch run completes, THE Batch_Runner SHALL return a JSON response containing `processed` (count of submissions evaluated), `passed` (count that passed all tests), `failed` (count that failed), and `recalculated` (count of sessions rescored). + +--- + +### Requirement 3: Real-Time Progress Feedback in the UI + +**User Story:** As an instructor, I want to see progress while the batch run executes, so that I know the operation is working and can estimate when it will finish. + +#### Acceptance Criteria + +1. THE Batch_Progress_UI SHALL display a "Run Tests" button on the exercise submissions page. +2. WHEN the instructor clicks "Run Tests", THE Batch_Progress_UI SHALL disable the button and display a loading indicator. +3. WHILE the batch run is in progress, THE Batch_Progress_UI SHALL show a status message indicating the operation is running. +4. WHEN the Batch_Runner responds with a success result, THE Batch_Progress_UI SHALL display the summary: number of submissions processed, number passed, and number failed. +5. IF the Batch_Runner returns an error, THEN THE Batch_Progress_UI SHALL display the error message and re-enable the "Run Tests" button. +6. WHEN the batch run completes successfully, THE Batch_Progress_UI SHALL reload the submissions data to reflect updated scores. +7. THE Batch_Progress_UI SHALL only be visible to instructors. + +--- + +### Requirement 4: Idempotent Results and Score Integration + +**User Story:** As an instructor, I want to re-run the batch test at any time and always get up-to-date results, so that I can re-grade after editing test cases or after students resubmit. + +#### Acceptance Criteria + +1. WHEN the Batch_Runner processes a submission that already has a `tests_passed` value, THE Batch_Runner SHALL overwrite the existing value with the newly computed result. +2. WHEN the Batch_Runner completes, THE Recalculate_Scores endpoint SHALL use the updated `tests_passed` values to compute each session's score, consistent with the existing scoring logic for code questions with test cases. +3. WHEN a question has no test cases defined, THE Batch_Runner SHALL skip all submissions for that question without setting `tests_passed` on them. +4. WHEN a question has test cases defined but a submission is not final, THE Batch_Runner SHALL skip that submission without setting `tests_passed` on it. + +--- + +### Requirement 5: API Authorization and Safety + +**User Story:** As a platform administrator, I want the batch test run endpoint to be protected, so that only instructors can trigger batch operations. + +#### Acceptance Criteria + +1. WHEN a request to `/api/instructor/exercises/[id]/batch-test-run` is made without a valid instructor session, THE Batch_Runner SHALL return a 403 response. +2. WHEN the exercise ID in the request does not correspond to an existing exercise, THE Batch_Runner SHALL return a 404 response. +3. THE Batch_Runner SHALL not execute any submissions for exercises it does not own in the current request context — it SHALL only process submissions belonging to the exercise specified in the URL. diff --git a/.kiro/specs/batch-test-runner/tasks.md b/.kiro/specs/batch-test-runner/tasks.md new file mode 100644 index 0000000..e910092 --- /dev/null +++ b/.kiro/specs/batch-test-runner/tasks.md @@ -0,0 +1,104 @@ +# Implementation Plan: Batch Test Runner + +## Overview + +Implement the Batch Test Runner feature in two parts: a `TestCaseEditor` React component embedded in `QuestionManager` for managing test cases on code questions, and a `POST /api/instructor/exercises/[id]/batch-test-run` API route that executes all final submissions against their test cases and triggers score recalculation. A `BatchRunButton` component wires the UI to the API on the submissions page. + +## Tasks + +- [x] 1. Create the `TestCaseEditor` component + - [x] 1.1 Implement `TestCaseEditor` in `app/instructor/exercises/[id]/TestCaseEditor.tsx` + - Define `TestCase` interface (`input: string`, `expected_output: string`) and component props + - Render existing test cases as a list with inline edit and delete controls + - Add-row form: two text inputs + "Add" button; validate that `input` is non-empty before saving + - All mutations (add, edit, delete) send the full updated array via `PUT /api/instructor/exercises/[id]/questions/[qid]` + - Show success/error toast using `sonner` (already used in `QuestionManager`) + - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.7_ + + - [ ]* 1.2 Write property test for test case validation (P1) + - **Property 1: Test case validation rejects empty input** + - Generate arbitrary whitespace-only strings (including `""`) as input values; assert no PUT is fired and the list remains unchanged + - Tag: `// Feature: batch-test-runner, Property 1: empty input rejected` + - **Validates: Requirements 1.5** + + - [ ]* 1.3 Write property test for mutation preserving unmodified entries (P2) + - **Property 2: Test case list mutation preserves unmodified entries** + - Generate random test case arrays and a random mutation (add/edit/delete); assert all non-targeted entries survive intact + - Tag: `// Feature: batch-test-runner, Property 2: mutation preserves unmodified entries` + - **Validates: Requirements 1.2, 1.3, 1.4** + +- [x] 2. Integrate `TestCaseEditor` into `QuestionManager` + - Render `` inside the expanded question panel in `app/instructor/exercises/[id]/QuestionManager.tsx`, below the starter-code section + - Only render when `q.type === 'code'` (hidden for written questions) + - Pass `exerciseId`, `questionId`, and `initialTestCases` from the question data; questions already include `test_cases` from the GET response — extend the `Question` interface if not already present + - _Requirements: 1.1, 1.6_ + +- [x] 3. Implement the `POST /api/instructor/exercises/[id]/batch-test-run` API route + - [x] 3.1 Create `app/api/instructor/exercises/[id]/batch-test-run/route.ts` + - Auth guard: return `403` if no valid instructor session; return `404` if exercise not found + - Return `503` immediately if `RUNNER_URL` is not configured + - Fetch all final submissions (`is_final = true`) joined with their question's `test_cases`; skip questions with no test cases + - For each submission: if `response_text` is empty/null set `tests_passed = false` without calling runner; otherwise call `RUNNER_URL/run` for each test case (reuse the same fetch-with-retry pattern from `app/api/run-code/route.ts`) + - Compare `stdout.trim()` against `expected_output.trim()`; set `tests_passed = true` only if every test case passes + - Runner errors (non-2xx, network failure) mark that submission `tests_passed = false` and continue + - Bulk `UPDATE submissions SET tests_passed = ...` after processing all + - Call `POST /api/instructor/exercises/[id]/recalculate-scores` internally; if it fails, log and include a `warning` field in the response + - Return `{ processed, passed, failed, recalculated }` (all non-negative integers, `passed + failed === processed`) + - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 4.1, 4.2, 4.3, 4.4, 5.1, 5.2, 5.3_ + + - [ ]* 3.2 Write property test for submission selection filter (P3) + - **Property 3: Submission selection filter** + - Generate a random mix of final/non-final submissions and questions with/without test cases; assert the processed set equals exactly `is_final = true AND question.test_cases IS NOT NULL AND LENGTH(test_cases) > 0` + - Tag: `// Feature: batch-test-runner, Property 3: submission selection filter` + - **Validates: Requirements 2.1, 4.3, 4.4** + + - [ ]* 3.3 Write property test for `tests_passed` unanimous pass rule (P4) + - **Property 4: tests_passed reflects unanimous pass** + - Generate random arrays of `{actual, expected}` pairs; assert `tests_passed === arr.every(r => r.actual.trim() === r.expected.trim())` + - Tag: `// Feature: batch-test-runner, Property 4: tests_passed unanimous pass` + - **Validates: Requirements 2.3, 2.4, 2.6** + + - [ ]* 3.4 Write property test for idempotence (P5) + - **Property 5: Batch run idempotence** + - With a deterministic mock runner, run the batch logic twice; assert all `tests_passed` values are identical after both runs + - Tag: `// Feature: batch-test-runner, Property 5: batch run idempotence` + - **Validates: Requirements 2.8, 4.1** + + - [ ]* 3.5 Write property test for response count consistency (P6) + - **Property 6: Response counts are consistent** + - Generate random submission arrays; assert `passed + failed === processed` and all four fields are present non-negative integers + - Tag: `// Feature: batch-test-runner, Property 6: response counts consistent` + - **Validates: Requirements 2.9** + + - [ ]* 3.6 Write property test for exercise scope isolation (P7) + - **Property 7: Exercise scope isolation** + - Generate two exercises A and B with overlapping user sets; after running batch on A, assert B's submissions are untouched + - Tag: `// Feature: batch-test-runner, Property 7: exercise scope isolation` + - **Validates: Requirements 5.3** + +- [x] 4. Checkpoint — ensure all tests pass + - Ensure all tests pass, ask the user if questions arise. + +- [x] 5. Create the `BatchRunButton` component and wire it to the submissions page + - [x] 5.1 Implement `BatchRunButton` in `app/instructor/exercises/[id]/submissions/BatchRunButton.tsx` + - Idle state: renders a "Run Tests" button + - On click: disable button, show spinner, call `POST /api/instructor/exercises/[id]/batch-test-run` + - On success: display summary `"Processed N submissions — P passed, F failed"` and call `onComplete` prop + - On error: display error message and re-enable button + - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.7_ + + - [x] 5.2 Add `BatchRunButton` to the submissions page + - Convert `app/instructor/exercises/[id]/submissions/page.tsx` to a Client Component wrapper (or add a thin client shell) so `useRouter` is available for `router.refresh()` on `onComplete` + - Place `` alongside the Export CSV button in the page header + - Pass `onComplete={() => router.refresh()}` so updated scores reload automatically + - _Requirements: 3.1, 3.6, 3.7_ + +- [x] 6. Final checkpoint — ensure all tests pass + - Ensure all tests pass, ask the user if questions arise. + +## Notes + +- Tasks marked with `*` are optional and can be skipped for faster MVP +- Property tests use [fast-check](https://github.com/dubzzz/fast-check) with a minimum of 100 iterations each +- The batch-test-run route must not mutate submissions belonging to other exercises (scope enforced by the SQL JOIN on `exercise_id`) +- Runner errors are per-submission, non-fatal — the batch always completes and always calls recalculate-scores unless auth/exercise checks fail diff --git a/app/api/feedback/route.ts b/app/api/feedback/route.ts index 339441f..f7bc7a1 100644 --- a/app/api/feedback/route.ts +++ b/app/api/feedback/route.ts @@ -15,7 +15,6 @@ export async function POST(req: NextRequest) { attachment_url?: string; anonymous_name?: string; anonymous_email?: string; - anonymous_matric?: string; }; try { body = await req.json(); @@ -23,15 +22,15 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } - const { rating, comments, challenges, improvements, malfunctions, attachment_url, anonymous_name, anonymous_email, anonymous_matric } = body; + const { rating, comments, challenges, improvements, malfunctions, attachment_url, anonymous_name, anonymous_email } = body; if (rating && (rating < 1 || rating > 5)) { return NextResponse.json({ error: 'Rating must be between 1 and 5' }, { status: 400 }); } await sql` - INSERT INTO feedback (user_id, rating, comments, challenges, improvements, malfunctions, attachment_url, anonymous_name, anonymous_email, anonymous_matric) - VALUES (${session?.user?.id ?? null}, ${rating ?? null}, ${comments ?? null}, ${challenges ?? null}, ${improvements ?? null}, ${malfunctions ?? null}, ${attachment_url ?? null}, ${anonymous_name ?? null}, ${anonymous_email ?? null}, ${anonymous_matric ?? null}) + INSERT INTO feedback (user_id, rating, comments, challenges, improvements, malfunctions, attachment_url, anonymous_name, anonymous_email) + VALUES (${session?.user?.id ?? null}, ${rating ?? null}, ${comments ?? null}, ${challenges ?? null}, ${improvements ?? null}, ${malfunctions ?? null}, ${attachment_url ?? null}, ${anonymous_name ?? null}, ${anonymous_email ?? null}) `; return NextResponse.json({ success: true }); diff --git a/app/feedback/FeedbackForm.tsx b/app/feedback/FeedbackForm.tsx index ca1f8b8..2a18886 100644 --- a/app/feedback/FeedbackForm.tsx +++ b/app/feedback/FeedbackForm.tsx @@ -15,7 +15,6 @@ export default function FeedbackForm({ isAuthenticated }: { isAuthenticated: boo const [attachmentUrl, setAttachmentUrl] = useState(''); const [anonymousName, setAnonymousName] = useState(''); const [anonymousEmail, setAnonymousEmail] = useState(''); - const [anonymousMatric, setAnonymousMatric] = useState(''); const [submitting, setSubmitting] = useState(false); const ratingConfig = [ @@ -50,7 +49,6 @@ export default function FeedbackForm({ isAuthenticated }: { isAuthenticated: boo attachment_url: attachmentUrl || null, anonymous_name: !isAuthenticated ? anonymousName : undefined, anonymous_email: !isAuthenticated ? anonymousEmail : undefined, - anonymous_matric: !isAuthenticated ? anonymousMatric : undefined, }), }); @@ -83,7 +81,7 @@ export default function FeedbackForm({ isAuthenticated }: { isAuthenticated: boo className="form-input" value={anonymousName} onChange={(e) => setAnonymousName(e.target.value)} - placeholder="Name / Username *" + placeholder="GitTea Username *" required style={{ border: 'none', background: 'var(--bg2)' }} /> @@ -95,14 +93,6 @@ export default function FeedbackForm({ isAuthenticated }: { isAuthenticated: boo placeholder="Email (optional)" style={{ border: 'none', background: 'var(--bg2)' }} /> - setAnonymousMatric(e.target.value)} - placeholder="Matric Number (optional)" - style={{ border: 'none', background: 'var(--bg2)' }} - /> )} diff --git a/app/instructor/feedback/page.tsx b/app/instructor/feedback/page.tsx index 6441363..6a9e764 100644 --- a/app/instructor/feedback/page.tsx +++ b/app/instructor/feedback/page.tsx @@ -19,7 +19,7 @@ export default async function FeedbackPage() { SELECT f.id, f.rating, f.comments, f.challenges, f.improvements, f.malfunctions, f.attachment_url, f.submitted_at, f.anonymous_name, f.anonymous_email, - f.anonymous_matric, u.username + u.username FROM feedback f LEFT JOIN users u ON u.id = f.user_id ORDER BY f.submitted_at DESC @@ -87,14 +87,9 @@ export default async function FeedbackPage() {
{displayName} - {isAnonymous && f.anonymous_matric && ( - - ({f.anonymous_matric}) - - )} {isAnonymous && f.anonymous_email && ( - {f.anonymous_email} + ({f.anonymous_email}) )} {f.rating && ( diff --git a/scripts/seed_test_cases.sql b/scripts/seed_test_cases.sql new file mode 100644 index 0000000..654ca84 --- /dev/null +++ b/scripts/seed_test_cases.sql @@ -0,0 +1,60 @@ +-- Seed test cases for Go Reloaded exercise +-- question_index 0: Drill 1 — Read Input +UPDATE questions SET test_cases = '[{"input":"hello world\nthis is a test","expected_output":"hello world\nthis is a test"}]'::jsonb +WHERE exercise_id = '7b4102ef-57b6-4e17-b631-16232468f82b' AND question_index = 0; + +-- question_index 1: Drill 2 — convertHex +UPDATE questions SET test_cases = '[{"input":"1E (hex) files were added","expected_output":"30 files were added"}]'::jsonb +WHERE exercise_id = '7b4102ef-57b6-4e17-b631-16232468f82b' AND question_index = 1; + +-- question_index 2: Drill 3 — convertBin +UPDATE questions SET test_cases = '[{"input":"It has been 10 (bin) years","expected_output":"It has been 2 years"}]'::jsonb +WHERE exercise_id = '7b4102ef-57b6-4e17-b631-16232468f82b' AND question_index = 2; + +-- question_index 3: Drill 4 — single case modifiers +UPDATE questions SET test_cases = '[{"input":"Ready, set, go (up) !","expected_output":"Ready, set, GO !"}]'::jsonb +WHERE exercise_id = '7b4102ef-57b6-4e17-b631-16232468f82b' AND question_index = 3; + +-- question_index 4: Drill 5 — multi-word case modifiers +UPDATE questions SET test_cases = '[{"input":"it was the age of foolishness (cap, 6)","expected_output":"It Was The Age Of Foolishness"}]'::jsonb +WHERE exercise_id = '7b4102ef-57b6-4e17-b631-16232468f82b' AND question_index = 4; + +-- question_index 5: Drill 6 — fixPunctuation +UPDATE questions SET test_cases = '[{"input":"Punctuation tests are ... kinda boring ,what do you think ?","expected_output":"Punctuation tests are... kinda boring, what do you think?"}]'::jsonb +WHERE exercise_id = '7b4102ef-57b6-4e17-b631-16232468f82b' AND question_index = 5; + +-- question_index 6: Drill 7 — fixSingleQuotes +UPDATE questions SET test_cases = '[{"input":"As Elton John said: '' I am the most well-known homosexual in the world ''","expected_output":"As Elton John said: ''I am the most well-known homosexual in the world''"}]'::jsonb +WHERE exercise_id = '7b4102ef-57b6-4e17-b631-16232468f82b' AND question_index = 6; + +-- question_index 7: Drill 8 — fixArticles +UPDATE questions SET test_cases = '[{"input":"There is no greater agony than bearing a untold story inside you.","expected_output":"There is no greater agony than bearing an untold story inside you."}]'::jsonb +WHERE exercise_id = '7b4102ef-57b6-4e17-b631-16232468f82b' AND question_index = 7; + +-- question_index 8: Drill 9 — full pipeline +UPDATE questions SET test_cases = '[{"input":"it (cap) was the best of times, it was the worst of times (up) , it was the age of wisdom, it was the age of foolishness (cap, 6) , it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of darkness, it was the spring of hope, IT WAS THE (low, 3) winter of despair.","expected_output":"It was the best of times, it was the worst of TIMES, it was the age of wisdom, It Was The Age Of Foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of darkness, it was the spring of hope, it was the winter of despair."}]'::jsonb +WHERE exercise_id = '7b4102ef-57b6-4e17-b631-16232468f82b' AND question_index = 8; + +-- question_index 9: Drill 10 — hex + bin together +UPDATE questions SET test_cases = '[{"input":"Simply add 42 (hex) and 10 (bin) and you will see the result is 68.","expected_output":"Simply add 66 and 2 and you will see the result is 68."}]'::jsonb +WHERE exercise_id = '7b4102ef-57b6-4e17-b631-16232468f82b' AND question_index = 9; + +-- question_index 10: Drill 11 — punctuation cleanup +UPDATE questions SET test_cases = '[{"input":"I was sitting over there ,and then BAMM !!","expected_output":"I was sitting over there, and then BAMM!!"}]'::jsonb +WHERE exercise_id = '7b4102ef-57b6-4e17-b631-16232468f82b' AND question_index = 10; + +-- question_index 11: Drill 12 — single quote formatting +UPDATE questions SET test_cases = '[{"input":"I am exactly how they describe me: '' awesome ''","expected_output":"I am exactly how they describe me: ''awesome''"}]'::jsonb +WHERE exercise_id = '7b4102ef-57b6-4e17-b631-16232468f82b' AND question_index = 11; + +-- question_index 12: Drill 13 — article rule +UPDATE questions SET test_cases = '[{"input":"There it was. A amazing rock!","expected_output":"There it was. An amazing rock!"}]'::jsonb +WHERE exercise_id = '7b4102ef-57b6-4e17-b631-16232468f82b' AND question_index = 12; + +-- question_index 13: Drill 14 — combined rules +UPDATE questions SET test_cases = '[{"input":"it (cap) was the best of times, it was the worst of times (up) , it was the age of wisdom, it was the age of foolishness (cap, 6) , it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of darkness, it was the spring of hope, IT WAS THE (low, 3) winter of despair.","expected_output":"It was the best of times, it was the worst of TIMES, it was the age of wisdom, It Was The Age Of Foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of darkness, it was the spring of hope, it was the winter of despair."}]'::jsonb +WHERE exercise_id = '7b4102ef-57b6-4e17-b631-16232468f82b' AND question_index = 13; + +-- question_index 14: Drill 15 — edge cases (two test cases) +UPDATE questions SET test_cases = '[{"input":"Simply add 42 (hex) and 10 (bin) and you will see the result is 68.","expected_output":"Simply add 66 and 2 and you will see the result is 68."},{"input":"Ready, set, go (up) !","expected_output":"Ready, set, GO!"}]'::jsonb +WHERE exercise_id = '7b4102ef-57b6-4e17-b631-16232468f82b' AND question_index = 14;