from datetime import datetime from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from sqlalchemy import func from app.database import get_db from app.models.quiz import Quiz from app.models.question import Question from app.models.attempt import QuizAttempt, AttemptAnswer from app.models.pdf_document import PDFDocument from app.models.user import User from app.schemas.attempt import ( AttemptSubmit, AttemptResponse, AttemptDetail, AnswerDetail, DashboardStats, QuizStats, ) from app.utils.auth import get_current_user from app.utils.quiz_questions import get_quiz_questions router = APIRouter() @router.post("/start", response_model=AttemptResponse) def start_attempt( quiz_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): quiz = db.query(Quiz).filter(Quiz.id == quiz_id).first() if not quiz: raise HTTPException(status_code=404, detail="Quiz not found") attempt = QuizAttempt( quiz_id=quiz_id, user_id=current_user.id, total_questions=quiz.questions_count, ) db.add(attempt) db.commit() db.refresh(attempt) return AttemptResponse( id=attempt.id, quiz_id=attempt.quiz_id, score=0, total_questions=attempt.total_questions, percentage=0.0, started_at=attempt.started_at, completed_at=None, ) @router.post("/{attempt_id}/submit", response_model=AttemptDetail) def submit_attempt( attempt_id: int, submission: AttemptSubmit, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): attempt = db.query(QuizAttempt).filter( QuizAttempt.id == attempt_id, QuizAttempt.user_id == current_user.id, ).first() if not attempt: raise HTTPException(status_code=404, detail="Attempt not found") if attempt.completed_at: raise HTTPException(status_code=400, detail="Attempt already submitted") # Get all questions for this quiz via junction table questions = {q.id: q for q in get_quiz_questions(db, attempt.quiz_id)} score = 0 submitted = {ans.question_id: ans.user_answer for ans in submission.answers} # Save submitted answers for ans in submission.answers: question = questions.get(ans.question_id) if not question: continue is_correct = ans.user_answer.strip().lower() == question.correct_answer.strip().lower() if is_correct: score += 1 db.add(AttemptAnswer( attempt_id=attempt_id, question_id=ans.question_id, user_answer=ans.user_answer, is_correct=is_correct, )) # Build full review — include ALL questions, unanswered marked as incorrect answer_details = [] for q in get_quiz_questions(db, attempt.quiz_id): user_answer = submitted.get(q.id, "") is_correct = bool(user_answer) and user_answer.strip().lower() == q.correct_answer.strip().lower() answer_details.append(AnswerDetail( question_id=q.id, question_text=q.question_text, question_type=q.question_type, options=q.options, user_answer=user_answer, correct_answer=q.correct_answer, is_correct=is_correct, explanation=q.explanation, )) attempt.score = score attempt.completed_at = datetime.utcnow() db.commit() percentage = (score / attempt.total_questions * 100) if attempt.total_questions > 0 else 0 # Update reminder schedule try: from app.services.reminder_service import update_reminder_schedule update_reminder_schedule(db, current_user.id, attempt.quiz_id, percentage) except Exception: pass # Don't fail submission if reminder update fails return AttemptDetail( id=attempt.id, quiz_id=attempt.quiz_id, score=score, total_questions=attempt.total_questions, percentage=round(percentage, 1), started_at=attempt.started_at, completed_at=attempt.completed_at, answers=answer_details, ) @router.get("/", response_model=list[AttemptResponse]) def list_attempts( quiz_id: int | None = None, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): query = db.query(QuizAttempt).filter(QuizAttempt.user_id == current_user.id) if quiz_id: query = query.filter(QuizAttempt.quiz_id == quiz_id) attempts = query.order_by(QuizAttempt.started_at.desc()).all() return [ AttemptResponse( id=a.id, quiz_id=a.quiz_id, score=a.score, total_questions=a.total_questions, percentage=round((a.score / a.total_questions * 100) if a.total_questions > 0 else 0, 1), started_at=a.started_at, completed_at=a.completed_at, ) for a in attempts ] @router.get("/history") def get_quiz_history( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """Return per-quiz attempt history for the performance line graph.""" completed = db.query(QuizAttempt).filter( QuizAttempt.user_id == current_user.id, QuizAttempt.completed_at.isnot(None), ).order_by(QuizAttempt.completed_at).all() # Group by quiz from collections import defaultdict by_quiz: dict = defaultdict(list) quiz_titles: dict = {} for a in completed: pct = round((a.score / a.total_questions * 100) if a.total_questions > 0 else 0, 1) by_quiz[a.quiz_id].append({ "date": a.completed_at.isoformat(), "percentage": pct, "score": a.score, "total": a.total_questions, }) if a.quiz_id not in quiz_titles: quiz = db.query(Quiz).filter(Quiz.id == a.quiz_id).first() quiz_titles[a.quiz_id] = quiz.title if quiz else f"Quiz {a.quiz_id}" return [ {"quiz_id": qid, "title": quiz_titles[qid], "attempts": attempts} for qid, attempts in by_quiz.items() ] @router.get("/stats/dashboard", response_model=DashboardStats) def get_dashboard_stats( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): total_docs = db.query(PDFDocument).filter(PDFDocument.user_id == current_user.id).count() # Count distinct quizzes the user has attempted (not created) total_quizzes = db.query(QuizAttempt.quiz_id).filter( QuizAttempt.user_id == current_user.id, QuizAttempt.completed_at.isnot(None), ).distinct().count() completed_attempts = db.query(QuizAttempt).filter( QuizAttempt.user_id == current_user.id, QuizAttempt.completed_at.isnot(None), ).all() total_attempts = len(completed_attempts) avg_score = 0.0 if completed_attempts: scores = [(a.score / a.total_questions * 100) if a.total_questions > 0 else 0 for a in completed_attempts] avg_score = round(sum(scores) / len(scores), 1) # Per-quiz stats — based on quizzes the user has attempted, not created quiz_stats = [] attempted_quiz_ids = {a.quiz_id for a in completed_attempts} quizzes = db.query(Quiz).filter(Quiz.id.in_(attempted_quiz_ids)).all() if attempted_quiz_ids else [] for quiz in quizzes: quiz_attempts = [a for a in completed_attempts if a.quiz_id == quiz.id] if quiz_attempts: pcts = [(a.score / a.total_questions * 100) if a.total_questions > 0 else 0 for a in quiz_attempts] quiz_stats.append(QuizStats( quiz_id=quiz.id, quiz_title=quiz.title, attempts_count=len(quiz_attempts), best_score=round(max(pcts), 1), latest_score=round(pcts[-1], 1), average_score=round(sum(pcts) / len(pcts), 1), )) return DashboardStats( total_documents=total_docs, total_quizzes=total_quizzes, total_attempts=total_attempts, average_score=avg_score, quiz_stats=quiz_stats, ) @router.get("/{attempt_id}", response_model=AttemptDetail) def get_attempt( attempt_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): attempt = db.query(QuizAttempt).filter( QuizAttempt.id == attempt_id, QuizAttempt.user_id == current_user.id, ).first() if not attempt: raise HTTPException(status_code=404, detail="Attempt not found") # Show ALL questions in review, not just answered ones submitted_map = {ans.question_id: ans for ans in attempt.answers} answer_details = [] for q in get_quiz_questions(db, attempt.quiz_id): ans = submitted_map.get(q.id) answer_details.append(AnswerDetail( question_id=q.id, question_text=q.question_text, question_type=q.question_type, options=q.options, user_answer=ans.user_answer if ans else "", correct_answer=q.correct_answer, is_correct=ans.is_correct if ans else False, explanation=q.explanation, )) percentage = (attempt.score / attempt.total_questions * 100) if attempt.total_questions > 0 else 0 return AttemptDetail( id=attempt.id, quiz_id=attempt.quiz_id, score=attempt.score, total_questions=attempt.total_questions, percentage=round(percentage, 1), started_at=attempt.started_at, completed_at=attempt.completed_at, answers=answer_details, )