""" Progress API — Tracks student learning progress per unit. Stores coins, crowns, streak data, and exercise completion stats. Uses JSON file storage (same pattern as learning_units.py). """ import os import json import logging from datetime import datetime, date from typing import Dict, Any, Optional, List from pathlib import Path from fastapi import APIRouter, HTTPException from pydantic import BaseModel logger = logging.getLogger(__name__) router = APIRouter( prefix="/progress", tags=["progress"], ) PROGRESS_DIR = os.path.expanduser("~/Arbeitsblaetter/Lerneinheiten/progress") def _ensure_dir(): os.makedirs(PROGRESS_DIR, exist_ok=True) def _progress_path(unit_id: str) -> Path: return Path(PROGRESS_DIR) / f"{unit_id}.json" def _load_progress(unit_id: str) -> Dict[str, Any]: path = _progress_path(unit_id) if path.exists(): with open(path, "r", encoding="utf-8") as f: return json.load(f) return { "unit_id": unit_id, "coins": 0, "crowns": 0, "streak_days": 0, "last_activity": None, "exercises": { "flashcards": {"completed": 0, "correct": 0, "incorrect": 0}, "quiz": {"completed": 0, "correct": 0, "incorrect": 0}, "type": {"completed": 0, "correct": 0, "incorrect": 0}, "story": {"generated": 0}, }, "created_at": datetime.now().isoformat(), } def _save_progress(unit_id: str, data: Dict[str, Any]): _ensure_dir() path = _progress_path(unit_id) with open(path, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) class RewardPayload(BaseModel): exercise_type: str # flashcards, quiz, type, story correct: bool = True first_try: bool = True @router.get("/{unit_id}") def get_progress(unit_id: str): """Get learning progress for a unit.""" return _load_progress(unit_id) @router.post("/{unit_id}/reward") def add_reward(unit_id: str, payload: RewardPayload): """Record an exercise result and award coins.""" progress = _load_progress(unit_id) # Update exercise stats ex = progress["exercises"].get(payload.exercise_type, {"completed": 0, "correct": 0, "incorrect": 0}) ex["completed"] = ex.get("completed", 0) + 1 if payload.correct: ex["correct"] = ex.get("correct", 0) + 1 else: ex["incorrect"] = ex.get("incorrect", 0) + 1 progress["exercises"][payload.exercise_type] = ex # Award coins if payload.correct: coins = 3 if payload.first_try else 1 else: coins = 0 progress["coins"] = progress.get("coins", 0) + coins # Update streak today = date.today().isoformat() last = progress.get("last_activity") if last != today: if last == (date.today().replace(day=date.today().day - 1)).isoformat() if date.today().day > 1 else None: progress["streak_days"] = progress.get("streak_days", 0) + 1 elif last != today: progress["streak_days"] = 1 progress["last_activity"] = today # Award crowns for milestones total_correct = sum( e.get("correct", 0) for e in progress["exercises"].values() if isinstance(e, dict) ) progress["crowns"] = total_correct // 20 # 1 crown per 20 correct answers _save_progress(unit_id, progress) return { "coins_awarded": coins, "total_coins": progress["coins"], "crowns": progress["crowns"], "streak_days": progress["streak_days"], } @router.get("/") def list_all_progress(): """List progress for all units.""" _ensure_dir() results = [] for f in Path(PROGRESS_DIR).glob("*.json"): with open(f, "r", encoding="utf-8") as fh: results.append(json.load(fh)) return results