"""Which study-plan block a quiz belongs to, for the learner who started it. A session that came out of a study plan is still a session — it sits in the learner's list and has its own analysis — but it also has a place in a sequence. Both the session list and the analysis need to say so, and to offer the way back to the plan and on to the next block, so the lookup lives here rather than in either router. """ from sqlalchemy.orm import Session from app.models.study_plan import StudyPlan, StudyPlanBlock, StudyPlanBlockProgress def plan_context_for_quizzes(db: Session, user_id: int, quiz_ids: list[int]) -> dict[int, dict]: """Map quiz id → plan context, for quizzes that a block produced for this learner. Quizzes with no plan behind them are simply absent from the result. """ if not quiz_ids: return {} rows = ( db.query(StudyPlanBlockProgress, StudyPlanBlock, StudyPlan) .join(StudyPlanBlock, StudyPlanBlock.id == StudyPlanBlockProgress.block_id) .join(StudyPlan, StudyPlan.id == StudyPlanBlock.plan_id) .filter(StudyPlanBlockProgress.user_id == user_id, StudyPlanBlockProgress.quiz_id.in_(quiz_ids)) .all() ) if not rows: return {} # Neighbours come from the plan's full block order, fetched once per plan. plan_ids = {plan.id for _, _, plan in rows} order: dict[int, list[StudyPlanBlock]] = {} for block in (db.query(StudyPlanBlock) .filter(StudyPlanBlock.plan_id.in_(plan_ids)) .order_by(StudyPlanBlock.plan_id, StudyPlanBlock.position).all()): order.setdefault(block.plan_id, []).append(block) out: dict[int, dict] = {} for progress, block, plan in rows: siblings = order.get(plan.id, []) index = next((i for i, b in enumerate(siblings) if b.id == block.id), -1) prev_block = siblings[index - 1] if index > 0 else None next_block = siblings[index + 1] if 0 <= index < len(siblings) - 1 else None out[progress.quiz_id] = { "plan_id": plan.id, "plan_name": plan.name, "block_id": block.id, "block_title": block.title, "block_position": block.position, "block_count": len(siblings), "prev_block_id": prev_block.id if prev_block else None, "next_block_id": next_block.id if next_block else None, "completed": progress.completed_at is not None, } return out def mark_block_complete(db: Session, user_id: int, quiz_id: int) -> None: """Record that the block behind `quiz_id` has been sat through. Called when an attempt is submitted. Idempotent: a block done twice is still done once, and the first completion is the date that counts. """ from datetime import datetime row = (db.query(StudyPlanBlockProgress) .filter(StudyPlanBlockProgress.user_id == user_id, StudyPlanBlockProgress.quiz_id == quiz_id) .first()) if row is not None and row.completed_at is None: row.completed_at = datetime.utcnow() def unmark_block_complete(db: Session, user_id: int, quiz_id: int) -> None: """Put the block behind `quiz_id` back to unfinished. For when the attempt that completed it is deleted. The quiz link stays, so the learner resumes the same session rather than being given a new one. """ row = (db.query(StudyPlanBlockProgress) .filter(StudyPlanBlockProgress.user_id == user_id, StudyPlanBlockProgress.quiz_id == quiz_id) .first()) if row is not None: row.completed_at = None