This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +01:00

249 lines
8.4 KiB
Python

"""
Klausur-Service Fairness Routes
Endpoints for fairness analysis, audit logs, and utility functions.
"""
from typing import Optional
import httpx
from fastapi import APIRouter, HTTPException, Request
from models.grading import GRADE_THRESHOLDS, GRADE_LABELS, DEFAULT_CRITERIA
from services.auth_service import get_current_user
from config import SCHOOL_SERVICE_URL
import storage
router = APIRouter()
@router.get("/api/v1/klausuren/{klausur_id}/fairness")
async def get_fairness_analysis(klausur_id: str, request: Request):
"""Analyze grading fairness across all students in a Klausur."""
user = get_current_user(request)
if klausur_id not in storage.klausuren_db:
raise HTTPException(status_code=404, detail="Klausur not found")
klausur = storage.klausuren_db[klausur_id]
students = klausur.students
if len(students) < 2:
return {
"klausur_id": klausur_id,
"analysis": "Zu wenige Arbeiten fuer Vergleich (mindestens 2 benoetigt)",
"students_count": len(students)
}
# Calculate statistics
grades = [s.grade_points for s in students if s.grade_points > 0]
raw_points = [s.raw_points for s in students if s.raw_points > 0]
if not grades:
return {
"klausur_id": klausur_id,
"analysis": "Keine bewerteten Arbeiten vorhanden",
"students_count": len(students)
}
avg_grade = sum(grades) / len(grades)
avg_raw = sum(raw_points) / len(raw_points) if raw_points else 0
min_grade = min(grades)
max_grade = max(grades)
spread = max_grade - min_grade
# Calculate standard deviation
variance = sum((g - avg_grade) ** 2 for g in grades) / len(grades)
std_dev = variance ** 0.5
# Identify outliers (more than 2 std deviations from mean)
outliers = []
for s in students:
if s.grade_points > 0:
deviation = abs(s.grade_points - avg_grade)
if deviation > 2 * std_dev:
outliers.append({
"student_id": s.id,
"student_name": s.student_name,
"grade_points": s.grade_points,
"deviation": round(deviation, 2),
"direction": "above" if s.grade_points > avg_grade else "below"
})
# Criteria comparison
criteria_averages = {}
for criterion in DEFAULT_CRITERIA.keys():
scores = []
for s in students:
if criterion in s.criteria_scores:
scores.append(s.criteria_scores[criterion].get("score", 0))
if scores:
criteria_averages[criterion] = {
"average": round(sum(scores) / len(scores), 1),
"min": min(scores),
"max": max(scores),
"count": len(scores)
}
# Fairness score (0-100, higher is more consistent)
# Based on: low std deviation, no extreme outliers, reasonable spread
fairness_score = 100
if std_dev > 3:
fairness_score -= min(30, std_dev * 5)
if len(outliers) > 0:
fairness_score -= len(outliers) * 10
if spread > 10:
fairness_score -= min(20, spread)
fairness_score = max(0, fairness_score)
# Warnings
warnings = []
if spread > 12:
warnings.append("Sehr grosse Notenspreizung - bitte Extremwerte pruefen")
if std_dev > 4:
warnings.append("Hohe Streuung der Noten - moegliche Inkonsistenz")
if len(outliers) > 2:
warnings.append(f"{len(outliers)} Ausreisser erkannt - manuelle Pruefung empfohlen")
if avg_grade < 5:
warnings.append("Durchschnitt unter 5 Punkten - sind die Aufgaben angemessen?")
if avg_grade > 12:
warnings.append("Durchschnitt ueber 12 Punkten - Bewertungsmassstab pruefen")
return {
"klausur_id": klausur_id,
"students_count": len(students),
"graded_count": len(grades),
"statistics": {
"average_grade": round(avg_grade, 2),
"average_raw_points": round(avg_raw, 2),
"min_grade": min_grade,
"max_grade": max_grade,
"spread": spread,
"standard_deviation": round(std_dev, 2)
},
"criteria_breakdown": criteria_averages,
"outliers": outliers,
"fairness_score": round(fairness_score),
"warnings": warnings,
"recommendation": "Bewertung konsistent" if fairness_score >= 70 else "Pruefung empfohlen"
}
# =============================================
# AUDIT LOG ENDPOINTS
# =============================================
@router.get("/api/v1/audit-log")
async def get_audit_log(
request: Request,
entity_type: Optional[str] = None,
entity_id: Optional[str] = None,
limit: int = 100
):
"""Get audit log entries."""
user = get_current_user(request)
# Only admins can see full audit log
if user.get("role") != "admin":
# Regular users only see their own actions
entries = [e for e in storage.audit_log_db if e.user_id == user["user_id"]]
else:
entries = storage.audit_log_db
# Filter by entity
if entity_type:
entries = [e for e in entries if e.entity_type == entity_type]
if entity_id:
entries = [e for e in entries if e.entity_id == entity_id]
# Sort by timestamp descending and limit
entries = sorted(entries, key=lambda e: e.timestamp, reverse=True)[:limit]
return [e.to_dict() for e in entries]
@router.get("/api/v1/students/{student_id}/audit-log")
async def get_student_audit_log(student_id: str, request: Request):
"""Get audit log for a specific student."""
user = get_current_user(request)
if student_id not in storage.students_db:
raise HTTPException(status_code=404, detail="Student work not found")
entries = [e for e in storage.audit_log_db if e.entity_id == student_id]
entries = sorted(entries, key=lambda e: e.timestamp, reverse=True)
return [e.to_dict() for e in entries]
# =============================================
# UTILITY ENDPOINTS
# =============================================
@router.get("/api/v1/grade-info")
async def get_grade_info():
"""Get grade thresholds and labels."""
return {
"thresholds": GRADE_THRESHOLDS,
"labels": GRADE_LABELS,
"criteria": DEFAULT_CRITERIA
}
# =============================================
# SCHOOL CLASSES PROXY
# =============================================
@router.get("/api/school/classes")
async def get_school_classes(request: Request):
"""Proxy to school-service or return demo data."""
try:
auth_header = request.headers.get("Authorization", "")
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{SCHOOL_SERVICE_URL}/api/v1/school/classes",
headers={"Authorization": auth_header},
timeout=5.0
)
if resp.status_code == 200:
return resp.json()
except Exception as e:
print(f"School service not available: {e}")
# Return demo classes if school service unavailable
return [
{"id": "demo-q1", "name": "Q1 Deutsch GK", "student_count": 24},
{"id": "demo-q2", "name": "Q2 Deutsch LK", "student_count": 18},
{"id": "demo-q3", "name": "Q3 Deutsch GK", "student_count": 22},
{"id": "demo-q4", "name": "Q4 Deutsch LK", "student_count": 15}
]
@router.get("/api/school/classes/{class_id}/students")
async def get_class_students(class_id: str, request: Request):
"""Proxy to school-service or return demo data."""
try:
auth_header = request.headers.get("Authorization", "")
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{SCHOOL_SERVICE_URL}/api/v1/school/classes/{class_id}/students",
headers={"Authorization": auth_header},
timeout=5.0
)
if resp.status_code == 200:
return resp.json()
except Exception as e:
print(f"School service not available: {e}")
# Return demo students
demo_names = [
"Anna Mueller", "Ben Schmidt", "Clara Weber", "David Fischer",
"Emma Schneider", "Felix Wagner", "Greta Becker", "Hans Hoffmann",
"Ida Schulz", "Jan Koch", "Katharina Bauer", "Leon Richter",
"Maria Braun", "Niklas Lange", "Olivia Wolf", "Paul Krause"
]
return [
{"id": f"student-{i}", "name": name, "class_id": class_id}
for i, name in enumerate(demo_names)
]