| """ | |
| Quiz scoring, progress tracking, and performance feedback. | |
| """ | |
| from collections import defaultdict | |
| def calculate_score(answers: dict, questions: list[dict]) -> dict: | |
| """ | |
| answers: {question_index: user_answer} | |
| For MCQ: int (index of chosen option) | |
| For SATA: list[int] (indices of chosen options) | |
| Returns comprehensive score report. | |
| """ | |
| total = len(questions) | |
| correct = 0 | |
| by_category = defaultdict(lambda: {"correct": 0, "total": 0}) | |
| by_difficulty = defaultdict(lambda: {"correct": 0, "total": 0}) | |
| wrong_questions = [] | |
| for i, q in enumerate(questions): | |
| cat = q.get("category", "Unknown") | |
| diff = q.get("difficulty", "unknown") | |
| by_category[cat]["total"] += 1 | |
| by_difficulty[diff]["total"] += 1 | |
| user_ans = answers.get(i) | |
| if user_ans is None: | |
| wrong_questions.append({"question": q, "user_answer": None, "correct": False}) | |
| continue | |
| is_correct = _check_answer(q, user_ans) | |
| if is_correct: | |
| correct += 1 | |
| by_category[cat]["correct"] += 1 | |
| by_difficulty[diff]["correct"] += 1 | |
| else: | |
| wrong_questions.append({ | |
| "question": q, | |
| "user_answer": user_ans, | |
| "correct": False, | |
| }) | |
| pct = round((correct / total) * 100, 1) if total > 0 else 0 | |
| return { | |
| "total": total, | |
| "correct": correct, | |
| "incorrect": total - correct, | |
| "percentage": pct, | |
| "pass": pct >= 60, | |
| "by_category": dict(by_category), | |
| "by_difficulty": dict(by_difficulty), | |
| "wrong_questions": wrong_questions, | |
| "performance_band": _band(pct), | |
| "feedback": _feedback(pct), | |
| } | |
| def _check_answer(question: dict, user_answer) -> bool: | |
| correct = question.get("correct") | |
| if question["type"] == "sata": | |
| if not isinstance(user_answer, (list, set)): | |
| return False | |
| return set(user_answer) == set(correct) | |
| else: | |
| return user_answer == correct | |
| def _band(pct: float) -> str: | |
| if pct >= 85: return "Excellent" | |
| if pct >= 75: return "Proficient" | |
| if pct >= 60: return "Borderline β keep practising" | |
| if pct >= 45: return "Developing β review weak areas" | |
| return "Needs significant review" | |
| def _feedback(pct: float) -> str: | |
| if pct >= 85: | |
| return "Outstanding performance! You demonstrate strong clinical knowledge across NCLEX domains." | |
| if pct >= 75: | |
| return "Good work! You're on track for NCLEX success. Focus on your weaker categories." | |
| if pct >= 60: | |
| return "Borderline pass. Review your missed questions carefully β focus on rationales, not just answers." | |
| if pct >= 45: | |
| return "Keep studying! Focus on understanding WHY each answer is correct using the rationale." | |
| return "Significant review needed. Consider revisiting core nursing content for each category you missed." | |
| def category_percentage(score_report: dict) -> dict[str, float]: | |
| """Return {category: percentage} for charting.""" | |
| out = {} | |
| for cat, data in score_report["by_category"].items(): | |
| t = data["total"] | |
| out[cat] = round((data["correct"] / t) * 100, 1) if t > 0 else 0 | |
| return out | |