pdf-quiz-generator/backend/app/schemas/attempt.py
Daniel 0e6c18d886 feat: figures as records, question-centred dashboard, fewer hints mid-quiz
Figures
A question could carry exactly one stem image and one explanation image, each a
bare path with no title, no legend, and no way for the prose to refer to it.
`question_media` makes a figure a row: it points at an image already in the bank,
carries a role, a label the text can name ("Figure 1"), a caption and an order,
and there can be as many as the question needs. The same radiograph can serve two
questions without being stored twice.

The 346 existing paths were backfilled into figure records and retitled —
`page_339_img_0.png` says where a file came from and nothing about what it shows,
so the filename moved into the caption where it is still searchable, and the
title became something a person can read.

On the editor question: no new platform needed. Milkdown is already installed —
ProseMirror-based, MIT, GFM tables, code blocks, LaTeX — and already used for
articles, courses and the quick question modal. Only the question *page* still
has plain textareas, and that swap is written down rather than rushed, because
the stem carries manual-highlight offsets and a WYSIWYG rewrite would move them.

Fewer hints during a quiz
The category trail and the difficulty pill were shown beside every stem. Being
told a question is filed under Neonatology, or that it is "hard", narrows the
answer before the stem has been read. Both now wait until the answer is in,
where the trail becomes a way to more of the same topic.

The dashboard is about questions
Quizzes and attempts describe how the material happens to be packaged. What a
learner is working through is questions: how many of the bank they have seen,
how many they have answered correctly, and their average. The old per-quiz
performance card — which needed two attempts before it showed anything — is
gone, superseded by the session analysis. The greeting sits above "continue your
study" rather than below it, where it read as a heading for the wrong section.

208 backend, 249 frontend green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-11 03:18:49 +02:00

76 lines
1.8 KiB
Python

from datetime import datetime
from pydantic import BaseModel, Field
class AnswerSubmission(BaseModel):
question_id: int
user_answer: str
class AttemptSubmit(BaseModel):
answers: list[AnswerSubmission]
# {question_id: seconds}. Absent for a client that does not measure, which is
# why the column is nullable rather than defaulted to zero.
timings: dict[int, int] | None = None
class AnswerDetail(BaseModel):
question_id: int
question_text: str
question_type: str
options: list[str] | None = None
user_answer: str
correct_answer: str
is_correct: bool
explanation: str | None
explanation_image_path: str | None = None
image_path: str | None = None
page_reference: int | None = None
category_breadcrumbs: list[dict] = Field(default_factory=list)
class Config:
from_attributes = True
class AttemptResponse(BaseModel):
id: int
quiz_id: int
score: int
total_questions: int
percentage: float
started_at: datetime
completed_at: datetime | None
mode: str | None = None
course_id: int | None = None
allow_review: bool = True
class Config:
from_attributes = True
class AttemptDetail(AttemptResponse):
answers: list[AnswerDetail] = []
# Stats schemas
class QuizStats(BaseModel):
quiz_id: int
quiz_title: str
attempts_count: int
best_score: float
latest_score: float
average_score: float
class DashboardStats(BaseModel):
total_documents: int
total_quizzes: int
total_attempts: int
average_score: float
quiz_stats: list[QuizStats] = []
# Question-centred figures. The quiz counts above describe how the material
# happens to be packaged; these describe what the learner has worked through.
questions_seen: int = 0
questions_correct: int = 0
bank_total: int = 0