Download phase/Student_view/quiz.py from FinEDProj/FInFront: direct link, hf CLI and curl.
- Browser
- Download file 16.9 kB
-
https://huggingface.co/spaces/FinEDProj/FInFront/resolve/main/phase/Student_view/quiz.py
- Command line
-
hf download hf://spaces/FinEDProj/FInFront/phase/Student_view/quiz.py
-
curl -L -o quiz.py https://huggingface.co/spaces/FinEDProj/FInFront/resolve/main/phase/Student_view/quiz.py
16.9 kB
| #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(""" | |
| <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;"> | |
| <h3 style="margin: 0; color: #333;">Quiz Progress</h3> | |
| <div style="font-size: 18px;">β°</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(f""" | |
| <div style="margin-bottom: 15px;"> | |
| <strong style="color: #333; font-size: 14px;">{qobj.get('title','Quiz')}</strong> | |
| </div> | |
| """, 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""" | |
| <div style="text-align: center; margin: 10px 0; font-weight: bold; color: #333;"> | |
| {min(current_q + 1, total_q)} of {total_q} | |
| </div> | |
| """, 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""" | |
| <div style="background-color: #28a745; color: white; text-align: center; padding: 8px; border-radius: 6px; margin: 2px; font-weight: bold; font-size: 14px;"> | |
| {i + 1} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| elif i in st.session_state.get("answers", {}): | |
| st.markdown(f""" | |
| <div style="background-color: #d4edda; color: #155724; text-align: center; padding: 8px; border-radius: 6px; margin: 2px; font-size: 14px;"> | |
| {i + 1} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| else: | |
| st.markdown(f""" | |
| <div style="background-color: #f8f9fa; color: #6c757d; text-align: center; padding: 8px; border-radius: 6px; margin: 2px; border: 1px solid #dee2e6; font-size: 14px;"> | |
| {i + 1} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(f""" | |
| <div style="font-size: 12px; color: #666; margin: 15px 0;"> | |
| <div style="margin: 5px 0;"> | |
| <span style="display: inline-block; width: 12px; height: 12px; background-color: #28a745; border-radius: 50%; margin-right: 8px;"></span> | |
| <span>Answered ({answered_count})</span> | |
| </div> | |
| <div style="margin: 5px 0;"> | |
| <span style="display: inline-block; width: 12px; height: 12px; background-color: #17a2b8; border-radius: 50%; margin-right: 8px;"></span> | |
| <span>Current</span> | |
| </div> | |
| <div style="margin: 5px 0;"> | |
| <span style="display: inline-block; width: 12px; height: 12px; background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 50%; margin-right: 8px;"></span> | |
| <span>Not answered</span> | |
| </div> | |
| </div> | |
| """, 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""" | |
| <div style="border:1px solid #e1e5e9; border-radius:12px; padding:20px; margin-bottom:20px; background:white; box-shadow:0 2px 6px rgba(0,0,0,0.08);"> | |
| <span style="background-color:{color}; color:white; font-size:12px; padding:4px 8px; border-radius:6px;">{label}</span> | |
| <span style="float:right; color:#666; font-size:13px;">β± {quiz['duration']}</span> | |
| <h4 style="margin-top:10px; margin-bottom:6px; color:#222;">{quiz['title']}</h4> | |
| <p style="font-size:14px; color:#555; line-height:1.4; margin-bottom:10px;">{quiz['description']}</p> | |
| <p style="font-size:13px; color:#666;">π {len(quiz['questions'])} questions</p> | |
| </div> | |
| """, 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. |