From 365aaf3df0717878a0ee2814a84bbc1c8c15d6f1 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 1 Apr 2026 15:29:38 +0200 Subject: [PATCH] Fix option selection, submit failure, and slow load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root causes: 1. joinedload(Quiz.questions) was SLOWER (129ms vs 75ms) with the custom secondary relationship — removed, back to lazy load 2. Redis progress could have stale attempt_id from a completed submission: GET /attempts/progress now validates the attempt exists and is not completed before returning; stale entries are auto-deleted POST /attempts/{id}/submit now clears Redis progress server-side on submit 3. Navbar jobs polling was running every 4s even with no active jobs, causing unnecessary requests — now polls every 30s when idle, every 3s when jobs are actively running Also cleared 3 stale Redis progress entries that were pointing to already-completed attempts (which caused silent submit failures). Co-Authored-By: Claude Sonnet 4.6 (1M context) --- backend/app/routers/attempts.py | 33 +++++++++++++++++++++++----- frontend/src/components/Navbar.jsx | 12 +++++++--- frontend/src/pages/DashboardPage.jsx | 5 +++-- frontend/src/pages/QuizPage.jsx | 2 +- 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/backend/app/routers/attempts.py b/backend/app/routers/attempts.py index 7ac5722..0d39c28 100644 --- a/backend/app/routers/attempts.py +++ b/backend/app/routers/attempts.py @@ -100,7 +100,7 @@ def submit_attempt( question = questions.get(ans.question_id) if not question: continue - is_correct = ans.user_answer.strip().lower() == question.correct_answer.strip().lower() + is_correct = bool(question.correct_answer) and ans.user_answer.strip().lower() == question.correct_answer.strip().lower() if is_correct: score += 1 db.add(AttemptAnswer( @@ -114,7 +114,7 @@ def submit_attempt( 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() + is_correct = bool(user_answer) and bool(q.correct_answer) and user_answer.strip().lower() == q.correct_answer.strip().lower() answer_details.append(AnswerDetail( question_id=q.id, question_text=q.question_text, @@ -130,6 +130,15 @@ def submit_attempt( attempt.completed_at = datetime.utcnow() db.commit() + # Clear saved progress from Redis when submitted + try: + import redis as redis_lib + from app.config import settings + r = redis_lib.from_url(settings.REDIS_URL, decode_responses=True) + r.delete(f"quiz_progress:{current_user.id}:{attempt.quiz_id}") + except Exception: + pass + percentage = (score / attempt.total_questions * 100) if attempt.total_questions > 0 else 0 # Update reminder schedule @@ -211,17 +220,31 @@ def save_progress( @router.get("/progress") def get_progress( quiz_id: int, + db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): - """Retrieve in-progress quiz answers from Redis.""" + """Retrieve in-progress quiz answers from Redis. + Clears stale data if the saved attempt has already been submitted.""" try: import redis as redis_lib, json as _json from app.config import settings r = redis_lib.from_url(settings.REDIS_URL, decode_responses=True) key = f"quiz_progress:{current_user.id}:{quiz_id}" data = r.get(key) - if data: - return _json.loads(data) + if not data: + return None + saved = _json.loads(data) + attempt_id = saved.get("attempt_id") + if attempt_id: + attempt = db.query(QuizAttempt).filter( + QuizAttempt.id == attempt_id, + QuizAttempt.user_id == current_user.id, + ).first() + # Stale: attempt was submitted or doesn't exist + if not attempt or attempt.completed_at is not None: + r.delete(key) + return None + return saved except Exception: pass return None diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index df7bac0..99e27d9 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -9,15 +9,21 @@ function JobsBadge() { const [open, setOpen] = useState(false) useEffect(() => { + let interval const load = async () => { try { const res = await api.get('/quizzes/jobs') - setAllJobs(res.data) - setActiveJobs(res.data.filter(j => j.status !== 'completed' && j.status !== 'failed')) + const jobs = res.data || [] + setAllJobs(jobs) + const running = jobs.filter(j => j.status !== 'completed' && j.status !== 'failed') + setActiveJobs(running) + // Poll faster when jobs running, slower when idle + clearInterval(interval) + interval = setInterval(load, running.length > 0 ? 3000 : 30000) } catch { } } load() - const interval = setInterval(load, 4000) + interval = setInterval(load, 30000) return () => clearInterval(interval) }, []) diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx index 0b68cc0..2b8612f 100644 --- a/frontend/src/pages/DashboardPage.jsx +++ b/frontend/src/pages/DashboardPage.jsx @@ -13,6 +13,7 @@ function greeting(name) { export default function DashboardPage() { const { user } = useAuth() + const isModerator = user?.role === 'admin' || user?.role === 'moderator' const [documents, setDocuments] = useState([]) const [stats, setStats] = useState(null) const [history, setHistory] = useState([]) @@ -103,8 +104,8 @@ export default function DashboardPage() {
-

Your Documents

- Upload PDF +

{isModerator ? 'Documents' : 'Your Documents'}

+ {isModerator && Upload PDF}
{documents.length === 0 ? (

No documents yet. Upload a PDF to get started!

diff --git a/frontend/src/pages/QuizPage.jsx b/frontend/src/pages/QuizPage.jsx index b687964..2eaef1e 100644 --- a/frontend/src/pages/QuizPage.jsx +++ b/frontend/src/pages/QuizPage.jsx @@ -420,7 +420,7 @@ useEffect(() => { {current.options.map((opt, i) => { const isSelected = answers[current.id] === opt const hasAnswered = isStudy && !!answers[current.id] - const isCorrectOpt = opt === current.correct_answer + const isCorrectOpt = opt.trim().toLowerCase() === (current.correct_answer || '').trim().toLowerCase() const showCorrect = hasAnswered && isCorrectOpt const showWrong = hasAnswered && isSelected && !isCorrectOpt const letter = String.fromCharCode(65 + i)