Python (6 files in klausur-service): - rbac.py (1,132 → 4), admin_api.py (1,012 → 4) - routes/eh.py (1,111 → 4), ocr_pipeline_geometry.py (1,105 → 5) Python (2 files in backend-lehrer): - unit_api.py (1,226 → 6), game_api.py (1,129 → 5) Website (6 page files): - 4x klausur-korrektur pages (1,249-1,328 LOC each) → shared components in website/components/klausur-korrektur/ (17 shared files) - companion (1,057 → 10), magic-help (1,017 → 8) All re-export barrels preserve backward compatibility. Zero import errors verified. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
190 lines
5.6 KiB
Python
190 lines
5.6 KiB
Python
# ==============================================
|
|
# Breakpilot Drive - Game Extended Routes
|
|
# ==============================================
|
|
# Phase 5 features: achievements, progress, parent dashboard,
|
|
# class leaderboard, and display leaderboard.
|
|
# Extracted from game_api.py for file-size compliance.
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, Depends, Request
|
|
from typing import List, Optional, Dict, Any
|
|
import logging
|
|
|
|
from game_routes import (
|
|
get_optional_current_user,
|
|
get_user_id_from_auth,
|
|
get_game_database,
|
|
REQUIRE_AUTH,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/game", tags=["Breakpilot Drive"])
|
|
|
|
|
|
# ==============================================
|
|
# Phase 5: Erweiterte Features
|
|
# ==============================================
|
|
|
|
@router.get("/achievements/{user_id}")
|
|
async def get_achievements(
|
|
user_id: str,
|
|
user: Optional[Dict[str, Any]] = Depends(get_optional_current_user)
|
|
) -> dict:
|
|
"""
|
|
Gibt Achievements mit Fortschritt fuer einen Benutzer zurueck.
|
|
|
|
Achievements werden basierend auf Spielstatistiken berechnet.
|
|
"""
|
|
# Verify access rights
|
|
user_id = get_user_id_from_auth(user, user_id)
|
|
|
|
db = await get_game_database()
|
|
if not db:
|
|
return {"achievements": [], "message": "Database not available"}
|
|
|
|
try:
|
|
achievements = await db.get_student_achievements(user_id)
|
|
|
|
unlocked = [a for a in achievements if a.unlocked]
|
|
locked = [a for a in achievements if not a.unlocked]
|
|
|
|
return {
|
|
"user_id": user_id,
|
|
"total": len(achievements),
|
|
"unlocked_count": len(unlocked),
|
|
"achievements": [
|
|
{
|
|
"id": a.id,
|
|
"name": a.name,
|
|
"description": a.description,
|
|
"icon": a.icon,
|
|
"category": a.category,
|
|
"threshold": a.threshold,
|
|
"progress": a.progress,
|
|
"unlocked": a.unlocked,
|
|
}
|
|
for a in achievements
|
|
]
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to get achievements: {e}")
|
|
return {"achievements": [], "message": str(e)}
|
|
|
|
|
|
@router.get("/progress/{user_id}")
|
|
async def get_progress(
|
|
user_id: str,
|
|
days: int = Query(30, ge=7, le=90, description="Anzahl Tage zurueck"),
|
|
user: Optional[Dict[str, Any]] = Depends(get_optional_current_user)
|
|
) -> dict:
|
|
"""
|
|
Gibt Lernfortschritt ueber Zeit zurueck (fuer Charts).
|
|
|
|
- Taegliche Statistiken
|
|
- Fuer Eltern-Dashboard und Fortschrittsanzeige
|
|
"""
|
|
# Verify access rights
|
|
user_id = get_user_id_from_auth(user, user_id)
|
|
|
|
db = await get_game_database()
|
|
if not db:
|
|
return {"progress": [], "message": "Database not available"}
|
|
|
|
try:
|
|
progress = await db.get_progress_over_time(user_id, days)
|
|
return {
|
|
"user_id": user_id,
|
|
"days": days,
|
|
"data_points": len(progress),
|
|
"progress": progress,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to get progress: {e}")
|
|
return {"progress": [], "message": str(e)}
|
|
|
|
|
|
@router.get("/parent/children")
|
|
async def get_children_dashboard(
|
|
user: Optional[Dict[str, Any]] = Depends(get_optional_current_user)
|
|
) -> dict:
|
|
"""
|
|
Eltern-Dashboard: Statistiken fuer alle Kinder.
|
|
|
|
Erfordert Auth mit Eltern-Rolle und children_ids Claim.
|
|
"""
|
|
if not REQUIRE_AUTH or user is None:
|
|
return {
|
|
"message": "Auth required for parent dashboard",
|
|
"children": []
|
|
}
|
|
|
|
# Get children IDs from token
|
|
children_ids = user.get("raw_claims", {}).get("children_ids", [])
|
|
|
|
if not children_ids:
|
|
return {
|
|
"message": "No children associated with this account",
|
|
"children": []
|
|
}
|
|
|
|
db = await get_game_database()
|
|
if not db:
|
|
return {"children": [], "message": "Database not available"}
|
|
|
|
try:
|
|
children_stats = await db.get_children_stats(children_ids)
|
|
return {
|
|
"parent_id": user.get("user_id"),
|
|
"children_count": len(children_ids),
|
|
"children": children_stats,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to get children stats: {e}")
|
|
return {"children": [], "message": str(e)}
|
|
|
|
|
|
@router.get("/leaderboard/class/{class_id}")
|
|
async def get_class_leaderboard(
|
|
class_id: str,
|
|
timeframe: str = Query("week", description="day, week, month, all"),
|
|
limit: int = Query(10, ge=1, le=50),
|
|
user: Optional[Dict[str, Any]] = Depends(get_optional_current_user)
|
|
) -> List[dict]:
|
|
"""
|
|
Klassenspezifische Rangliste.
|
|
|
|
Nur fuer Lehrer oder Schueler der Klasse sichtbar.
|
|
"""
|
|
db = await get_game_database()
|
|
if not db:
|
|
return []
|
|
|
|
try:
|
|
leaderboard = await db.get_class_leaderboard(class_id, timeframe, limit)
|
|
return leaderboard
|
|
except Exception as e:
|
|
logger.error(f"Failed to get class leaderboard: {e}")
|
|
return []
|
|
|
|
|
|
@router.get("/leaderboard/display")
|
|
async def get_display_leaderboard(
|
|
timeframe: str = Query("day", description="day, week, month, all"),
|
|
limit: int = Query(10, ge=1, le=100),
|
|
anonymize: bool = Query(True, description="Namen anonymisieren")
|
|
) -> List[dict]:
|
|
"""
|
|
Oeffentliche Rangliste mit Anzeigenamen.
|
|
|
|
Standardmaessig anonymisiert fuer Datenschutz.
|
|
"""
|
|
db = await get_game_database()
|
|
if not db:
|
|
return []
|
|
|
|
try:
|
|
return await db.get_leaderboard_with_names(timeframe, limit, anonymize)
|
|
except Exception as e:
|
|
logger.error(f"Failed to get display leaderboard: {e}")
|
|
return []
|