Restructure: Move final 16 root files into packages (backend-lehrer)
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 37s
CI / test-go-edu-search (push) Successful in 35s
CI / test-python-klausur (push) Failing after 2m41s
CI / test-python-agent-core (push) Successful in 30s
CI / test-nodejs-website (push) Successful in 38s
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 37s
CI / test-go-edu-search (push) Successful in 35s
CI / test-python-klausur (push) Failing after 2m41s
CI / test-python-agent-core (push) Successful in 30s
CI / test-nodejs-website (push) Successful in 38s
classroom/ (+2): state_engine_api, state_engine_models vocabulary/ (2): api, db worksheets/ (2): api, models services/ (+6): audio, email, translation, claude_vision, ai_processor, story_generator api/ (4): school, klausur_proxy, progress, user_language Only main.py + config.py remain at root. 16 shims added. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
6
backend-lehrer/api/__init__.py
Normal file
6
backend-lehrer/api/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# API Module — thin proxy routers and standalone API endpoints
|
||||
#
|
||||
# api/school.py — Proxy to Go school-service
|
||||
# api/klausur_proxy.py — Proxy to klausur-service
|
||||
# api/progress.py — Student learning progress tracking
|
||||
# api/user_language.py — User language preferences
|
||||
135
backend-lehrer/api/klausur_proxy.py
Normal file
135
backend-lehrer/api/klausur_proxy.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""
|
||||
Klausur-Service API Proxy
|
||||
Routes API requests from /api/klausur/* to the klausur-service microservice
|
||||
"""
|
||||
|
||||
import os
|
||||
import jwt
|
||||
import datetime
|
||||
import httpx
|
||||
from fastapi import APIRouter, Request, HTTPException, Response
|
||||
|
||||
# Klausur Service URL
|
||||
KLAUSUR_SERVICE_URL = os.getenv("KLAUSUR_SERVICE_URL", "http://klausur-service:8086")
|
||||
JWT_SECRET = os.getenv("JWT_SECRET", "your-super-secret-jwt-key-change-in-production")
|
||||
ENVIRONMENT = os.getenv("ENVIRONMENT", "development")
|
||||
|
||||
# Demo teacher UUID for development mode
|
||||
DEMO_TEACHER_ID = "e9484ad9-32ee-4f2b-a4e1-d182e02ccf20"
|
||||
|
||||
router = APIRouter(prefix="/klausur", tags=["klausur"])
|
||||
|
||||
|
||||
def get_demo_token() -> str:
|
||||
"""Generate a demo JWT token for development mode"""
|
||||
payload = {
|
||||
"user_id": DEMO_TEACHER_ID,
|
||||
"email": "demo@breakpilot.app",
|
||||
"role": "admin",
|
||||
"exp": datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=24),
|
||||
"iat": datetime.datetime.now(datetime.timezone.utc)
|
||||
}
|
||||
return jwt.encode(payload, JWT_SECRET, algorithm="HS256")
|
||||
|
||||
|
||||
async def proxy_request(request: Request, path: str) -> Response:
|
||||
"""Forward a request to the klausur service"""
|
||||
url = f"{KLAUSUR_SERVICE_URL}/api/v1{path}"
|
||||
|
||||
# Forward headers, especially Authorization
|
||||
headers = {}
|
||||
if "authorization" in request.headers:
|
||||
headers["Authorization"] = request.headers["authorization"]
|
||||
elif ENVIRONMENT == "development":
|
||||
# In development mode, use demo token if no auth provided
|
||||
demo_token = get_demo_token()
|
||||
headers["Authorization"] = f"Bearer {demo_token}"
|
||||
if "content-type" in request.headers:
|
||||
headers["Content-Type"] = request.headers["content-type"]
|
||||
|
||||
# Get request body for POST/PUT/PATCH/DELETE
|
||||
body = None
|
||||
if request.method in ("POST", "PUT", "PATCH", "DELETE"):
|
||||
body = await request.body()
|
||||
|
||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||
try:
|
||||
response = await client.request(
|
||||
method=request.method,
|
||||
url=url,
|
||||
headers=headers,
|
||||
content=body,
|
||||
params=request.query_params
|
||||
)
|
||||
return Response(
|
||||
content=response.content,
|
||||
status_code=response.status_code,
|
||||
headers=dict(response.headers),
|
||||
media_type=response.headers.get("content-type", "application/json")
|
||||
)
|
||||
except httpx.ConnectError:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Klausur service unavailable"
|
||||
)
|
||||
except httpx.TimeoutException:
|
||||
raise HTTPException(
|
||||
status_code=504,
|
||||
detail="Klausur service timeout"
|
||||
)
|
||||
|
||||
|
||||
# Health check
|
||||
@router.get("/health")
|
||||
async def health():
|
||||
"""Health check for klausur service connection"""
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
try:
|
||||
response = await client.get(f"{KLAUSUR_SERVICE_URL}/health")
|
||||
return {"klausur_service": "healthy", "connected": response.status_code == 200}
|
||||
except Exception:
|
||||
return {"klausur_service": "unhealthy", "connected": False}
|
||||
|
||||
|
||||
# Klausuren
|
||||
@router.api_route("/klausuren", methods=["GET", "POST"])
|
||||
async def klausuren(request: Request):
|
||||
return await proxy_request(request, "/klausuren")
|
||||
|
||||
|
||||
@router.api_route("/klausuren/{klausur_id}", methods=["GET", "PUT", "DELETE"])
|
||||
async def klausur_by_id(klausur_id: str, request: Request):
|
||||
return await proxy_request(request, f"/klausuren/{klausur_id}")
|
||||
|
||||
|
||||
# Students
|
||||
@router.api_route("/klausuren/{klausur_id}/students", methods=["GET", "POST"])
|
||||
async def klausur_students(klausur_id: str, request: Request):
|
||||
return await proxy_request(request, f"/klausuren/{klausur_id}/students")
|
||||
|
||||
|
||||
@router.api_route("/students/{student_id}", methods=["GET", "DELETE"])
|
||||
async def student_by_id(student_id: str, request: Request):
|
||||
return await proxy_request(request, f"/students/{student_id}")
|
||||
|
||||
|
||||
# Grading
|
||||
@router.api_route("/students/{student_id}/criteria", methods=["PUT"])
|
||||
async def update_criteria(student_id: str, request: Request):
|
||||
return await proxy_request(request, f"/students/{student_id}/criteria")
|
||||
|
||||
|
||||
@router.api_route("/students/{student_id}/gutachten", methods=["PUT"])
|
||||
async def update_gutachten(student_id: str, request: Request):
|
||||
return await proxy_request(request, f"/students/{student_id}/gutachten")
|
||||
|
||||
|
||||
@router.api_route("/students/{student_id}/finalize", methods=["POST"])
|
||||
async def finalize_student(student_id: str, request: Request):
|
||||
return await proxy_request(request, f"/students/{student_id}/finalize")
|
||||
|
||||
|
||||
# Grade info
|
||||
@router.get("/grade-info")
|
||||
async def grade_info(request: Request):
|
||||
return await proxy_request(request, "/grade-info")
|
||||
131
backend-lehrer/api/progress.py
Normal file
131
backend-lehrer/api/progress.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
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
|
||||
250
backend-lehrer/api/school.py
Normal file
250
backend-lehrer/api/school.py
Normal file
@@ -0,0 +1,250 @@
|
||||
"""
|
||||
School Service API Proxy
|
||||
Routes API requests from /api/school/* to the Go school-service
|
||||
"""
|
||||
|
||||
import os
|
||||
import jwt
|
||||
import datetime
|
||||
import httpx
|
||||
from fastapi import APIRouter, Request, HTTPException, Response
|
||||
|
||||
# School Service URL
|
||||
SCHOOL_SERVICE_URL = os.getenv("SCHOOL_SERVICE_URL", "http://school-service:8084")
|
||||
JWT_SECRET = os.getenv("JWT_SECRET", "your-super-secret-jwt-key-change-in-production")
|
||||
ENVIRONMENT = os.getenv("ENVIRONMENT", "development")
|
||||
|
||||
# Demo teacher UUID for development mode
|
||||
DEMO_TEACHER_ID = "e9484ad9-32ee-4f2b-a4e1-d182e02ccf20"
|
||||
|
||||
router = APIRouter(prefix="/school", tags=["school"])
|
||||
|
||||
|
||||
def get_demo_token() -> str:
|
||||
"""Generate a demo JWT token for development mode"""
|
||||
payload = {
|
||||
"user_id": DEMO_TEACHER_ID,
|
||||
"email": "demo@breakpilot.app",
|
||||
"role": "admin",
|
||||
"exp": datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=24),
|
||||
"iat": datetime.datetime.now(datetime.timezone.utc)
|
||||
}
|
||||
return jwt.encode(payload, JWT_SECRET, algorithm="HS256")
|
||||
|
||||
|
||||
async def proxy_request(request: Request, path: str) -> Response:
|
||||
"""Forward a request to the school service"""
|
||||
url = f"{SCHOOL_SERVICE_URL}/api/v1/school{path}"
|
||||
|
||||
# Forward headers, especially Authorization
|
||||
headers = {}
|
||||
if "authorization" in request.headers:
|
||||
headers["Authorization"] = request.headers["authorization"]
|
||||
elif ENVIRONMENT == "development":
|
||||
# In development mode, use demo token if no auth provided
|
||||
demo_token = get_demo_token()
|
||||
headers["Authorization"] = f"Bearer {demo_token}"
|
||||
if "content-type" in request.headers:
|
||||
headers["Content-Type"] = request.headers["content-type"]
|
||||
|
||||
# Get request body for POST/PUT/PATCH
|
||||
body = None
|
||||
if request.method in ("POST", "PUT", "PATCH"):
|
||||
body = await request.body()
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
try:
|
||||
response = await client.request(
|
||||
method=request.method,
|
||||
url=url,
|
||||
headers=headers,
|
||||
content=body,
|
||||
params=request.query_params
|
||||
)
|
||||
return Response(
|
||||
content=response.content,
|
||||
status_code=response.status_code,
|
||||
headers=dict(response.headers),
|
||||
media_type=response.headers.get("content-type", "application/json")
|
||||
)
|
||||
except httpx.ConnectError:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="School service unavailable"
|
||||
)
|
||||
except httpx.TimeoutException:
|
||||
raise HTTPException(
|
||||
status_code=504,
|
||||
detail="School service timeout"
|
||||
)
|
||||
|
||||
|
||||
# Health check
|
||||
@router.get("/health")
|
||||
async def health():
|
||||
"""Health check for school service connection"""
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
try:
|
||||
response = await client.get(f"{SCHOOL_SERVICE_URL}/health")
|
||||
return {"school_service": "healthy", "connected": response.status_code == 200}
|
||||
except Exception:
|
||||
return {"school_service": "unhealthy", "connected": False}
|
||||
|
||||
|
||||
# School Years
|
||||
@router.api_route("/years", methods=["GET", "POST"])
|
||||
async def years(request: Request):
|
||||
return await proxy_request(request, "/years")
|
||||
|
||||
|
||||
@router.api_route("/years/{year_id}", methods=["GET", "PUT", "DELETE"])
|
||||
async def year_by_id(request: Request, year_id: str):
|
||||
return await proxy_request(request, f"/years/{year_id}")
|
||||
|
||||
|
||||
# Classes
|
||||
@router.api_route("/classes", methods=["GET", "POST"])
|
||||
async def classes(request: Request):
|
||||
return await proxy_request(request, "/classes")
|
||||
|
||||
|
||||
@router.api_route("/classes/{class_id}", methods=["GET", "PUT", "DELETE"])
|
||||
async def class_by_id(request: Request, class_id: str):
|
||||
return await proxy_request(request, f"/classes/{class_id}")
|
||||
|
||||
|
||||
# Students
|
||||
@router.api_route("/classes/{class_id}/students", methods=["GET", "POST"])
|
||||
async def students(request: Request, class_id: str):
|
||||
return await proxy_request(request, f"/classes/{class_id}/students")
|
||||
|
||||
|
||||
@router.api_route("/classes/{class_id}/students/import", methods=["POST"])
|
||||
async def import_students(request: Request, class_id: str):
|
||||
return await proxy_request(request, f"/classes/{class_id}/students/import")
|
||||
|
||||
|
||||
@router.api_route("/classes/{class_id}/students/{student_id}", methods=["GET", "PUT", "DELETE"])
|
||||
async def student_by_id(request: Request, class_id: str, student_id: str):
|
||||
return await proxy_request(request, f"/classes/{class_id}/students/{student_id}")
|
||||
|
||||
|
||||
# Subjects
|
||||
@router.api_route("/subjects", methods=["GET", "POST"])
|
||||
async def subjects(request: Request):
|
||||
return await proxy_request(request, "/subjects")
|
||||
|
||||
|
||||
@router.api_route("/subjects/{subject_id}", methods=["GET", "PUT", "DELETE"])
|
||||
async def subject_by_id(request: Request, subject_id: str):
|
||||
return await proxy_request(request, f"/subjects/{subject_id}")
|
||||
|
||||
|
||||
# Exams
|
||||
@router.api_route("/exams", methods=["GET", "POST"])
|
||||
async def exams(request: Request):
|
||||
return await proxy_request(request, "/exams")
|
||||
|
||||
|
||||
@router.api_route("/exams/{exam_id}", methods=["GET", "PUT", "DELETE"])
|
||||
async def exam_by_id(request: Request, exam_id: str):
|
||||
return await proxy_request(request, f"/exams/{exam_id}")
|
||||
|
||||
|
||||
@router.api_route("/exams/{exam_id}/results", methods=["GET", "POST"])
|
||||
async def exam_results(request: Request, exam_id: str):
|
||||
return await proxy_request(request, f"/exams/{exam_id}/results")
|
||||
|
||||
|
||||
@router.api_route("/exams/{exam_id}/results/{student_id}", methods=["PUT"])
|
||||
async def exam_result_update(request: Request, exam_id: str, student_id: str):
|
||||
return await proxy_request(request, f"/exams/{exam_id}/results/{student_id}")
|
||||
|
||||
|
||||
@router.api_route("/exams/{exam_id}/results/{student_id}/approve", methods=["PUT"])
|
||||
async def approve_result(request: Request, exam_id: str, student_id: str):
|
||||
return await proxy_request(request, f"/exams/{exam_id}/results/{student_id}/approve")
|
||||
|
||||
|
||||
@router.api_route("/exams/{exam_id}/generate-variant", methods=["POST"])
|
||||
async def generate_variant(request: Request, exam_id: str):
|
||||
return await proxy_request(request, f"/exams/{exam_id}/generate-variant")
|
||||
|
||||
|
||||
# Grades
|
||||
@router.api_route("/grades/{class_id}", methods=["GET"])
|
||||
async def grades_by_class(request: Request, class_id: str):
|
||||
return await proxy_request(request, f"/grades/{class_id}")
|
||||
|
||||
|
||||
@router.api_route("/grades/student/{student_id}", methods=["GET"])
|
||||
async def grades_by_student(request: Request, student_id: str):
|
||||
return await proxy_request(request, f"/grades/student/{student_id}")
|
||||
|
||||
|
||||
@router.api_route("/grades/{student_id}/{subject_id}/oral", methods=["PUT"])
|
||||
async def update_oral_grade(request: Request, student_id: str, subject_id: str):
|
||||
return await proxy_request(request, f"/grades/{student_id}/{subject_id}/oral")
|
||||
|
||||
|
||||
@router.api_route("/grades/calculate", methods=["POST"])
|
||||
async def calculate_grades(request: Request):
|
||||
return await proxy_request(request, "/grades/calculate")
|
||||
|
||||
|
||||
# Attendance
|
||||
@router.api_route("/attendance/{class_id}", methods=["GET"])
|
||||
async def attendance_by_class(request: Request, class_id: str):
|
||||
return await proxy_request(request, f"/attendance/{class_id}")
|
||||
|
||||
|
||||
@router.api_route("/attendance", methods=["POST"])
|
||||
async def create_attendance(request: Request):
|
||||
return await proxy_request(request, "/attendance")
|
||||
|
||||
|
||||
@router.api_route("/attendance/bulk", methods=["POST"])
|
||||
async def bulk_attendance(request: Request):
|
||||
return await proxy_request(request, "/attendance/bulk")
|
||||
|
||||
|
||||
# Gradebook
|
||||
@router.api_route("/gradebook/{class_id}", methods=["GET"])
|
||||
async def gradebook_by_class(request: Request, class_id: str):
|
||||
return await proxy_request(request, f"/gradebook/{class_id}")
|
||||
|
||||
|
||||
@router.api_route("/gradebook", methods=["POST"])
|
||||
async def create_gradebook_entry(request: Request):
|
||||
return await proxy_request(request, "/gradebook")
|
||||
|
||||
|
||||
@router.api_route("/gradebook/{entry_id}", methods=["DELETE"])
|
||||
async def delete_gradebook_entry(request: Request, entry_id: str):
|
||||
return await proxy_request(request, f"/gradebook/{entry_id}")
|
||||
|
||||
|
||||
# Certificates
|
||||
@router.api_route("/certificates/templates", methods=["GET"])
|
||||
async def certificate_templates(request: Request):
|
||||
return await proxy_request(request, "/certificates/templates")
|
||||
|
||||
|
||||
@router.api_route("/certificates/generate", methods=["POST"])
|
||||
async def generate_certificate(request: Request):
|
||||
return await proxy_request(request, "/certificates/generate")
|
||||
|
||||
|
||||
@router.api_route("/certificates/{certificate_id}", methods=["GET"])
|
||||
async def certificate_by_id(request: Request, certificate_id: str):
|
||||
return await proxy_request(request, f"/certificates/{certificate_id}")
|
||||
|
||||
|
||||
@router.api_route("/certificates/{certificate_id}/pdf", methods=["GET"])
|
||||
async def certificate_pdf(request: Request, certificate_id: str):
|
||||
return await proxy_request(request, f"/certificates/{certificate_id}/pdf")
|
||||
|
||||
|
||||
@router.api_route("/certificates/{certificate_id}/finalize", methods=["PUT"])
|
||||
async def finalize_certificate(request: Request, certificate_id: str):
|
||||
return await proxy_request(request, f"/certificates/{certificate_id}/finalize")
|
||||
86
backend-lehrer/api/user_language.py
Normal file
86
backend-lehrer/api/user_language.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
User Language Preferences API — Stores native language + learning level.
|
||||
|
||||
Each user (student, parent, teacher) can set their native language.
|
||||
This drives: UI language, third-language display in flashcards,
|
||||
parent portal language, and translation generation.
|
||||
|
||||
Supported languages: de, en, tr, ar, uk, ru, pl
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/user", tags=["user-language"])
|
||||
|
||||
# Supported native languages with metadata
|
||||
SUPPORTED_LANGUAGES = {
|
||||
"de": {"name": "Deutsch", "name_native": "Deutsch", "flag": "de", "rtl": False},
|
||||
"en": {"name": "English", "name_native": "English", "flag": "gb", "rtl": False},
|
||||
"tr": {"name": "Tuerkisch", "name_native": "Turkce", "flag": "tr", "rtl": False},
|
||||
"ar": {"name": "Arabisch", "name_native": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629", "flag": "sy", "rtl": True},
|
||||
"uk": {"name": "Ukrainisch", "name_native": "\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430", "flag": "ua", "rtl": False},
|
||||
"ru": {"name": "Russisch", "name_native": "\u0420\u0443\u0441\u0441\u043a\u0438\u0439", "flag": "ru", "rtl": False},
|
||||
"pl": {"name": "Polnisch", "name_native": "Polski", "flag": "pl", "rtl": False},
|
||||
}
|
||||
|
||||
# In-memory store (will be replaced with DB later)
|
||||
_preferences: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
|
||||
class LanguagePreference(BaseModel):
|
||||
native_language: str # ISO 639-1 code
|
||||
role: str = "student" # student, parent, teacher
|
||||
learning_level: str = "A1" # A1, A2, B1, B2, C1
|
||||
|
||||
|
||||
@router.get("/languages")
|
||||
def get_supported_languages():
|
||||
"""List all supported native languages with metadata."""
|
||||
return {
|
||||
"languages": [
|
||||
{"code": code, **meta}
|
||||
for code, meta in SUPPORTED_LANGUAGES.items()
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/language-preference")
|
||||
def get_language_preference(user_id: str = Query("default")):
|
||||
"""Get user's language preference."""
|
||||
pref = _preferences.get(user_id)
|
||||
if not pref:
|
||||
return {"user_id": user_id, "native_language": "de", "role": "student", "learning_level": "A1", "is_default": True}
|
||||
return {**pref, "is_default": False}
|
||||
|
||||
|
||||
@router.put("/language-preference")
|
||||
def set_language_preference(
|
||||
pref: LanguagePreference,
|
||||
user_id: str = Query("default"),
|
||||
):
|
||||
"""Set user's native language and learning level."""
|
||||
if pref.native_language not in SUPPORTED_LANGUAGES:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Sprache '{pref.native_language}' nicht unterstuetzt. "
|
||||
f"Verfuegbar: {', '.join(SUPPORTED_LANGUAGES.keys())}",
|
||||
)
|
||||
|
||||
_preferences[user_id] = {
|
||||
"user_id": user_id,
|
||||
"native_language": pref.native_language,
|
||||
"role": pref.role,
|
||||
"learning_level": pref.learning_level,
|
||||
}
|
||||
|
||||
lang_meta = SUPPORTED_LANGUAGES[pref.native_language]
|
||||
logger.info(f"Language preference set: user={user_id} lang={pref.native_language} ({lang_meta['name']})")
|
||||
|
||||
return {**_preferences[user_id], "language_meta": lang_meta}
|
||||
Reference in New Issue
Block a user