Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const MANUAL_SCORE = 4;
interface GraderQuestionStat {
name: string;
answered_count: number;
total_attempts: number;
correct_count: number;
}

Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const THRESHOLD_PERCENT = 70;
interface GraderQuestionStat {
name: string;
answered_count: number;
total_attempts: number;
correct_count: number;
}

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export const DeadlineExceptionDialog: React.FC<DeadlineExceptionDialogProps> = (
centered
>
<div className={styles.dialogStack}>
<p className={styles.dialogIntro}>
This accommodation is saved permanently. It remains active until it is changed or deleted.
</p>
<StudentMultiSelect
selected={selectedSids}
onChange={setSelectedSids}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ export const MultiGradeDialog: React.FC<MultiGradeDialogProps> = ({

return (
<Modal
title="Multi-grade selected questions"
title="Manually grade selected questions"
opened={visible}
onClose={close}
size="860px"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ export const RegradeWizard: React.FC<RegradeWizardProps> = ({

{!hasGradeable && (
<div className={styles.calloutWarning}>
None of the selected questions can be auto-graded. Use <strong>Multi-grade</strong> to
grade them by hand.
None of the selected questions can be auto-graded. Use <strong>Grade manually</strong>{" "}
to grade them by hand.
</div>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ function makeQuestion(
question_type: "mchoice",
points: 10,
answered_count: 0,
total_attempts: 0,
correct_count: 0,
average_score: 0,
...overrides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export const GraderQuestionPage: React.FC = () => {
size="xs"
onClick={() => setShowExtraTime(true)}
>
Extra time…
Deadline accommodations…
</Button>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,10 @@ type QuestionData = NonNullable<ReturnType<typeof getDemoQuestionsFor>>;
type QuestionRow = QuestionData["questions"][number];

interface QuestionStats {
attemptsPerStudent: number;
correctPct: number;
pointsPct: number;
isManual: boolean;
usePartial: boolean;
correctLabel: string;
correctTooltip: string;
avgTooltip: string;
}
Expand All @@ -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
Expand All @@ -87,11 +86,10 @@ const computeStats = (q: QuestionRow): QuestionStats => {
: "No graded submissions yet";

return {
attemptsPerStudent,
correctPct,
pointsPct,
isManual,
usePartial,
correctLabel,
correctTooltip,
avgTooltip
};
Expand Down Expand Up @@ -216,7 +214,7 @@ export const GraderQuestionsPage: React.FC = () => {
},
{
accessorKey: "answered_count",
header: "Answered",
header: "Students attempted",
filterFn: numericEquals,
meta: {
headerStyle: { width: 130 },
Expand All @@ -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 (
<span title={stats.correctTooltip}>
<strong>{row.original.correct_count}</strong>
<span className={styles.cellSubtle}> {stats.correctLabel}</span>
<span title="Average number of attempts among students who attempted the question">
<strong>{stats.attemptsPerStudent.toFixed(1)}</strong>
</span>
);
}
Expand Down Expand Up @@ -383,7 +380,7 @@ export const GraderQuestionsPage: React.FC = () => {
onClick={() => setShowMultiGrade(true)}
data-tour="grader-multigrade-button"
>
Multi-grade…
Grade manually…
</Button>
</Tooltip>
<Button
Expand All @@ -392,7 +389,7 @@ export const GraderQuestionsPage: React.FC = () => {
size="xs"
onClick={() => setShowExtraTime(true)}
>
Extra time…
Deadline accommodations…
</Button>
{hasSelection && (
<span className={styles.selectionChip}>
Expand Down Expand Up @@ -501,11 +498,15 @@ export const GraderQuestionsPage: React.FC = () => {
data-tour="grader-q-answered"
title="Distinct students who submitted at least one attempt"
>
<Icon name="users" size={14} /> <strong>{q.answered_count}</strong> answered
<Icon name="users" size={14} /> <strong>{q.answered_count}</strong> students
attempted
</span>
<span data-tour="grader-q-correct" title={stats.correctTooltip}>
<Icon name="check-circle" size={14} /> <strong>{q.correct_count}</strong>{" "}
{stats.correctLabel}
<span
data-tour="grader-q-attempts"
title="Average number of attempts among students who attempted the question"
>
<Icon name="history" size={14} /> <strong>{stats.attemptsPerStudent.toFixed(1)}</strong>{" "}
attempts / student
</span>
</div>
<div className={styles.metaRow}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand All @@ -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
},
Expand All @@ -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
},
Expand All @@ -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
},
Expand All @@ -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
},
Expand All @@ -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
},
Expand All @@ -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
},
Expand All @@ -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
},
Expand All @@ -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
},
Expand All @@ -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
}
Expand Down Expand Up @@ -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)
}))
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,18 @@ 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",
align: "start"
},
{
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"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface GraderQuestionStats {

answered_count: number;

total_attempts: number;

correct_count: number;

graded_count?: number;
Expand Down
13 changes: 13 additions & 0 deletions bases/rsptx/assignment_server_api/routers/grader.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ class GraderQuestionStats(BaseModel):

answered_count: int

total_attempts: int

correct_count: int

graded_count: int
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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 = [
Expand All @@ -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)

Expand Down Expand Up @@ -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),
Expand Down
Loading