backend-lehrer (10 files): - game/database.py (785 → 5), correction_api.py (683 → 4) - classroom_engine/antizipation.py (676 → 5) - llm_gateway schools/edu_search already done in prior batch klausur-service (12 files): - orientation_crop_api.py (694 → 5), pdf_export.py (677 → 4) - zeugnis_crawler.py (676 → 5), grid_editor_api.py (671 → 5) - eh_templates.py (658 → 5), mail/api.py (651 → 5) - qdrant_service.py (638 → 5), training_api.py (625 → 4) website (6 pages): - middleware (696 → 8), mail (733 → 6), consent (628 → 8) - compliance/risks (622 → 5), export (502 → 5), brandbook (629 → 7) studio-v2 (3 components): - B2BMigrationWizard (848 → 3), CleanupPanel (765 → 2) - dashboard-experimental (739 → 2) admin-lehrer (4 files): - uebersetzungen (769 → 4), manager (670 → 2) - ChunkBrowserQA (675 → 6), dsfa/page (674 → 5) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
237 lines
8.6 KiB
Python
237 lines
8.6 KiB
Python
# ==============================================
|
|
# 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
|