#quiz.py import os import streamlit as st from utils.quizdata import quizzes_data import datetime import json from utils import db as dbapi import utils.api as api USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1" def _get_quiz_from_source(quiz_id: int): """ Fetch a quiz payload from the local DB (if enabled) or from the backend API. Expected backend shape: {'quiz': {...}, 'items': [...]} """ if USE_LOCAL_DB and hasattr(dbapi, "get_quiz"): return dbapi.get_quiz(quiz_id) # backend: expose GET /quizzes/{quiz_id} return api.get_quiz(quiz_id) # phase/Student_view/quiz.py def _submit_quiz_result(*, student_id: int, assignment_id: int, quiz_id: int, score: int, total: int, answers: dict): # answers -> goes to DB as 'details' return api.submit_quiz( student_id=student_id, assignment_id=assignment_id, quiz_id=quiz_id, score=score, total=total, details=answers, ) # backend: POST /quizzes/submit (or your route of choice) return api.submit_quiz(student_id=student_id, assignment_id=assignment_id, quiz_id=quiz_id, score=score, total=total, details=details) def _load_quiz_obj(quiz_id): """ Return a normalized quiz object from either quizzes_data (built-in) or the backend/DB. Normalized shape: {"title": str, "questions": [{"question","options","answer","points"}...]} """ # Built-ins first if quiz_id in quizzes_data: q = quizzes_data[quiz_id] for qq in q.get("questions", []): qq.setdefault("points", 1) return q # Teacher-assigned (DB/backend) data = _get_quiz_from_source(int(quiz_id)) # <-- uses API when DISABLE_DB=1 if not data: return {"title": f"Quiz {quiz_id}", "questions": []} items_out = [] for it in (data.get("items") or []): opts = it.get("options") if isinstance(opts, (str, bytes)): try: opts = json.loads(opts) except Exception: opts = [] opts = opts or [] ans = it.get("answer_key") if isinstance(ans, (str, bytes)): try: ans = json.loads(ans) # support '["A","C"]' except Exception: pass # allow "A" def letter_to_text(letter): if isinstance(letter, str): idx = ord(letter.upper()) - 65 return opts[idx] if 0 <= idx < len(opts) else letter return letter if isinstance(ans, list): ans_text = [letter_to_text(a) for a in ans] else: ans_text = letter_to_text(ans) items_out.append({ "question": it.get("question", ""), "options": opts, "answer": ans_text, # text or list of texts "points": int(it.get("points", 1)), }) title = (data.get("quiz") or {}).get("title", f"Quiz {quiz_id}") return {"title": title, "questions": items_out} def _letter_to_index(ch: str) -> int: return ord(ch.upper()) - 65 # 'A'->0, 'B'->1, ... def _correct_to_indices(correct, options: list[str]): """ Map 'correct' (letters like 'A' or ['A','C'] OR option text(s)) -> list of indices. """ idxs = [] if isinstance(correct, list): for c in correct: if isinstance(c, str): if len(c) == 1 and c.isalpha(): idxs.append(_letter_to_index(c)) elif c in options: idxs.append(options.index(c)) elif isinstance(correct, str): if len(correct) == 1 and correct.isalpha(): idxs.append(_letter_to_index(correct)) elif correct in options: idxs.append(options.index(correct)) # keep only valid unique indices return sorted({i for i in idxs if 0 <= i < len(options)}) def _normalize_user_to_indices(user_answer, options: list[str]): """ user_answer can be option text (or list of texts), or letters; return indices. """ idxs = [] if isinstance(user_answer, list): for a in user_answer: if isinstance(a, str): if a in options: idxs.append(options.index(a)) elif len(a) == 1 and a.isalpha(): idxs.append(_letter_to_index(a)) elif isinstance(user_answer, str): if user_answer in options: idxs.append(options.index(user_answer)) elif len(user_answer) == 1 and user_answer.isalpha(): idxs.append(_letter_to_index(user_answer)) return sorted([i for i in idxs if 0 <= i < len(options)]) # --- Helper for level styling --- def get_level_style(level): if level.lower() == "beginner": return ("#28a745", "Beginner") # Green elif level.lower() == "intermediate": return ("#ffc107", "Intermediate") # Yellow elif level.lower() == "advanced": return ("#dc3545", "Advanced") # Red else: return ("#6c757d", level) # --- Sidebar Progress --- def show_quiz_progress_sidebar(quiz_id): qobj = _load_quiz_obj(quiz_id) total_q = max(1, len(qobj.get("questions", []))) current_q = int(st.session_state.get("current_q", 0)) answered_count = len(st.session_state.get("answers", {})) with st.sidebar: st.markdown("""

Quiz Progress

""", unsafe_allow_html=True) st.markdown(f"""
{qobj.get('title','Quiz')}
""", unsafe_allow_html=True) progress_value = (current_q) / total_q if current_q < total_q else 1.0 st.progress(progress_value) st.markdown(f"""
{min(current_q + 1, total_q)} of {total_q}
""", unsafe_allow_html=True) cols = st.columns(5) for i in range(total_q): col = cols[i % 5] with col: if i == current_q and current_q < total_q: st.markdown(f"""
{i + 1}
""", unsafe_allow_html=True) elif i in st.session_state.get("answers", {}): st.markdown(f"""
{i + 1}
""", unsafe_allow_html=True) else: st.markdown(f"""
{i + 1}
""", unsafe_allow_html=True) st.markdown(f"""
Answered ({answered_count})
Current
Not answered
""", unsafe_allow_html=True) if st.button("← Back to Quizzes", use_container_width=True): st.session_state.selected_quiz = None st.rerun() # --- Quiz Question --- def show_quiz(quiz_id): qobj = _load_quiz_obj(quiz_id) q_index = int(st.session_state.current_q) questions = qobj.get("questions", []) question_data = questions[q_index] st.header(qobj.get("title", "Quiz")) st.subheader(question_data.get("question", "")) options = question_data.get("options", []) correct_answer = question_data.get("answer") key = f"q_{q_index}" prev_answer = st.session_state.answers.get(q_index) if isinstance(correct_answer, list): # multiselect; convert any letter defaults to texts default_texts = [] if isinstance(prev_answer, list): for a in prev_answer: if isinstance(a, str): if a in options: default_texts.append(a) elif len(a) == 1 and a.isalpha(): i = _letter_to_index(a) if 0 <= i < len(options): default_texts.append(options[i]) answer = st.multiselect("Select all that apply:", options, default=default_texts, key=key) else: # single answer; compute default index from letter or text if isinstance(prev_answer, str): if prev_answer in options: default_idx = options.index(prev_answer) elif len(prev_answer) == 1 and prev_answer.isalpha(): i = _letter_to_index(prev_answer) default_idx = i if 0 <= i < len(options) else 0 else: default_idx = 0 else: default_idx = 0 answer = st.radio("Select your answer:", options, index=default_idx, key=key) st.session_state.answers[q_index] = answer # auto-save if st.button("Next Question ➡"): st.session_state.current_q += 1 st.rerun() # --- Quiz Results --- def show_results(quiz_id): qobj = _load_quiz_obj(quiz_id) questions = qobj.get("questions", []) total_points = 0 earned_points = 0 details = {"answers": {}} for i, q in enumerate(questions): options = q.get("options", []) or [] pts = int(q.get("points", 1)) total_points += pts correct = q.get("answer") correct_idx = _correct_to_indices(correct, options) user_answer = st.session_state.answers.get(i) user_idx = _normalize_user_to_indices(user_answer, options) is_correct = (sorted(user_idx) == sorted(correct_idx)) if is_correct: earned_points += pts # friendly display correct_disp = ", ".join(options[j] for j in correct_idx if 0 <= j < len(options)) or str(correct) user_disp = ", ".join(options[j] for j in user_idx if 0 <= j < len(options)) or ( ", ".join(user_answer) if isinstance(user_answer, list) else str(user_answer) ) if is_correct: st.markdown(f"✅ **Q{i+1}: {q.get('question','')}** \nYour answer: {user_disp}") else: st.markdown(f"❌ **Q{i+1}: {q.get('question','')}** \nYour answer: {user_disp} \nCorrect answer: {correct_disp}") details["answers"][str(i+1)] = { "question": q.get("question", ""), "selected": user_answer, "correct": correct, "points": pts, "earned": pts if is_correct else 0 } percent = int(round(100 * earned_points / max(1, total_points))) st.success(f"{qobj.get('title','Quiz')} - Completed! 🎉") st.markdown(f"### 🏆 Score: {percent}% ({earned_points}/{total_points} points)") # Save submission to DB for assigned quizzes if isinstance(quiz_id, int): assignment_id = st.session_state.get("current_assignment") if assignment_id: _submit_quiz_result( student_id=st.session_state.user["user_id"], assignment_id=assignment_id, quiz_id=quiz_id, score=int(earned_points), total=int(total_points), details=details ) if st.button("🔁 Retake Quiz"): st.session_state.current_q = 0 st.session_state.answers = {} st.rerun() if st.button("⬅ Back to Quizzes"): st.session_state.selected_quiz = None st.rerun() # tutor handoff (kept as-is) wrong_answers = [] for i, q in enumerate(questions): user_answer = st.session_state.answers.get(i) correct = q.get("answer") if (isinstance(correct, list) and set(user_answer or []) != set(correct)) or (not isinstance(correct, list) and user_answer != correct): wrong_answers.append((q.get("question",""), user_answer, correct, q.get("explanation",""))) if wrong_answers and st.button("💬 Talk to AI Financial Tutor"): st.session_state.selected_quiz = None st.session_state.current_page = "Chatbot" st.session_state.current_q = 0 st.session_state.answers = {} if "messages" not in st.session_state: st.session_state.messages = [] # keep only first 3 wrong items, and cap text length short = wrong_answers[:3] rows = [] for q, ua, ca, ex in short: q = (q or "")[:160] ua = (", ".join(ua) if isinstance(ua, list) else str(ua or ""))[:120] ca = (", ".join(ca) if isinstance(ca, list) else str(ca or ""))[:120] ex = (ex or "")[:200] rows.append(f"Q: {q}\nYour answer: {ua}\nCorrect answer: {ca}\nExplanation: {ex}") handoff = ( "I just completed a financial quiz and missed a few. " "Explain each briefly and give one easy tip to remember:\n\n" + "\n\n".join(rows) ) st.session_state.messages.append({ "id": str(datetime.datetime.now().timestamp()), "text": handoff, "sender": "user", "timestamp": datetime.datetime.now() }) st.session_state.is_typing = True st.rerun() # --- Quiz List --- def show_quiz_list(): st.title("📊 Financial Knowledge Quizzes") st.caption("Test your financial literacy across different modules") cols = st.columns(3) for i, (quiz_id, quiz) in enumerate(quizzes_data.items()): col = cols[i % 3] with col: color, label = get_level_style(quiz["level"]) st.markdown(f"""
{label} ⏱ {quiz['duration']}

{quiz['title']}

{quiz['description']}

📝 {len(quiz['questions'])} questions

""", unsafe_allow_html=True) if st.button("Start Quiz ➡", key=f"quiz_{quiz_id}"): st.session_state.selected_quiz = quiz_id st.session_state.current_q = 0 st.session_state.answers = {} st.rerun() # --- Main Router for Quiz Page --- def show_page(): if "selected_quiz" not in st.session_state: st.session_state.selected_quiz = None if "current_q" not in st.session_state: st.session_state.current_q = 0 if "answers" not in st.session_state: st.session_state.answers = {} if st.session_state.selected_quiz is None: show_quiz_list() else: quiz_id = st.session_state.selected_quiz qobj = _load_quiz_obj(quiz_id) total_q = len(qobj.get("questions", [])) if st.session_state.current_q < total_q: show_quiz(quiz_id) else: show_results(quiz_id) # Note: No changes needed here as this file handles pre-loaded quizzes and teacher-assigned quizzes, # which use /quizzes/{quiz_id} and /quizzes/submit, not /generate_quiz.