Fix option selection, submit failure, and slow load
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) <noreply@anthropic.com>
This commit is contained in:
parent
1f95c4d1ea
commit
365aaf3df0
4 changed files with 41 additions and 11 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}, [])
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
|
||||
<div className="card">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<h2>Your Documents</h2>
|
||||
<Link to="/upload" className="btn btn-primary">Upload PDF</Link>
|
||||
<h2>{isModerator ? 'Documents' : 'Your Documents'}</h2>
|
||||
{isModerator && <Link to="/upload" className="btn btn-primary">Upload PDF</Link>}
|
||||
</div>
|
||||
{documents.length === 0 ? (
|
||||
<div className="empty-state"><p>No documents yet. Upload a PDF to get started!</p></div>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue