From a1e934000415fa26bf7bf5edbaeb8cbd8aa18f01 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 8 Sep 2026 19:07:27 +0200 Subject: [PATCH] fix: auto-start quizzes in their mode, drop quiz code and verbose stats note Timed quizzes start as exams and learning quizzes as study without a second mode prompt; reopening resumes automatically. Removed quiz code display from in-progress list and the verbose statistics basis sentence. Lab rows keep logical age order per test. 93 frontend tests pass. --- backend/app/routers/study_tools.py | 3 +- backend/tests/test_study_tools.py | 5 + frontend/src/components/InProgressQuizzes.jsx | 15 --- frontend/src/pages/QuizPage.jsx | 104 +++--------------- frontend/src/pages/QuizPage.test.jsx | 26 +++-- 5 files changed, 39 insertions(+), 114 deletions(-) diff --git a/backend/app/routers/study_tools.py b/backend/app/routers/study_tools.py index 8afc849..bfbc108 100644 --- a/backend/app/routers/study_tools.py +++ b/backend/app/routers/study_tools.py @@ -59,7 +59,8 @@ def lab_values(include_drafts: bool = False, db: Session = Depends(get_db), user if not include_drafts: query = query.filter(LabReference.is_published.is_(True)) # ponytail: bounded personal-project table; add pagination before exceeding 500 entries. - entries = query.order_by(LabReference.group, LabReference.name, LabReference.age_group).limit(500).all() + # Insertion order keeps each test's age rows in their logical sequence. + entries = query.order_by(LabReference.group, LabReference.name, LabReference.id).limit(500).all() card_rows = db.query(LabReferenceCardLink.lab_reference_id, Flashcard.id, Flashcard.front, Flashcard.deck_id, FlashcardDeck.title).join( Flashcard, Flashcard.id == LabReferenceCardLink.flashcard_id, diff --git a/backend/tests/test_study_tools.py b/backend/tests/test_study_tools.py index d5367c6..9bcbd81 100644 --- a/backend/tests/test_study_tools.py +++ b/backend/tests/test_study_tools.py @@ -192,6 +192,11 @@ class StudyToolTests(unittest.TestCase): self.assertEqual(created.status_code, 201, created.text) entry_id = created.json()['id'] self.assertEqual(self.client.put(f'/study-tools/lab-values/{entry_id}/cards/999').status_code, 404) + second = self.client.post('/study-tools/lab-values', json={**payload, 'age_group': 'z', 'article_id': None, 'article_section_id': None}) + self.assertEqual(second.status_code, 201, second.text) + # Same-name rows keep insertion (logical age) order, not alphabetical age order. + names_in_order = [r['name'] + ':' + r['age_group'] for r in self.client.get('/study-tools/lab-values').json() if r['name'] == 'Deep linked'] + self.assertEqual(names_in_order, ['Deep linked:a', 'Deep linked:z']) self.assertEqual(self.client.put(f'/study-tools/lab-values/{entry_id}/cards/{card.id}').json()['linked'], True) self.assertEqual(self.client.put(f'/study-tools/lab-values/{entry_id}/cards/{card.id}').json()['linked'], False) row = next(r for r in self.client.get('/study-tools/lab-values').json() if r['id'] == entry_id) diff --git a/frontend/src/components/InProgressQuizzes.jsx b/frontend/src/components/InProgressQuizzes.jsx index 72184a2..68e3943 100644 --- a/frontend/src/components/InProgressQuizzes.jsx +++ b/frontend/src/components/InProgressQuizzes.jsx @@ -16,12 +16,6 @@ export default function InProgressQuizzes() { setInProgress(prev => prev.filter(a => a.attempt_id !== attemptId)) } - const copyQuizCode = async (code) => { - try { - await navigator.clipboard.writeText(String(code)) - } catch {} - } - if (inProgress.length === 0) return null return ( @@ -40,15 +34,6 @@ export default function InProgressQuizzes() {
Started {new Date(a.started_at).toLocaleDateString()} · {a.total_questions} questions
-
- Quiz code - - {a.quiz_code || a.quiz_id} - - -
diff --git a/frontend/src/pages/QuizPage.jsx b/frontend/src/pages/QuizPage.jsx index e4a8016..68616ed 100644 --- a/frontend/src/pages/QuizPage.jsx +++ b/frontend/src/pages/QuizPage.jsx @@ -361,86 +361,7 @@ function CourseQuizStart({ quiz, onStart, onShareChanged }) { ) } -function ModeSelectScreen({ quiz, voices, onStart, onShareChanged }) { - const [selectedVoice, setSelectedVoice] = useState(voices.find(v => v.is_default)?.id || voices[0]?.id || '') - const [customTimer, setCustomTimer] = useState(quiz.time_limit_minutes || '') - const [startError, setStartError] = useState('') - const [startingMode, setStartingMode] = useState('') - - const handleStart = async (mode) => { - if (mode === 'exam' && customTimer && (!Number.isInteger(Number(customTimer)) || Number(customTimer) < 1)) { - setStartError('Enter a positive whole number of minutes, or leave the timer blank.') - return - } - const timerMinutes = mode === 'exam' && customTimer ? Number(customTimer) : null - setStartError('') - setStartingMode(mode) - try { - await onStart(mode, selectedVoice, timerMinutes) - } catch { - setStartError('Could not start the quiz. Try again.') - } finally { - setStartingMode('') - } - } - - return ( -
-
-
📝
-

{quiz.title}

- -

- {quiz.questions_count} questions - {quiz.time_limit_minutes ? ` · ${quiz.time_limit_minutes} min limit` : ''} -

-

Choose how to take this quiz:

-
- {[ - { mode: 'study', icon: '📖', label: 'Study Mode', desc: 'Answers & explanations shown as you go', color: '#22c55e', bg: '#f0fdf4' }, - { mode: 'exam', icon: '🎯', label: 'Exam Mode', desc: 'Answers hidden until submitted', color: '#3b82f6', bg: '#eff6ff' }, - ].map(({ mode, icon, label, desc, color, bg }) => ( - - ))} -
- {startError && ( -
- {startError} -
- )} - -
- - setCustomTimer(e.target.value)} - placeholder="No time limit" - onClick={e => e.stopPropagation()} - style={{ padding: '6px 10px', borderRadius: 8, border: '1px solid var(--border)', fontSize: '0.9rem', width: '100%', background: 'var(--input-bg)', color: 'var(--text)' }} /> -

Auto-submits when timer expires. Timer pauses if you leave and resumes when you come back.

-
- - {voices.length > 0 && ( -
- - -
- )} -
-
- ) -} - +// Mode selection prompt removed: general quizzes start in their own mode automatically. function getQuizSessionId() { const key = 'pedshub_quiz_session_id' try { @@ -681,7 +602,8 @@ export default function QuizPage() { const def = voicesRes.data.find(v => v.is_default) if (def) setSelectedVoice(def.id) - // Check for saved progress and auto-resume + // Check for saved progress and auto-resume; otherwise start straight away + // in the quiz's own mode — no second mode prompt. try { const progressRes = await api.get('/attempts/progress', { params: { quiz_id: id }, @@ -689,6 +611,10 @@ export default function QuizPage() { }) if (progressRes.data) { await resumeQuiz(progressRes.data, voicesRes.data) + return + } + if (!returnTo && quizRes.data) { + await startAttempt(quizRes.data.mode === 'timed' ? 'exam' : 'study', null, null) } } catch { setResumeError('Could not restore your saved attempt. Retry resume before starting; your saved answers have not been replaced.') @@ -700,8 +626,7 @@ export default function QuizPage() { return () => clearInterval(timerRef.current) }, [id, resumeRetry]) - const startQuiz = async (mode, voice, timerMinutes = null) => { - if (hasStarted.current || resumeError || loading) return + const startAttempt = async (mode, voice, timerMinutes = null) => { hasStarted.current = true setSelectedVoice(voice) setStarting(true) @@ -754,6 +679,11 @@ export default function QuizPage() { finally { setStarting(false) } } + const startQuiz = async (mode, voice, timerMinutes = null) => { + if (hasStarted.current || resumeError || loading) return + return startAttempt(mode, voice, timerMinutes) + } + const timerStarted = timeLeft !== null useEffect(() => { if (!timerStarted) return @@ -934,7 +864,10 @@ const timerStarted = timeLeft !== null ) : returnTo ? ( setQuiz(q => ({ ...q, share_token: token }))} /> ) : ( - setQuiz(q => ({ ...q, share_token: token }))} /> +
+
+
Starting…
+
)}
) @@ -942,7 +875,6 @@ const timerStarted = timeLeft !== null const answeredCount = Object.keys(answers).length const totalCount = questions.length const isLast = currentIdx === totalCount - 1 - const quizCode = quiz.quiz_code || quiz.id || id const quizNavigation = (position = 'bottom') => (