"""Helpers for managing the quiz ↔ question junction table.""" from fastapi import HTTPException from sqlalchemy.orm import Session from app.models.question import Question from app.models.quiz_question_link import QuizQuestionLink def validate_key_points(key_points, db=None): """Key points are short text items that may smart-link to an article section.""" if key_points is None: return None if not isinstance(key_points, list) or len(key_points) > 12: raise HTTPException(400, "Key points must be a list of up to 12 items") from app.models.article import Article result = [] for item in key_points: if not isinstance(item, dict) or not isinstance(item.get("text"), str) or not item["text"].strip(): raise HTTPException(400, "Each key point needs text") text = item["text"].strip() if len(text) > 300: raise HTTPException(400, "Key point text is too long (max 300 characters)") article_id = item.get("article_id") section_id = item.get("article_section_id") if article_id is not None: article = db.get(Article, int(article_id)) if db else None if not article: raise HTTPException(400, "Linked article not found") if section_id and section_id not in {s["id"] for s in (article.sections or [])}: raise HTTPException(400, "Linked article section not found") result.append({"text": text, "article_id": int(article_id) if article_id is not None else None, "article_section_id": section_id or None}) return result def validate_option_explanations(options, explanations): """Per-option explanations must be a {option_text: explanation} map within options.""" if explanations is None: return None if not isinstance(explanations, dict): raise HTTPException(400, "Option explanations must be an object keyed by option text") allowed = set(options or []) for key, value in explanations.items(): if not isinstance(key, str) or key not in allowed: raise HTTPException(400, "Option explanation keys must match existing options") if not isinstance(value, str) or len(value) > 2000: raise HTTPException(400, "Option explanations are strings up to 2000 characters") return {key: value for key, value in explanations.items() if value.strip()} def get_quiz_questions(db: Session, quiz_id: int) -> list[Question]: """Fetch questions for a quiz in position order via junction table.""" links = ( db.query(QuizQuestionLink) .filter(QuizQuestionLink.quiz_id == quiz_id) .order_by(QuizQuestionLink.position) .all() ) if not links: return [] q_map = { q.id: q for q in db.query(Question).filter(Question.id.in_([l.question_id for l in links])).all() } return [q_map[l.question_id] for l in links if l.question_id in q_map] def grade_quiz_answers(questions, answers, selected_ids=None): """Grade each selected question once; omissions are recorded as incorrect.""" question_map = {q.id: q for q in questions} ids = list(question_map) if selected_ids is None else selected_ids if not ids or len(ids) != len(set(ids)) or set(ids) - question_map.keys(): raise HTTPException(400, "Question selection is invalid or no longer available") submitted = dict(answers) if len(submitted) != len(answers) or submitted.keys() - set(ids) or any(not isinstance(value, str) for value in submitted.values()): raise HTTPException(400, "Answers contain duplicate or unselected question IDs") grades = [] for qid in ids: question = question_map[qid] answer = submitted.get(qid, "") correct = bool(answer.strip()) and bool(question.correct_answer) and answer.strip().lower() == question.correct_answer.strip().lower() grades.append((question, answer, correct)) return grades def add_questions_to_quiz(db: Session, quiz_id: int, question_ids: list[int], start_pos: int = 0): """Add question links to a quiz (skips duplicates).""" existing = { l.question_id for l in db.query(QuizQuestionLink).filter(QuizQuestionLink.quiz_id == quiz_id).all() } for i, qid in enumerate(question_ids): if qid not in existing: db.add(QuizQuestionLink(quiz_id=quiz_id, question_id=qid, position=start_pos + i)) def question_in_quiz(db: Session, quiz_id: int, question_id: int) -> bool: return db.query(QuizQuestionLink).filter( QuizQuestionLink.quiz_id == quiz_id, QuizQuestionLink.question_id == question_id, ).first() is not None def remove_question_from_quiz(db: Session, quiz_id: int, question_id: int): db.query(QuizQuestionLink).filter( QuizQuestionLink.quiz_id == quiz_id, QuizQuestionLink.question_id == question_id, ).delete()