# ============================================== # Breakpilot Drive - Extended DB Methods # ============================================== # Leaderboard extensions, parent dashboard, achievements, progress. import logging from typing import List, Dict, Any from .database_models import Achievement, ACHIEVEMENTS logger = logging.getLogger(__name__) class ExtrasMixin: """Mixin providing leaderboard, parent dashboard, and achievement methods.""" async def get_class_leaderboard( self, class_id: str, timeframe: str = "week", limit: int = 10 ) -> List[Dict[str, Any]]: """ Get leaderboard filtered by class. Note: Requires class_id to be stored in user metadata or a separate class_memberships table. For now, this is a placeholder that can be extended. """ # For now, fall back to regular leaderboard # TODO: Join with class_memberships table when available return await self.get_leaderboard(timeframe, limit) async def get_leaderboard_with_names( self, timeframe: str = "day", limit: int = 10, anonymize: bool = True ) -> List[Dict[str, Any]]: """Get leaderboard with anonymized display names.""" leaderboard = await self.get_leaderboard(timeframe, limit) # Anonymize names for privacy (e.g., "Spieler 1", "Spieler 2") if anonymize: for entry in leaderboard: entry["display_name"] = f"Spieler {entry['rank']}" else: # In production: Join with users table to get real names for entry in leaderboard: entry["display_name"] = f"Spieler {entry['rank']}" return leaderboard # ============================================== # Parent Dashboard Methods # ============================================== async def get_children_stats( self, children_ids: List[str] ) -> List[Dict[str, Any]]: """Get stats for multiple children (parent dashboard).""" if not children_ids: return [] results = [] for child_id in children_ids: state = await self.get_learning_state(child_id) sessions = await self.get_user_sessions(child_id, limit=5) results.append({ "student_id": child_id, "learning_state": state.to_dict() if state else None, "recent_sessions": sessions, "has_played": state is not None and state.total_sessions > 0, }) return results async def get_progress_over_time( self, student_id: str, days: int = 30 ) -> List[Dict[str, Any]]: """Get learning progress over time for charts.""" await self._ensure_connected() if not self._pool: return [] try: async with self._pool.acquire() as conn: rows = await conn.fetch( """ SELECT DATE(ended_at) as date, COUNT(*) as sessions, SUM(score) as total_score, SUM(questions_answered) as questions, SUM(questions_correct) as correct, AVG(difficulty_level) as avg_difficulty FROM game_sessions WHERE student_id = $1 AND ended_at > NOW() - make_interval(days => $2) GROUP BY DATE(ended_at) ORDER BY date ASC """, student_id, days ) return [ { "date": row["date"].isoformat(), "sessions": row["sessions"], "total_score": int(row["total_score"]), "questions": row["questions"], "correct": row["correct"], "accuracy": row["correct"] / row["questions"] if row["questions"] > 0 else 0, "avg_difficulty": float(row["avg_difficulty"]) if row["avg_difficulty"] else 3.0, } for row in rows ] except Exception as e: logger.error(f"Failed to get progress over time: {e}") return [] # ============================================== # Achievement Methods # ============================================== async def get_student_achievements( self, student_id: str ) -> List[Achievement]: """Get achievements with unlock status for a student.""" await self._ensure_connected() # Get student stats for progress calculation state = await self.get_learning_state(student_id) # Calculate progress for each achievement achievements = [] for a in ACHIEVEMENTS: achievement = Achievement( id=a.id, name=a.name, description=a.description, icon=a.icon, category=a.category, threshold=a.threshold, ) # Calculate progress based on category if state: if a.category == "general": achievement.progress = state.total_sessions achievement.unlocked = state.total_sessions >= a.threshold elif a.category == "time": achievement.progress = state.total_play_time_minutes achievement.unlocked = state.total_play_time_minutes >= a.threshold elif a.category == "level": achievement.progress = state.overall_level achievement.unlocked = state.overall_level >= a.threshold elif a.category == "accuracy": if a.id == "accuracy_80" and state.questions_answered >= 50: achievement.progress = int(state.accuracy * 100) achievement.unlocked = state.accuracy >= 0.8 achievements.append(achievement) # Check DB for unlocked achievements (streak, score, perfect game) if self._pool: try: async with self._pool.acquire() as conn: # Check for score achievements max_score = await conn.fetchval( "SELECT MAX(score) FROM game_sessions WHERE student_id = $1", student_id ) if max_score: for a in achievements: if a.category == "score": a.progress = max_score a.unlocked = max_score >= a.threshold # Check for perfect game perfect = await conn.fetchval( """ SELECT COUNT(*) FROM game_sessions WHERE student_id = $1 AND questions_answered >= 5 AND questions_correct = questions_answered """, student_id ) for a in achievements: if a.id == "perfect_game": a.progress = 100 if perfect and perfect > 0 else 0 a.unlocked = perfect is not None and perfect > 0 except Exception as e: logger.error(f"Failed to check achievements: {e}") return achievements async def check_new_achievements( self, student_id: str, session_score: int, session_accuracy: float, streak: int ) -> List[Achievement]: """ Check for newly unlocked achievements after a session. Returns list of newly unlocked achievements. """ all_achievements = await self.get_student_achievements(student_id) newly_unlocked = [] for a in all_achievements: # Check streak achievements if a.category == "streak" and streak >= a.threshold and not a.unlocked: a.unlocked = True newly_unlocked.append(a) # Check score achievements if a.category == "score" and session_score >= a.threshold and not a.unlocked: a.unlocked = True newly_unlocked.append(a) # Check perfect game if a.id == "perfect_game" and session_accuracy == 1.0: if not a.unlocked: a.unlocked = True newly_unlocked.append(a) return newly_unlocked