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 # Question ids where the learner opened a tip before answering. Getting it # right after a nudge is not the same as getting it right, and the two are # worth telling apart without calling either of them wrong. hints: list[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) figures: 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 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