From f244484d05447c7dce8c6c67add976f531ff108a Mon Sep 17 00:00:00 2001 From: Andrey Morozov Date: Thu, 17 Sep 2026 17:24:28 +0200 Subject: [PATCH] f-1252 changes to summary data for questions --- .../e2e/grader/grader-cycle.spec.ts | 2 + .../e2e/grader/grader-full-cycle.spec.ts | 2 + .../e2e/grader/grader.spec.ts | 4 +- .../components/DeadlineExceptionDialog.tsx | 3 ++ .../Grader/components/MultiGradeDialog.tsx | 2 +- .../Grader/components/RegradeWizard.tsx | 4 +- .../Grader/hooks/useStudentNavigation.spec.ts | 1 + .../Grader/pages/GraderQuestionPage.tsx | 2 +- .../Grader/pages/GraderQuestionsPage.tsx | 43 ++++++++++--------- .../routes/Grader/tour/graderDemoData.ts | 13 +++++- .../routes/Grader/tour/graderTourConfig.ts | 8 ++-- .../src/store/grader/grader.logic.api.spec.ts | 2 + .../src/store/grader/grader.logic.api.ts | 2 + .../assignment_server_api/routers/grader.py | 13 ++++++ 14 files changed, 69 insertions(+), 32 deletions(-) diff --git a/bases/rsptx/assignment_server_api/assignment_builder/e2e/grader/grader-cycle.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/e2e/grader/grader-cycle.spec.ts index 3d7909408..4cd73acdb 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/e2e/grader/grader-cycle.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/e2e/grader/grader-cycle.spec.ts @@ -21,6 +21,7 @@ const MANUAL_SCORE = 4; interface GraderQuestionStat { name: string; answered_count: number; + total_attempts: number; correct_count: number; } @@ -114,6 +115,7 @@ test( const stat = await fetchQuestionStat(page.request, assignmentId, divId); expect(stat.answered_count).toBe(3); + expect(stat.total_attempts).toBe(3); expect(stat.correct_count).toBe(2); await gotoApp( diff --git a/bases/rsptx/assignment_server_api/assignment_builder/e2e/grader/grader-full-cycle.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/e2e/grader/grader-full-cycle.spec.ts index bdeed5bed..f6ac0f3aa 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/e2e/grader/grader-full-cycle.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/e2e/grader/grader-full-cycle.spec.ts @@ -25,6 +25,7 @@ const THRESHOLD_PERCENT = 70; interface GraderQuestionStat { name: string; answered_count: number; + total_attempts: number; correct_count: number; } @@ -189,6 +190,7 @@ test( const stat = await fetchQuestionStat(page.request, assignmentId, divId); expect(stat.answered_count, "both students submitted").toBe(2); + expect(stat.total_attempts, "each student submitted once").toBe(2); expect(stat.correct_count, "only the correct submission counts").toBe(1); await gradeInSplitView(page, assignmentId, question, correctSid, CORRECT_GRADE); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/e2e/grader/grader.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/e2e/grader/grader.spec.ts index 09c184c41..1b487050d 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/e2e/grader/grader.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/e2e/grader/grader.spec.ts @@ -165,8 +165,8 @@ const TOUR_TITLES = [ "Assignment card", "Questions overview", "Question card", - "Answered", - "Fully correct or fully scored", + "Students attempted", + "Attempts per student", "Average score", "% correct or % credit", "Progress bar", diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/DeadlineExceptionDialog.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/DeadlineExceptionDialog.tsx index 5b947898d..a133d13dc 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/DeadlineExceptionDialog.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/DeadlineExceptionDialog.tsx @@ -66,6 +66,9 @@ export const DeadlineExceptionDialog: React.FC = ( centered >
+

+ This accommodation is saved permanently. It remains active until it is changed or deleted. +

= ({ return ( = ({ {!hasGradeable && (
- None of the selected questions can be auto-graded. Use Multi-grade to - grade them by hand. + None of the selected questions can be auto-graded. Use Grade manually{" "} + to grade them by hand.
)} diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/hooks/useStudentNavigation.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/hooks/useStudentNavigation.spec.ts index 15cbbeab2..cc7ab4677 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/hooks/useStudentNavigation.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/hooks/useStudentNavigation.spec.ts @@ -42,6 +42,7 @@ function makeQuestion( question_type: "mchoice", points: 10, answered_count: 0, + total_attempts: 0, correct_count: 0, average_score: 0, ...overrides diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/pages/GraderQuestionPage.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/pages/GraderQuestionPage.tsx index feba68aa7..f77821491 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/pages/GraderQuestionPage.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/pages/GraderQuestionPage.tsx @@ -306,7 +306,7 @@ export const GraderQuestionPage: React.FC = () => { size="xs" onClick={() => setShowExtraTime(true)} > - Extra time… + Deadline accommodations…
)} diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/pages/GraderQuestionsPage.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/pages/GraderQuestionsPage.tsx index b770f2fef..26d5bf916 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/pages/GraderQuestionsPage.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/pages/GraderQuestionsPage.tsx @@ -54,11 +54,10 @@ type QuestionData = NonNullable>; type QuestionRow = QuestionData["questions"][number]; interface QuestionStats { + attemptsPerStudent: number; correctPct: number; pointsPct: number; - isManual: boolean; usePartial: boolean; - correctLabel: string; correctTooltip: string; avgTooltip: string; } @@ -75,8 +74,8 @@ const computeStats = (q: QuestionRow): QuestionStats => { const pointsPct = q.points > 0 ? (q.average_score / q.points) * 100 : 0; const avgDenominator = q.graded_count ?? 0; + const attemptsPerStudent = q.answered_count > 0 ? q.total_attempts / q.answered_count : 0; - const correctLabel = usePartial ? "avg. credit" : isManual ? "fully scored" : "fully correct"; const correctTooltip = usePartial ? `Mean partial credit across all answers (${q.question_type})` : isManual @@ -87,11 +86,10 @@ const computeStats = (q: QuestionRow): QuestionStats => { : "No graded submissions yet"; return { + attemptsPerStudent, correctPct, pointsPct, - isManual, usePartial, - correctLabel, correctTooltip, avgTooltip }; @@ -216,7 +214,7 @@ export const GraderQuestionsPage: React.FC = () => { }, { accessorKey: "answered_count", - header: "Answered", + header: "Students attempted", filterFn: numericEquals, meta: { headerStyle: { width: 130 }, @@ -232,21 +230,20 @@ export const GraderQuestionsPage: React.FC = () => { ) }, { - accessorKey: "correct_count", - header: "Correct", - filterFn: numericEquals, + id: "attempts_per_student", + header: "Attempts / student", + accessorFn: (row) => (row.answered_count > 0 ? row.total_attempts / row.answered_count : 0), + enableColumnFilter: false, meta: { - headerStyle: { width: 130 }, + headerStyle: { width: 160 }, align: "right", - cellClassName: "numeric", - filter: { variant: "numeric", placeholder: "=" } + cellClassName: "numeric" }, cell: ({ row }) => { const stats = computeStats(row.original); return ( - - {row.original.correct_count} - {stats.correctLabel} + + {stats.attemptsPerStudent.toFixed(1)} ); } @@ -383,7 +380,7 @@ export const GraderQuestionsPage: React.FC = () => { onClick={() => setShowMultiGrade(true)} data-tour="grader-multigrade-button" > - Multi-grade… + Grade manually… {hasSelection && ( @@ -501,11 +498,15 @@ export const GraderQuestionsPage: React.FC = () => { data-tour="grader-q-answered" title="Distinct students who submitted at least one attempt" > - {q.answered_count} answered + {q.answered_count} students + attempted - - {q.correct_count}{" "} - {stats.correctLabel} + + {stats.attemptsPerStudent.toFixed(1)}{" "} + attempts / student
diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/tour/graderDemoData.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/tour/graderDemoData.ts index 8e4560253..6d2477b87 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/tour/graderDemoData.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/tour/graderDemoData.ts @@ -159,6 +159,7 @@ export const DEMO_QUESTIONS: GraderQuestionsResponse = { autograde: "all_or_nothing", which_to_grade: "best_answer", answered_count: 22, + total_attempts: 41, correct_count: 17, average_score: 3.9 }, @@ -170,6 +171,7 @@ export const DEMO_QUESTIONS: GraderQuestionsResponse = { autograde: "unittest", which_to_grade: "last_answer", answered_count: 20, + total_attempts: 56, correct_count: 11, average_score: 5.2 }, @@ -181,6 +183,7 @@ export const DEMO_QUESTIONS: GraderQuestionsResponse = { autograde: "manual", which_to_grade: "best_answer", answered_count: 18, + total_attempts: 25, correct_count: 14, average_score: 3.4 }, @@ -192,6 +195,7 @@ export const DEMO_QUESTIONS: GraderQuestionsResponse = { autograde: "all_or_nothing", which_to_grade: "first_answer", answered_count: 24, + total_attempts: 37, correct_count: 19, average_score: 2.6 }, @@ -203,6 +207,7 @@ export const DEMO_QUESTIONS: GraderQuestionsResponse = { autograde: "manual", which_to_grade: "manual", answered_count: 15, + total_attempts: 18, correct_count: 0, average_score: 3.1 }, @@ -214,6 +219,7 @@ export const DEMO_QUESTIONS: GraderQuestionsResponse = { autograde: "all_or_nothing", which_to_grade: "best_answer", answered_count: 19, + total_attempts: 31, correct_count: 12, average_score: 2.0 }, @@ -225,6 +231,7 @@ export const DEMO_QUESTIONS: GraderQuestionsResponse = { autograde: "all_or_nothing", which_to_grade: "best_answer", answered_count: 17, + total_attempts: 29, correct_count: 10, average_score: 2.6 }, @@ -236,6 +243,7 @@ export const DEMO_QUESTIONS: GraderQuestionsResponse = { autograde: "interact", which_to_grade: "last_answer", answered_count: 13, + total_attempts: 13, correct_count: 13, average_score: 3.0 }, @@ -247,6 +255,7 @@ export const DEMO_QUESTIONS: GraderQuestionsResponse = { autograde: "all_or_nothing", which_to_grade: "best_answer", answered_count: 16, + total_attempts: 27, correct_count: 9, average_score: 1.3 }, @@ -258,6 +267,7 @@ export const DEMO_QUESTIONS: GraderQuestionsResponse = { autograde: "all_or_nothing", which_to_grade: "best_answer", answered_count: 8, + total_attempts: 12, correct_count: 5, average_score: 1.4 } @@ -479,7 +489,8 @@ export const getDemoQuestionsFor = (aid: number): GraderQuestionsResponse | null ...q, id: q.id + 10_000, name: `alt_${q.name}`, - answered_count: Math.max(0, q.answered_count - 4 - i) + answered_count: Math.max(0, q.answered_count - 4 - i), + total_attempts: Math.max(0, q.total_attempts - 6 - i) })) }; } diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/tour/graderTourConfig.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/tour/graderTourConfig.ts index 07e557ae2..073615f13 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/tour/graderTourConfig.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/tour/graderTourConfig.ts @@ -90,7 +90,7 @@ export const GRADER_TOUR_STEPS: TourStepConfig[] = [ { route: "questions", element: '[data-tour="grader-q-answered"]', - title: "Answered", + title: "Students attempted", description: "How many students submitted at least one attempt. For ActiveCode this also includes students who only ran the code, so the card matches what you'll see on the per-question screen.", side: "right", @@ -98,10 +98,10 @@ export const GRADER_TOUR_STEPS: TourStepConfig[] = [ }, { route: "questions", - element: '[data-tour="grader-q-correct"]', - title: "Fully correct or fully scored", + element: '[data-tour="grader-q-attempts"]', + title: "Attempts per student", description: - "Students whose last attempt is a complete solution. The label changes with the question type:\n• Auto-graded types (multiple choice, ActiveCode, CodeLens, WebWork, …) show 'fully correct': answers the system marked fully correct.\n• Manually graded types (short answer, manual Parsons, …) show 'fully scored': students whose recorded score equals the question's maximum points.\n• Partial-credit types (drag and drop, clickable areas, matching, fill in the blank, Parsons) show 'avg. credit' instead; see the next step.", + "The average number of attempts made by students who attempted the question. Students with no submission are not included.", side: "right", align: "start" }, diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/grader/grader.logic.api.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/grader/grader.logic.api.spec.ts index 13cf16fc1..6410862bf 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/grader/grader.logic.api.spec.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/grader/grader.logic.api.spec.ts @@ -204,6 +204,7 @@ describe("getGraderQuestions transformResponse", () => { question_type: "mchoice", points: 5, answered_count: 20, + total_attempts: 42, correct_count: 15, average_score: 3.5 } @@ -225,6 +226,7 @@ describe("getGraderQuestions transformResponse", () => { }; const q = response.detail.questions[0]; expect(q.answered_count).toBe(20); + expect(q.total_attempts).toBe(42); expect(q.correct_count).toBe(15); expect(q.average_score).toBe(3.5); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/store/grader/grader.logic.api.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/store/grader/grader.logic.api.ts index f7d038c27..37194d98e 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/store/grader/grader.logic.api.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/store/grader/grader.logic.api.ts @@ -23,6 +23,8 @@ export interface GraderQuestionStats { answered_count: number; + total_attempts: number; + correct_count: number; graded_count?: number; diff --git a/bases/rsptx/assignment_server_api/routers/grader.py b/bases/rsptx/assignment_server_api/routers/grader.py index 9fbee9b28..d5375bf0e 100644 --- a/bases/rsptx/assignment_server_api/routers/grader.py +++ b/bases/rsptx/assignment_server_api/routers/grader.py @@ -74,6 +74,8 @@ class GraderQuestionStats(BaseModel): answered_count: int + total_attempts: int + correct_count: int graded_count: int @@ -153,6 +155,7 @@ async def list_assignment_questions( interaction_events = interaction_events_for(q.question_type) answered_count = 0 + total_attempts = 0 correct_count = 0 graded_count = 0 average_score = 0.0 @@ -183,6 +186,10 @@ async def list_assignment_questions( select(func.distinct(tbl.sid)).where(and_(*base_clauses)) ) answered_sids.update(s for (s,) in res.all() if s) + res = await session.execute( + select(func.count(tbl.id)).where(and_(*base_clauses)) + ) + total_attempts += int(res.scalar() or 0) if interaction_events: for u in await fetch_interaction_useinfo( @@ -192,6 +199,7 @@ async def list_assignment_questions( continue if is_interaction_event(u.event, u.act): answered_sids.add(u.sid) + total_attempts += 1 if q.question_type in CODE_TABLE_TYPES: code_clauses = [ @@ -204,6 +212,10 @@ async def list_assignment_questions( select(func.distinct(Code.sid)).where(and_(*code_clauses)) ) answered_sids.update(s for (s,) in res.all() if s) + res = await session.execute( + select(func.count(Code.id)).where(and_(*code_clauses)) + ) + total_attempts += int(res.scalar() or 0) answered_count = len(answered_sids) @@ -295,6 +307,7 @@ async def list_assignment_questions( autograde=aq.autograde, which_to_grade=aq.which_to_grade, answered_count=answered_count, + total_attempts=total_attempts, correct_count=correct_count, graded_count=graded_count, average_score=round(average_score, 2),