[split-required] Split 500-850 LOC files (batch 2)
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>
This commit is contained in:
236
backend-lehrer/game/database_extras.py
Normal file
236
backend-lehrer/game/database_extras.py
Normal file
@@ -0,0 +1,236 @@
|
||||
# ==============================================
|
||||
# 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
|
||||
Reference in New Issue
Block a user